Skip to content

bug(@langchain/google-common): functionCallingConfig.mode is serialized lowercase, which Vertex AI silently ignores — tool_choice ("any"/"none"/forced function name) is a no-op on ChatVertexAI #11265

Description

@bobkellyspace150

Checked other resources

  • This is a bug, not a usage question. For questions, please use the LangChain Forum (https://forum.langchain.com/).
  • I added a very descriptive title to this issue.
  • I searched the LangChain.js documentation with the integrated search.
  • I used the GitHub search to find a similar question and didn't find it.
  • I am sure that this is a bug in LangChain.js rather than my code.
  • The bug is not resolved by updating to the latest stable version of LangChain (or the specific integration package).

Example Code

import { ChatVertexAI } from "@langchain/google-vertexai";

const model = new ChatVertexAI({
  model: "gemini-2.5-flash",
  temperature: 0,
}).withConfig({
  tools: [{ functionDeclarations: [myFunctionDeclaration] }],
  // Intended: force the model to call this function (Gemini "ANY" mode).
  tool_choice: myFunctionDeclaration.name,
});

const result = await model.invoke(messages);
// Expected: request carries toolConfig.functionCallingConfig.mode: "ANY",
// guaranteeing the function call with schema-constrained decoding.
// Actual: the request body contains mode: "any" (lowercase — inspect the
// outgoing payload; this is model-independent), which Vertex AI silently
// ignores, leaving default AUTO mode: the model may answer in plain text,
// and function-call args lose ANY-mode schema anchoring.

Error Message and Stack Trace (if applicable)

No response

Description

formatToolConfig in @langchain/google-common builds the request's
toolConfig.functionCallingConfig.mode using lowercase values
("auto" | "any" | "none"), both for the pass-through case and for the
forced-function-name case:

https://github.com/langchain-ai/langchainjs/blob/main/libs/providers/langchain-google-common/src/utils/gemini.ts#L1983-L2008

if (["auto", "any", "none"].includes(parameters.tool_choice)) {
  config = {
    functionCallingConfig: {
      mode: parameters.tool_choice as "auto" | "any" | "none",
      ...

The Vertex AI REST API defines FunctionCallingConfig.mode as a protobuf
enum with values MODE_UNSPECIFIED | AUTO | ANY | NONE (uppercase). In our
testing against the production Vertex AI generateContent endpoint, a
lowercase mode is not rejected — the request returns 200 and behaves
exactly as if no functionCallingConfig had been sent (i.e. AUTO). That
makes the defect completely silent.

Two independent lines of evidence that the field is ignored (not just
mis-set):

  1. Request-payload inspection (model-independent, no behavioral test
    needed): intercept/log the outgoing request body and the lowercase
    toolConfig.functionCallingConfig.mode is visible for any model.
  2. The NONE test (behavioral, verified with gemini-3.5-flash via the
    global endpoint): with tool_choice: "none" — i.e. mode: "none" on the
    wire — the model still emits function calls. That is categorically
    impossible if the mode were honored, so the field is being ignored, and
    requests run under default AUTO semantics.

The practical consequences follow from Google's documented mode semantics:
tool_choice: "<functionName>" / "any" does NOT force the call (the model
may answer in plain text instead), does NOT restrict to
allowedFunctionNames, and the response loses ANY-mode's guaranteed
schema-constrained function call. This is nasty to diagnose because the
model usually calls the tool anyway when prompted to — behavior degrades
intermittently rather than anything erroring.

By contrast, @langchain/google-genai converts the same inputs to the
uppercase enum via FunctionCallingMode.ANY in convertToolsToGenAI
(libs/providers/langchain-google-genai/src/utils/tools.ts), which works
correctly — so the two Google wrappers silently disagree about whether
tool_choice does anything.

Suggested fix

Map the user-facing lowercase values to the uppercase wire enum in
formatToolConfig, the same way formatGenerationConfig in the same file
already maps lowercase reasoningLevel values to uppercase thinkingLevel
enum values via its levelMap:

const modeMap = { auto: "AUTO", any: "ANY", none: "NONE" } as const;

(The GeminiRequest type's mode union would need the corresponding
update, or a cast at the assignment.)

Affected versions: observed in @langchain/google-common 2.1.31 and 2.2.0
(latest stable), 3.0.0-dev, and current main.

System Info

  • @langchain/google-common 2.1.31 / 2.2.0 (also inspected 3.0.0-dev and main)
  • @langchain/google-vertexai 2.1.31
  • Node.js 22.x
  • Behavioral verification (the NONE test above): Vertex AI generateContent
    with gemini-3.5-flash (global endpoint). The lowercase serialization
    itself is model-independent (confirmed by request-payload inspection).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions