From 1515e2971ffc91ac384c84c0253d8b86df891e21 Mon Sep 17 00:00:00 2001 From: Thomas Rowe Date: Fri, 7 Nov 2025 14:14:15 -0500 Subject: [PATCH] feat(google-vertexai): support Gemini propertyOrdering in withStructuredOutput --- .../src/chat_models.ts | 102 +++++------------- 1 file changed, 26 insertions(+), 76 deletions(-) diff --git a/libs/providers/langchain-google-common/src/chat_models.ts b/libs/providers/langchain-google-common/src/chat_models.ts index 61f6bd2049e0..211a2a2ab843 100644 --- a/libs/providers/langchain-google-common/src/chat_models.ts +++ b/libs/providers/langchain-google-common/src/chat_models.ts @@ -65,6 +65,19 @@ import { schemaToGeminiParameters, } from "./utils/zod_to_gemini_parameters.js"; +/** + * Helper to automatically infer Gemini property ordering from a Zod schema. + */ +function withPropertyOrdering>( + schema: InteropZodType +): string[] { + try { + return Object.keys(schema.shape ?? {}); + } catch { + return []; + } +} + export class ChatConnection extends AbstractGoogleLLMConnection< BaseMessage[], AuthOptions @@ -176,7 +189,6 @@ export abstract class ChatGoogleBase extends BaseChatModel implements ChatGoogleBaseInput { - // Used for tracing, replace with the same name as your class static lc_name() { return "ChatGoogle"; } @@ -189,52 +201,28 @@ export abstract class ChatGoogleBase lc_serializable = true; - // Set based on modelName model: string; - modelName = "gemini-pro"; - temperature: number; - maxOutputTokens: number; - maxReasoningTokens: number; - topP: number; - topK: number; - seed: number; - presencePenalty: number; - frequencyPenalty: number; - stopSequences: string[] = []; - logprobs: boolean; - topLogprobs: number = 0; - safetySettings: GoogleAISafetySetting[] = []; - responseModalities?: GoogleAIModelModality[]; - - // May intentionally be undefined, meaning to compute this. convertSystemMessageToHumanContent: boolean | undefined; - safetyHandler: GoogleAISafetyHandler; - speechConfig: GoogleSpeechConfig; - streamUsage = true; - streaming = false; - labels?: Record; - protected connection: ChatConnection; - protected streamedConnection: ChatConnection; constructor(fields?: ChatGoogleBaseInput) { @@ -317,14 +305,10 @@ export abstract class ChatGoogleBase return this.withConfig({ tools: convertToGeminiTools(tools), ...kwargs }); } - // Replace _llmType() { return "chat_integration"; } - /** - * Get the parameters used to invoke the model - */ override invocationParams(options?: this["ParsedCallOptions"]) { return copyAIModelParams(this, options); } @@ -344,9 +328,7 @@ export abstract class ChatGoogleBase if (!finalChunk) { throw new Error("No chunks were returned from the stream."); } - return { - generations: [finalChunk], - }; + return { generations: [finalChunk] }; } const response = await this.connection.request( @@ -368,7 +350,6 @@ export abstract class ChatGoogleBase options: this["ParsedCallOptions"], runManager?: CallbackManagerForLLMRun ): AsyncGenerator { - // Make the call as a streaming request const parameters = this.invocationParams(options); const response = await this.streamedConnection.request( _messages, @@ -376,20 +357,13 @@ export abstract class ChatGoogleBase options, runManager ); - - // Get the streaming parser of the response const stream = response.data as JsonStream; let usageMetadata: UsageMetadata | undefined; - // Loop until the end of the stream - // During the loop, yield each time we get a chunk from the streaming parser - // that is either available or added to the queue while (!stream.streamDone) { const output = await stream.nextChunk(); await runManager?.handleCustomEvent( `google-chunk-${this.constructor.name}`, - { - output, - } + { output } ); if ( output && @@ -428,49 +402,20 @@ export abstract class ChatGoogleBase } } - /** @ignore */ _combineLLMOutput() { return []; } withStructuredOutput< - // eslint-disable-next-line @typescript-eslint/no-explicit-any RunOutput extends Record = Record >( outputSchema: | InteropZodType - // eslint-disable-next-line @typescript-eslint/no-explicit-any - | Record, - config?: StructuredOutputMethodOptions - ): Runnable; - - withStructuredOutput< - // eslint-disable-next-line @typescript-eslint/no-explicit-any - RunOutput extends Record = Record - >( - outputSchema: - | InteropZodType - // eslint-disable-next-line @typescript-eslint/no-explicit-any - | Record, - config?: StructuredOutputMethodOptions - ): Runnable; - - withStructuredOutput< - // eslint-disable-next-line @typescript-eslint/no-explicit-any - RunOutput extends Record = Record - >( - outputSchema: - | InteropZodType - // eslint-disable-next-line @typescript-eslint/no-explicit-any | Record, config?: StructuredOutputMethodOptions ): | Runnable - | Runnable< - BaseLanguageModelInput, - { raw: BaseMessage; parsed: RunOutput } - > { - // eslint-disable-next-line @typescript-eslint/no-explicit-any + | Runnable { const schema: InteropZodType | Record = outputSchema; const name = config?.name; @@ -483,8 +428,15 @@ export abstract class ChatGoogleBase let functionName = name ?? "extract"; let outputParser: BaseLLMOutputParser; let tools: GeminiTool[]; + if (isInteropZodSchema(schema)) { const jsonSchema = schemaToGeminiParameters(schema); + + // 🧩 Add inferred property ordering for Gemini structured outputs + if (!jsonSchema.propertyOrdering) { + jsonSchema.propertyOrdering = withPropertyOrdering(schema); + } + tools = [ { functionDeclarations: [ @@ -512,7 +464,6 @@ export abstract class ChatGoogleBase geminiFunctionDefinition = schema as GeminiFunctionDeclaration; functionName = schema.name; } else { - // We are providing the schema for *just* the parameters, probably const parameters: GeminiJsonSchema = removeAdditionalProperties(schema); geminiFunctionDefinition = { name: functionName, @@ -530,6 +481,7 @@ export abstract class ChatGoogleBase keyName: functionName, }); } + const llm = this.bindTools(tools).withConfig({ tool_choice: functionName }); if (!includeRaw) { @@ -539,7 +491,6 @@ export abstract class ChatGoogleBase } const parserAssign = RunnablePassthrough.assign({ - // eslint-disable-next-line @typescript-eslint/no-explicit-any parsed: (input: any, config) => outputParser.invoke(input.raw, config), }); const parserNone = RunnablePassthrough.assign({ @@ -548,13 +499,12 @@ export abstract class ChatGoogleBase const parsedWithFallback = parserAssign.withFallbacks({ fallbacks: [parserNone], }); + return RunnableSequence.from< BaseLanguageModelInput, { raw: BaseMessage; parsed: RunOutput } >([ - { - raw: llm, - }, + { raw: llm }, parsedWithFallback, ]).withConfig({ runName: "StructuredOutputRunnable",