Skip to content

Commit 16b6aae

Browse files
committed
fix(google-common): set function calling mode to auto when responseMimeType is application/json
1 parent 26a1ce6 commit 16b6aae

File tree

4 files changed

+94
-11
lines changed

4 files changed

+94
-11
lines changed

libs/langchain-google-common/src/chat_models.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import {
3939
GoogleSearchToolSetting,
4040
GoogleSpeechConfig,
4141
GeminiJsonSchema,
42+
GoogleAIResponseMimeType,
4243
} from "./types.js";
4344
import {
4445
convertToGeminiTools,
@@ -220,6 +221,8 @@ export abstract class ChatGoogleBase<AuthOptions>
220221

221222
responseModalities?: GoogleAIModelModality[];
222223

224+
responseMimeType?: GoogleAIResponseMimeType;
225+
223226
// May intentionally be undefined, meaning to compute this.
224227
convertSystemMessageToHumanContent: boolean | undefined;
225228

@@ -455,6 +458,10 @@ export abstract class ChatGoogleBase<AuthOptions>
455458
config?: StructuredOutputMethodOptions<true>
456459
): Runnable<BaseLanguageModelInput, { raw: BaseMessage; parsed: RunOutput }>;
457460

461+
/**
462+
* Creates a structured output version that enforces a specific schema.
463+
* Automatically sets responseMimeType to "application/json" for optimal function calling.
464+
*/
458465
withStructuredOutput<
459466
// eslint-disable-next-line @typescript-eslint/no-explicit-any
460467
RunOutput extends Record<string, any> = Record<string, any>
@@ -530,7 +537,13 @@ export abstract class ChatGoogleBase<AuthOptions>
530537
keyName: functionName,
531538
});
532539
}
533-
const llm = this.bindTools(tools).withConfig({ tool_choice: functionName });
540+
// Configure for structured output: set responseMimeType to "application/json"
541+
// This ensures the function calling mode is set to "auto" instead of "any"
542+
// for optimal performance when extracting structured data
543+
const llm = this.bindTools(tools).withConfig({
544+
tool_choice: functionName,
545+
responseMimeType: "application/json"
546+
});
534547

