Skip to content

Commit 75e9d56

Browse files
authored
fix: support structured output (providerStrategy) for Google Gemini models in createAgent (langchain-ai#10489)
2 parents 0c14206 + 21094f3 commit 75e9d56

File tree

5 files changed

+59
-83
lines changed

5 files changed

+59
-83
lines changed

.changeset/nasty-cars-fold.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"langchain": patch
3+
---
4+
5+
support structured output (providerStrategy) for Google Gemini models in createAgent

libs/langchain/src/agents/nodes/AgentNode.ts

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
hasToolCalls,
3232
isClientTool,
3333
} from "../utils.js";
34+
import { isConfigurableModel } from "../model.js";
3435
import { mergeAbortSignals, toPartialZodObject } from "../nodes/utils.js";
3536
import { CreateAgentParams } from "../types.js";
3637
import type { InternalAgentState, Runtime } from "../runtime.js";
@@ -164,17 +165,28 @@ export class AgentNode<
164165
* @param model - The model to get the response format for.
165166
* @returns The response format.
166167
*/
167-
#getResponseFormat(
168+
async #getResponseFormat(
168169
model: string | LanguageModelLike
169-
): ResponseFormat | undefined {
170+
): Promise<ResponseFormat | undefined> {
170171
if (!this.#options.responseFormat) {
171172
return undefined;
172173
}
173174

175+
let resolvedModel: LanguageModelLike | undefined;
176+
if (isConfigurableModel(model)) {
177+
resolvedModel = await (
178+
model as unknown as {
179+
_getModelInstance: () => Promise<LanguageModelLike>;
180+
}
181+
)._getModelInstance();
182+
} else if (typeof model !== "string") {
183+
resolvedModel = model;
184+
}
185+
174186
const strategies = transformResponseFormat(
175187
this.#options.responseFormat,
176188
undefined,
177-
model
189+
resolvedModel
178190
);
179191

180192
/**
@@ -350,7 +362,9 @@ export class AgentNode<
350362
*/
351363
validateLLMHasNoBoundTools(request.model);
352364

353-
const structuredResponseFormat = this.#getResponseFormat(request.model);
365+
const structuredResponseFormat = await this.#getResponseFormat(
366+
request.model
367+
);
354368
const modelWithTools = await this.#bindTools(
355369
request.model,
356370
request,
@@ -943,6 +957,12 @@ export class AgentNode<
943957
},
944958
},
945959

960+
/**
961+
* Google-style options
962+
* Used by ChatGoogle and other Gemini-based providers.
963+
*/
964+
responseSchema: structuredResponseFormat.strategy.schema,
965+
946966
/**
947967
* for LangSmith structured output tracing
948968
*/

libs/langchain/src/agents/responses.ts

Lines changed: 18 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {
1919
StructuredOutputParsingError,
2020
MultipleStructuredOutputsError,
2121
} from "./errors.js";
22-
import { isConfigurableModel, isBaseChatModel } from "./model.js";
22+
import { isBaseChatModel } from "./model.js";
2323

2424
/**
2525
* Special type to indicate that no response format is provided.
@@ -311,7 +311,7 @@ export function transformResponseFormat(
311311
| ToolStrategy<any>[]
312312
| ResponseFormatUndefined,
313313
options?: ToolStrategyOptions,
314-
model?: LanguageModelLike | string
314+
model?: LanguageModelLike
315315
): ResponseFormat[] {
316316
if (!responseFormat) {
317317
return [];
@@ -675,76 +675,29 @@ export type JsonSchemaFormat = {
675675
__brand?: never;
676676
};
677677

678-
const CHAT_MODELS_THAT_SUPPORT_JSON_SCHEMA_OUTPUT = ["ChatOpenAI", "ChatXAI"];
679-
const MODEL_NAMES_THAT_SUPPORT_JSON_SCHEMA_OUTPUT = [
680-
"grok",
681-
"gpt-5",
682-
"gpt-4.1",
683-
"gpt-4o",
684-
"gpt-oss",
685-
"o3-pro",
686-
"o3-mini",
687-
];
688-
689678
/**
690-
* Identifies the models that support JSON schema output
691-
* @param model - The model to check
679+
* Identifies the models that support JSON schema output by reading
680+
* the model's profile metadata.
681+
*
682+
* @param model - A resolved model instance to check. Callers should resolve
683+
* string model names and ConfigurableModel wrappers before calling this.
692684
* @returns True if the model supports JSON schema output, false otherwise
693685
*/
694686
export function hasSupportForJsonSchemaOutput(
695-
model?: LanguageModelLike | string
687+
model?: LanguageModelLike
696688
): boolean {
697-
if (!model) {
698-
return false;
699-
}
700-
701-
if (typeof model === "string") {
702-
const modelName = model.split(":").pop() as string;
703-
return MODEL_NAMES_THAT_SUPPORT_JSON_SCHEMA_OUTPUT.some(
704-
(modelNameSnippet) => modelName.includes(modelNameSnippet)
705-
);
706-
}
707-
708-
if (isConfigurableModel(model)) {
709-
const configurableModel = model as unknown as {
710-
_defaultConfig: { model: string };
711-
};
712-
return hasSupportForJsonSchemaOutput(
713-
configurableModel._defaultConfig.model
714-
);
715-
}
716-
717-
if (!isBaseChatModel(model)) {
718-
return false;
719-
}
720-
721-
const chatModelClass = model.getName();
722-
723-
/**
724-
* for testing purposes only
725-
*/
726-
if (chatModelClass === "FakeToolCallingChatModel") {
727-
return true;
728-
}
729-
730689
if (
731-
CHAT_MODELS_THAT_SUPPORT_JSON_SCHEMA_OUTPUT.includes(chatModelClass) &&
732-
/**
733-
* OpenAI models
734-
*/ (("model" in model &&
735-
MODEL_NAMES_THAT_SUPPORT_JSON_SCHEMA_OUTPUT.some(
736-
(modelNameSnippet) =>
737-
typeof model.model === "string" &&
738-
model.model.includes(modelNameSnippet)
739-
)) ||
740-
/**
741-
* for testing purposes only
742-
*/
743-
(chatModelClass === "FakeToolCallingModel" &&
744-
"structuredResponse" in model))
690+
!model ||
691+
!isBaseChatModel(model) ||
692+
!("profile" in model) ||
693+
typeof model.profile !== "object" ||
694+
!model.profile
745695
) {
746-
return true;
696+
return false;
747697
}
748698

749-
return false;
699+
return (
700+
"structuredOutput" in model.profile &&
701+
model.profile.structuredOutput === true
702+
);
750703
}

