Background
PartTypeThought was added to the gai abstraction in #257. The clients/google package now publishes per-client ThinkingLevel constants (Minimal/Low/Medium/High) targeting the Gemini 3.x family. On the response side, genai.Part.Thought=true parts are surfaced as gai.PartTypeThought. On the request side, however, Gemini 3.x enforces a constraint that the current gai.Part shape cannot satisfy.
The Gemini constraint
When Gemini 3.x emits a functionCall part, it also returns a thoughtSignature on that part. The API requires the same signature to be sent back on the next turn alongside the functionCall for the response to be accepted — even when thinking is disabled via ThinkingBudget=0. Without it, the API returns:
Error 400, Message: Function call is missing a thought_signature in functionCall parts.
This is required for tools to work correctly, and missing thought_signature may lead to
degraded model performance. Additional data, function call `default_api:read_file`,
position 2.
See https://ai.google.dev/gemini-api/docs/thought-signatures for the full constraint.
The genai SDK exposes the signature on genai.Part.ThoughtSignature []byte, but gai.Part does not preserve it. Today, clients/google/chat_complete.go builds outgoing genai.Part values from gai.PartTypeToolCall without carrying any signature, and any multi-turn tool flow against a 3.x model fails on the second turn. The Google client also returns a typed error when gai.PartTypeThought is passed back as message history, mirroring the Anthropic-side behaviour in #250 (with the same span-recording shape).
What a fix would entail
- Extend
gai.Part to carry opaque provider-specific metadata. Concrete shape to evaluate during implementation:
- (a)
Provider any field on Part set by the originating client and consumed only by the same provider's request builder. Honest about leakage; uses any.
- (b) Sibling part types per provider gated behind an interface. Avoids
any but explodes the type surface.
- On the stream-read side,
clients/google/chat_complete.go populates the metadata from chunk.Candidates[0].Content.Parts[*].ThoughtSignature for both PartTypeToolCall and PartTypeThought parts.
- On the request-build side, when re-emitting a
genai.Part for an outgoing gai.PartTypeToolCall (and gai.PartTypeThought, once that round-trips), populate ThoughtSignature from the metadata.
- The Anthropic and OpenAI clients can either ignore the new metadata (current behaviour) or carry it through opaquely.
This issue and #250 (Anthropic extended-thinking signature round-trip) have the same shape — both want opaque per-provider metadata on gai.Part. The two should be designed together so we don't end up with two parallel mechanisms.
Why deferred
Same reason as #250: the cleanest abstraction for opaque per-provider metadata is non-trivial. A typed field leaks Gemini specifics into the gai surface; a generic map[string]any is loose. The two cases (Gemini signatures, Anthropic signed thinking blocks) have similar shapes and likely share a fix.
Workaround until fixed
clients/google/chat_complete_test.go defaults newChatCompleter(t) to gemini-2.5-flash (a 2.5 model that does not require thought_signature on tool follow-ups) so existing multi-turn tool tests stay green. The Gemini 3.x mappings are exercised single-turn by the table-driven thinking level matrix subtest of TestChatCompleter_ChatComplete, which does not need to round-trip a signature. Callers targeting Gemini 3.x for tool flows must stay single-turn, or filter gai.PartTypeThought out of history and accept that tool-use multi-turn flows will 400 today.
See docs/decisions.md (entry: "Per-client ThinkingLevel constants (2026-04-29)") for the broader principle that gai does not pre-validate provider constraints, and docs/diary/2026-04-29-per-client-thinking-levels.md for the empirical narrative behind both this issue and #250.
Background
PartTypeThoughtwas added to the gai abstraction in #257. Theclients/googlepackage now publishes per-clientThinkingLevelconstants (Minimal/Low/Medium/High) targeting the Gemini 3.x family. On the response side,genai.Part.Thought=trueparts are surfaced asgai.PartTypeThought. On the request side, however, Gemini 3.x enforces a constraint that the currentgai.Partshape cannot satisfy.The Gemini constraint
When Gemini 3.x emits a
functionCallpart, it also returns athoughtSignatureon that part. The API requires the same signature to be sent back on the next turn alongside thefunctionCallfor the response to be accepted — even when thinking is disabled viaThinkingBudget=0. Without it, the API returns:See https://ai.google.dev/gemini-api/docs/thought-signatures for the full constraint.
The genai SDK exposes the signature on
genai.Part.ThoughtSignature []byte, butgai.Partdoes not preserve it. Today,clients/google/chat_complete.gobuilds outgoinggenai.Partvalues fromgai.PartTypeToolCallwithout carrying any signature, and any multi-turn tool flow against a 3.x model fails on the second turn. The Google client also returns a typed error whengai.PartTypeThoughtis passed back as message history, mirroring the Anthropic-side behaviour in #250 (with the same span-recording shape).What a fix would entail
gai.Partto carry opaque provider-specific metadata. Concrete shape to evaluate during implementation:Provider anyfield onPartset by the originating client and consumed only by the same provider's request builder. Honest about leakage; usesany.anybut explodes the type surface.clients/google/chat_complete.gopopulates the metadata fromchunk.Candidates[0].Content.Parts[*].ThoughtSignaturefor bothPartTypeToolCallandPartTypeThoughtparts.genai.Partfor an outgoinggai.PartTypeToolCall(andgai.PartTypeThought, once that round-trips), populateThoughtSignaturefrom the metadata.This issue and #250 (Anthropic extended-thinking signature round-trip) have the same shape — both want opaque per-provider metadata on
gai.Part. The two should be designed together so we don't end up with two parallel mechanisms.Why deferred
Same reason as #250: the cleanest abstraction for opaque per-provider metadata is non-trivial. A typed field leaks Gemini specifics into the gai surface; a generic
map[string]anyis loose. The two cases (Gemini signatures, Anthropic signed thinking blocks) have similar shapes and likely share a fix.Workaround until fixed
clients/google/chat_complete_test.godefaultsnewChatCompleter(t)togemini-2.5-flash(a 2.5 model that does not requirethought_signatureon tool follow-ups) so existing multi-turn tool tests stay green. The Gemini 3.x mappings are exercised single-turn by the table-driventhinking level matrixsubtest ofTestChatCompleter_ChatComplete, which does not need to round-trip a signature. Callers targeting Gemini 3.x for tool flows must stay single-turn, or filtergai.PartTypeThoughtout of history and accept that tool-use multi-turn flows will 400 today.See
docs/decisions.md(entry: "Per-client ThinkingLevel constants (2026-04-29)") for the broader principle that gai does not pre-validate provider constraints, anddocs/diary/2026-04-29-per-client-thinking-levels.mdfor the empirical narrative behind both this issue and #250.