Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/google-tool-choice-none-response-metadata.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@langchain/google": patch
---

fix(google): tool_choice none with empty tools, response metadata, structured output format
15 changes: 15 additions & 0 deletions libs/providers/langchain-google/src/chat_models/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,10 @@ export abstract class BaseChatGoogle<
// Use the first candidate
const candidate = data.candidates[0];
const message = convertGeminiCandidateToAIMessage(candidate);
message.response_metadata = {
...message.response_metadata,
...(data.modelVersion ? { model: data.modelVersion } : {}),
};

// Extract text content from the message
const text = convertAIMessageToText(message);
Expand Down Expand Up @@ -595,6 +599,9 @@ export abstract class BaseChatGoogle<
tool_calls: toolCalls,
response_metadata: {
model_provider: "google",
...(chunk.modelVersion
? { model: chunk.modelVersion }
: {}),
},
additional_kwargs: {
...(message.additional_kwargs.originalTextContentBlock
Expand Down Expand Up @@ -766,6 +773,10 @@ export abstract class BaseChatGoogle<
// Use JSON mode with responseSchema
llm = this.withConfig({
responseSchema: schema,
ls_structured_output_format: {
kwargs: { method: "jsonSchema" },
schema: toJsonSchema(schema),
},
} as Partial<CallOptions>);

outputParser = RunnableLambda.from<BaseMessage, RunOutput>(
Expand Down Expand Up @@ -831,6 +842,10 @@ export abstract class BaseChatGoogle<
];
llm = this.bindTools(tools).withConfig({
tool_choice: functionName,
ls_structured_output_format: {
kwargs: { method: "functionCalling" },
schema: toJsonSchema(schema),
},
} as Partial<CallOptions>);

outputParser = createFunctionCallingParser(schema, functionName);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,18 @@ describe("convertToolChoiceToGeminiConfig", () => {
expect(result).toBeUndefined();
});

test("returns undefined when hasTools is false", () => {
test("returns undefined when hasTools is false and toolChoice is not none", () => {
const result = convertToolChoiceToGeminiConfig("auto", false);
expect(result).toBeUndefined();
});

test('maps "none" to NONE mode even when hasTools is false', () => {
const result = convertToolChoiceToGeminiConfig("none", false);
expect(result).toEqual({
functionCallingConfig: { mode: "NONE" },
});
});

test('maps "auto" to AUTO mode', () => {
const result = convertToolChoiceToGeminiConfig("auto", true);
expect(result).toEqual({
Expand Down
20 changes: 15 additions & 5 deletions libs/providers/langchain-google/src/converters/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -421,7 +421,8 @@ export const convertToolsToGeminiTools: Converter<
*
* @remarks
* The conversion logic:
* - If `hasTools` is `false`, returns `undefined` (no config needed)
* - If `hasTools` is `false` and `toolChoice` is not `"none"`, returns `undefined` (no config needed)
* - `"none"` is always honoured, even without tools, to explicitly prevent function calling
* - If `toolChoice` is provided, converts it to a Gemini mode:
* - `"auto"` → `"AUTO"` - Model decides whether to call functions
* - `"any"` or `"required"` → `"ANY"` - Model must call at least one function
Expand Down Expand Up @@ -454,20 +455,29 @@ export const convertToolsToGeminiTools: Converter<
* convertToolChoiceToGeminiConfig(undefined, true);
* // Returns: { functionCallingConfig: { mode: "AUTO" } }
*
* // No tools - returns undefined
* // No tools - returns undefined (except for "none")
* convertToolChoiceToGeminiConfig("auto", false);
* // Returns: undefined
*
* // "none" without tools - still returns NONE config
* convertToolChoiceToGeminiConfig("none", false);
* // Returns: { functionCallingConfig: { mode: "NONE" } }
* ```
*
* @param toolChoice - The tool choice option from LangChain (string, object, or undefined)
* @param hasTools - Whether tools are present. If false, returns undefined
* @returns The Gemini tool configuration object, or undefined if no tools are present
* @param hasTools - Whether tools are present. If false, returns undefined unless toolChoice is "none"
* @returns The Gemini tool configuration object, or undefined if not applicable
*/
export function convertToolChoiceToGeminiConfig(
toolChoice: ToolChoice | undefined,
hasTools: boolean
): Gemini.Tools.ToolConfig | undefined {
if (!hasTools || toolChoice === undefined) {
if (toolChoice === undefined) {
return undefined;
}
// Only NONE is meaningful without tools — it explicitly prevents function
// calling even when no declarations are present (e.g. bindTools([], { tool_choice: 'none' })).
if (!hasTools && toolChoice !== "none") {
return undefined;
}

Expand Down