libs/langchain/src/agents/tests/responses.test.ts

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -687,42 +687,32 @@ describe("hasSupportForJsonSchemaOutput", () => {
687687
expect(hasSupportForJsonSchemaOutput(undefined)).toBe(false);
688688
});
689689

690-
it("should return true for models that support JSON schema output", () => {
690+
it("should use model.profile.structuredOutput to determine support", () => {
691691
const model = new FakeToolCallingModel({});
692692
expect(hasSupportForJsonSchemaOutput(model)).toBe(false);
693693
const model2 = new FakeToolCallingChatModel({});
694694
expect(hasSupportForJsonSchemaOutput(model2)).toBe(true);
695695
});
696696

697-
it("should return true for OpenAI models that support JSON schema output", () => {
697+
it("should return true for OpenAI models whose profile reports structuredOutput", () => {
698698
const model = new ChatOpenAI({
699699
model: "gpt-4o",
700700
});
701701
expect(hasSupportForJsonSchemaOutput(model)).toBe(true);
702-
expect(hasSupportForJsonSchemaOutput("openai:gpt-4o")).toBe(true);
703-
expect(hasSupportForJsonSchemaOutput("gpt-4o-mini")).toBe(true);
704702
});
705703

706-
it("should return false for OpenAI models that do not support JSON schema output", () => {
704+
it("should return false for OpenAI models whose profile does not report structuredOutput", () => {
707705
const model = new ChatOpenAI({
708706
model: "gpt-3.5-turbo",
709707
});
710708
expect(hasSupportForJsonSchemaOutput(model)).toBe(false);
711-
expect(hasSupportForJsonSchemaOutput("openai:gpt-3.5-turbo")).toBe(false);
712-
expect(hasSupportForJsonSchemaOutput("gpt-3.5-turbo")).toBe(false);
713709
});
714710

715-
it("should return false for Anthropic models that don't support JSON schema output", () => {
711+
it("should return false for Anthropic models whose profile does not report structuredOutput", () => {
716712
const model = new ChatAnthropic({
717713
model: "claude-sonnet-4-5-20250929",
718714
anthropicApiKey: "foobar",
719715
});
720716
expect(hasSupportForJsonSchemaOutput(model)).toBe(false);
721-
expect(
722-
hasSupportForJsonSchemaOutput("anthropic:claude-sonnet-4-5-20250929")
723-
).toBe(false);
724-
expect(hasSupportForJsonSchemaOutput("claude-sonnet-4-5-20250929")).toBe(
725-
false
726-
);
727717
});
728718
});

libs/langchain/src/agents/tests/utils.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
BindToolsInput,
88
ToolChoice,
99
} from "@langchain/core/language_models/chat_models";
10+
import type { ModelProfile } from "@langchain/core/language_models/profile";
1011
import { StructuredTool } from "@langchain/core/tools";
1112
import {
1213
BaseMessage,
@@ -191,6 +192,13 @@ export class FakeToolCallingChatModel extends BaseChatModel {
191192
return "fake";
192193
}
193194

195+
get profile(): ModelProfile {
196+
return {
197+
toolCalling: true,
198+
structuredOutput: true,
199+
};
200+
}
201+
194202
async _generate(
195203
messages: BaseMessage[],
196204
_options: this["ParsedCallOptions"],

0 commit comments

Comments
 (0)