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
- 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.
- Run a turn that produces a tool call and a successful tool result (any tool works —
weather, add, etc.).
- Run a follow-up user turn. The full message history (including the prior
tool_call + tool_result) is replayed to the model.
- 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:
- 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.
- 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)
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:
Single-turn / no-tool conversations work. The break only triggers once the message history fed to the next
Generate/Streamcall already contains assistant tool-call parts and matching tool-result parts.Reproduction
Agent(or callLanguageModel.Generatedirectly) configured to route through Vertex AI — i.e. agemini-*model where the provider is constructed against the Vertex backend rather thanGEMINI_API_KEYagainstgenerativelanguage.googleapis.com.weather,add, etc.).tool_call+tool_result) is replayed to the model.The exact same conversation, replayed via the Gemini Developer API (
generativelanguage.googleapis.com/v1beta), succeeds. This is confirmed by the existing recorded cassettes inprovidertests/testdata/TestGoogleCommon/gemini-2.5-flash/tool.yamlandmulti_tool.yaml— those cassettes include"id":"1"onfunctionCall/functionResponseparts and pass against the Developer API.Root cause
providers/google/google.gopopulatesIDon bothgenai.FunctionCallandgenai.FunctionResponsewhen assembling the request:google.golang.org/genaideclares the field withomitempty: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 byname+ position withincontents[], not byid. Fantasy itself doesn't rely on round-tripping the ID through the wire either — it pairs internally viafantasy.ToolCallPart.ToolCallIDbefore serialization (theif tc.ToolCallID == result.ToolCallIDloop around L495).Suggested fix
In
providers/google/google.go, omitIDfrom bothFunctionCallandFunctionResponseparts. Two reasonable shapes: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.Contenthistory — 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
generativelanguage.googleapis.com(GEMINI_API_KEY) instead of Vertex. The Developer API silently accepts the extraid.Environment
v0.17.2google.golang.org/genai:v1.51.0(also reproduces on later patch versions — the genai type definition is unchanged)*-aiplatform.googleapis.com)generativelanguage.googleapis.com/v1beta)