535548
if (!includeRaw) {
536549
return llm.pipe(outputParser).withConfig({

libs/langchain-google-common/src/tests/chat_models.test.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1739,6 +1739,69 @@ describe("Mock ChatGoogle - Gemini", () => {
17391739
expect(func.parameters?.properties?.greeterName?.nullable).toEqual(true);
17401740
});
17411741

1742+
test("4. Functions withStructuredOutput - default behavior", async () => {
1743+
const record: Record<string, any> = {};
1744+
const projectId = mockId();
1745+
const authOptions: MockClientAuthInfo = {
1746+
record,
1747+
projectId,
1748+
resultFile: "chat-4-mock.json",
1749+
};
1750+
1751+
const schema = {
1752+
type: "object",
1753+
properties: {
1754+
name: { type: "string", description: "The person's name" },
1755+
age: { type: "number", description: "The person's age" },
1756+
},
1757+
required: ["name", "age"],
1758+
};
1759+
1760+
// No responseMimeType set - should default to undefined
1761+
const model = new ChatGoogle({ authOptions }).withStructuredOutput(schema);
1762+
await model.invoke("Extract info about John Doe, software engineer");
1763+
1764+
const toolConfig = record?.opts?.data?.toolConfig;
1765+
expect(toolConfig.functionCallingConfig.mode).toEqual("auto");
1766+
expect(toolConfig.functionCallingConfig.allowedFunctionNames).toEqual(["extract"]);
1767+
1768+
const generationConfig = record?.opts?.data?.generationConfig;
1769+
expect(generationConfig.responseMimeType).toEqual("application/json");
1770+
});
1771+
1772+
test("4. Functions withStructuredOutput - overrides user setting", async () => {
1773+
const record: Record<string, any> = {};
1774+
const projectId = mockId();
1775+
const authOptions: MockClientAuthInfo = {
1776+
record,
1777+
projectId,
1778+
resultFile: "chat-4-mock.json",
1779+
};
1780+
1781+
const schema = {
1782+
type: "object",
1783+
properties: {
1784+
sender: { type: "string", description: "Email sender" },
1785+
subject: { type: "string", description: "Email subject" },
1786+
},
1787+
required: ["sender", "subject"],
1788+
};
1789+
1790+
// User explicitly sets text/plain - withStructuredOutput should override it
1791+
const model = new ChatGoogle({
1792+
authOptions,
1793+
responseMimeType: "text/plain"
1794+
}).withStructuredOutput(schema);
1795+
await model.invoke("Analyze email from [email protected], subject: Project Update");
1796+
1797+
const toolConfig = record?.opts?.data?.toolConfig;
1798+
expect(toolConfig.functionCallingConfig.mode).toEqual("auto");
1799+
expect(toolConfig.functionCallingConfig.allowedFunctionNames).toEqual(["extract"]);
1800+
1801+
const generationConfig = record?.opts?.data?.generationConfig;
1802+
expect(generationConfig.responseMimeType).toEqual("application/json");
1803+
});
1804+
17421805
test("4. Functions - results", async () => {
17431806
const record: Record<string, any> = {};
17441807
const projectId = mockId();

libs/langchain-google-common/src/utils/common.ts

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,17 @@ export function copyAIModelParams(
3131
}
3232

3333
function processToolChoice(
34-
toolChoice: GoogleAIBaseLanguageModelCallOptions["tool_choice"],
35-
allowedFunctionNames: GoogleAIBaseLanguageModelCallOptions["allowed_function_names"]
34+
options: GoogleAIBaseLanguageModelCallOptions | undefined
3635
):
3736
| {
3837
tool_choice: "any" | "auto" | "none";
3938
allowed_function_names?: string[];
4039
}
4140
| undefined {
41+
const toolChoice = options?.tool_choice;
42+
const allowedFunctionNames = options?.allowed_function_names;
43+
const responseMimeType = options?.responseMimeType;
44+
4245
if (!toolChoice) {
4346
if (allowedFunctionNames) {
4447
// Allowed func names is passed, return 'any' so it forces the model to use a tool.
@@ -58,9 +61,11 @@ function processToolChoice(
5861
}
5962
if (typeof toolChoice === "string") {
6063
// String representing the function name.
61-
// Return any to force the model to predict the specified function call.
64+
// Use "auto" mode when responseMimeType is "application/json" for optimal performance
65+
const isJsonMode = responseMimeType === "application/json";
66+
const mode = isJsonMode ? "auto" : "any";
6267
return {
63-
tool_choice: "any",
68+
tool_choice: mode,
6469
allowed_function_names: [...(allowedFunctionNames ?? []), toolChoice],
6570
};
6671
}
@@ -205,10 +210,10 @@ export function copyAIModelParamsInto(
205210
options?.speechConfig ?? params?.speechConfig ?? target?.speechConfig
206211
);
207212
ret.streaming = options?.streaming ?? params?.streaming ?? target?.streaming;
208-
const toolChoice = processToolChoice(
209-
options?.tool_choice,
210-
options?.allowed_function_names
211-
);
213+
const toolChoice = processToolChoice({
214+
...options,
215+
responseMimeType: options?.responseMimeType ?? params?.responseMimeType ?? target?.responseMimeType
216+
});
212217
if (toolChoice) {
213218
ret.tool_choice = toolChoice.tool_choice;
214219
ret.allowed_function_names = toolChoice.allowed_function_names;

libs/langchain-google-common/src/utils/gemini.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1843,10 +1843,12 @@ export function getGeminiAPI(config?: GeminiAPIConfig): GoogleAIAPI {
18431843
};
18441844
}
18451845

1846-
// force tool choice to be a single function name in case of structured output
1846+
const isJsonMode = parameters.responseMimeType === "application/json";
1847+
const mode = isJsonMode ? "auto" : "any";
1848+
18471849
return {
18481850
functionCallingConfig: {
1849-
mode: "any",
1851+
mode,
18501852
allowedFunctionNames: [parameters.tool_choice],
18511853
},
18521854
};

0 commit comments

Comments
 (0)