Skip to content

google provider: Vertex AI rejects multi-turn tool history — id set on FunctionCall/FunctionResponse parts is unknown to Vertex #265

@ezynda3

Description

@ezynda3

Summary

When the Google provider is pointed at Vertex AI (rather than the Gemini Developer API) and the conversation includes prior tool calls + tool results, the request is rejected by the GCP REST endpoint:

Error from provider (GCP):
Invalid JSON payload received. Unknown name "id" at 'contents[N].parts[0].function_call': Cannot find field.
Invalid JSON payload received. Unknown name "id" at 'contents[M].parts[0].function_response': Cannot find field.

Single-turn / no-tool conversations work. The break only triggers once the message history fed to the next Generate / Stream call already contains assistant tool-call parts and matching tool-result parts.

Reproduction

  1. Build an Agent (or call LanguageModel.Generate directly) configured to route through Vertex AI — i.e. a gemini-* model where the provider is constructed against the Vertex backend rather than GEMINI_API_KEY against generativelanguage.googleapis.com.
  2. Run a turn that produces a tool call and a successful tool result (any tool works — weather, add, etc.).
  3. Run a follow-up user turn. The full message history (including the prior tool_call + tool_result) is replayed to the model.
  4. The Vertex endpoint 400s with the error above.

The exact same conversation, replayed via the Gemini Developer API (generativelanguage.googleapis.com/v1beta), succeeds. This is confirmed by the existing recorded cassettes in providertests/testdata/TestGoogleCommon/gemini-2.5-flash/tool.yaml and multi_tool.yaml — those cassettes include "id":"1" on functionCall / functionResponse parts and pass against the Developer API.

Root cause

providers/google/google.go populates ID on both genai.FunctionCall and genai.FunctionResponse when assembling the request:

// ~L458 — assistant message: tool call part
geminiPart := &genai.Part{
    FunctionCall: &genai.FunctionCall{
        ID:   toolCall.ToolCallID,
        Name: toolCall.ToolName,
        Args: result,
    },
}

// ~L509 — tool message: text result part
parts = append(parts, &genai.Part{
    FunctionResponse: &genai.FunctionResponse{
        ID:       result.ToolCallID,
        Response: response,
        Name:     toolCall.ToolName,
    },
})

// ~L523 — tool message: error result part
parts = append(parts, &genai.Part{
    FunctionResponse: &genai.FunctionResponse{
        ID:       result.ToolCallID,
        Response: response,
        Name:     toolCall.ToolName,
    },
})

google.golang.org/genai declares the field with omitempty:

// google.golang.org/genai/types.go
type FunctionCall struct {
    ID string `json:"id,omitempty"`
    ...
}
type FunctionResponse struct {
    ID string `json:"id,omitempty"`
    ...
}

So the field only goes on the wire when fantasy sets it to a non-empty value, which it does on every assistant/tool message in history.

Why this only breaks on Vertex

The Gemini Developer API (generativelanguage.googleapis.com/v1beta) accepts the field — it was introduced for parallel-tool-call parity with OpenAI's ID-based pairing. The Vertex AI REST validator (*-aiplatform.googleapis.com/v1) hasn't been updated to accept it and rejects the request as "Unknown name 'id'". The field is functionally optional in both backends: Gemini matches function calls to function responses by name + position within contents[], not by id. Fantasy itself doesn't rely on round-tripping the ID through the wire either — it pairs internally via fantasy.ToolCallPart.ToolCallID before serialization (the if tc.ToolCallID == result.ToolCallID loop around L495).

Suggested fix

In providers/google/google.go, omit ID from both FunctionCall and FunctionResponse parts. Two reasonable shapes:

  1. Unconditional — drop the field entirely. The Developer API tolerates its absence (omitempty + no consumer relies on it), the Vertex path starts working, and the internal pairing logic is untouched because it happens before serialization. This is the smallest patch.
  2. Vertex-aware — keep the field for Developer API requests, blank it for Vertex requests. More conservative but requires knowing the backend at serialization time.

Option 1 is the cleanest unless there's a use case I'm missing for surfacing the ID to clients that consume the raw genai.Content history — which I don't think exists, since the field is server-generated for tool calls coming back from Gemini, and ignored as input.

Workarounds while waiting for a fix

  • Route Gemini through generativelanguage.googleapis.com (GEMINI_API_KEY) instead of Vertex. The Developer API silently accepts the extra id.
  • Start a fresh conversation on every turn (no tool-call history → no rejected payload). Not viable for any real chat use case.
  • Use a non-Gemini model on Vertex (Claude-on-Vertex via the Anthropic provider, OpenAI-compat routes) — those go through entirely different fantasy providers with different wire schemas and aren't affected.

Environment

  • Fantasy version: v0.17.2
  • google.golang.org/genai: v1.51.0 (also reproduces on later patch versions — the genai type definition is unchanged)
  • Failing endpoint: Vertex AI REST (*-aiplatform.googleapis.com)
  • Working endpoint: Gemini Developer API (generativelanguage.googleapis.com/v1beta)
  • Models reproduced against: gemini-2.5-flash, gemini-3.x (any Vertex-hosted Gemini)

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions