Skip to content

fix(ai-gemini): read/write thoughtSignature at Part level for Gemini 3.x#459

Open
pemontto wants to merge 4 commits intoTanStack:mainfrom
pemontto:fix/gemini-thought-signature-part-level
Open

fix(ai-gemini): read/write thoughtSignature at Part level for Gemini 3.x#459
pemontto wants to merge 4 commits intoTanStack:mainfrom
pemontto:fix/gemini-thought-signature-part-level

Conversation

@pemontto
Copy link
Copy Markdown

@pemontto pemontto commented Apr 16, 2026

Summary

Gemini 3.x models emit thoughtSignature as a Part-level sibling of functionCall (per the @google/genai Part type definition), not nested inside functionCall. The FunctionCall interface has no thoughtSignature property at all.

The adapter was:

  • Reading from functionCall.thoughtSignature (wrong location, doesn't exist in SDK types)
  • Writing it back nested inside functionCall (wrong location, API ignores it there)

This causes Gemini 3.x to reject subsequent tool-call turns with:

400 INVALID_ARGUMENT: "Function call is missing a thought_signature"

The @google/genai Part type (for reference)

export declare interface Part {
    functionCall?: FunctionCall;
    thoughtSignature?: string;  // <-- Part-level sibling
    // ...
}

export declare interface FunctionCall {
    id?: string;
    args?: Record<string, unknown>;
    name?: string;
    // no thoughtSignature here
}

Changes

  • Read side (processStreamChunks): reads part.thoughtSignature first, falls back to functionCall.thoughtSignature for Gemini 2.x compatibility
  • Write side (formatMessages): emits thoughtSignature as a Part-level sibling of functionCall instead of nesting it inside

Test plan

  • Existing tests pass (66/66)
  • Added test: reads Part-level thoughtSignature from Gemini 3.x streaming response and round-trips it at the Part level
  • Added test: falls back to functionCall.thoughtSignature for Gemini 2.x wire format
  • Verified fix against live gemini-3.1-pro-preview and gemini-3.1-flash-lite-preview sessions (multi-turn tool calling with thinking enabled)

Closes #403
Related: #218, #401, #404

Summary by CodeRabbit

  • Bug Fixes

    • Fixed Gemini adapter to read and emit tool-call signature at the correct Part level for Gemini 3.x, while preserving a fallback for Gemini 2.x, preventing API validation errors on subsequent tool calls.
  • Tests

    • Updated and added tests to validate Part-level signature handling for Gemini 3.x and nested fallback behavior for Gemini 2.x.
  • Chores

    • Added a changeset entry documenting the adapter behavior update.

Gemini 3.x models emit thoughtSignature as a Part-level sibling of
functionCall (per @google/genai Part type), not nested inside
functionCall. The adapter was reading from functionCall.thoughtSignature
(which does not exist in the SDK types) and writing it back nested,
causing the API to reject subsequent tool-call turns with
400 INVALID_ARGUMENT: "Function call is missing a thought_signature".

Read side: check part.thoughtSignature first, fall back to
functionCall.thoughtSignature for Gemini 2.x compatibility.

Write side: emit thoughtSignature as a Part-level sibling of
functionCall instead of nesting it inside.

Closes TanStack#403
Related: TanStack#218, TanStack#401, TanStack#404
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 16, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 31f2b135-41b3-48d8-9568-c074a13408bf

📥 Commits

Reviewing files that changed from the base of the PR and between 37fb4c1 and ab05e4c.

📒 Files selected for processing (1)
  • .changeset/fix-gemini-thought-signature-part-level.md
✅ Files skipped from review due to trivial changes (1)
  • .changeset/fix-gemini-thought-signature-part-level.md

📝 Walkthrough

Walkthrough

Reads Gemini thoughtSignature from Part-level (part.thoughtSignature) for Gemini 3.x, falls back to functionCall.thoughtSignature for older models, and emits thoughtSignature as a Part-level sibling to functionCall when formatting outgoing Gemini requests. Tests updated to cover both placements.

Changes

Cohort / File(s) Summary
Changesets
\.changeset/fix-gemini-thought-signature-part-level.md
Adds patch changeset documenting new Part-level thoughtSignature handling and emission.
Gemini Adapter Implementation
packages/typescript/ai-gemini/src/adapters/text.ts
When streaming, derive thoughtSignature from part.thoughtSignature first, fallback to functionCall.thoughtSignature; only backfill unset values; format outgoing messages to include thoughtSignature as a Part-level sibling to functionCall.
Tests
packages/typescript/ai-gemini/tests/gemini-adapter.test.ts
Updates Gemini 3.x expectations to read Part-level thoughtSignature; adds Gemini 2.x scenario where thoughtSignature is nested under functionCall and asserts emitted requests place it at the Part level.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client (UI / StreamProcessor)
    participant Server as Server (chat() + Adapter)
    participant Gemini as Gemini API
    Client->>Server: Send history (may include ToolCall parts)
    Server->>Server: Adapter formats messages\n(reads part.thoughtSignature || functionCall.thoughtSignature)
    Server->>Gemini: POST chat request with Part-level `thoughtSignature`
    Gemini-->>Server: Streaming response with parts[] (part.thoughtSignature or functionCall.thoughtSignature)
    Server->>Server: processStreamChunks updates ToolCall state\n(prefer part.thoughtSignature, backfill if unset)
    Server->>Client: Stream events / UIMessages (ToolCall start/updates)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I dug through parts with curious paws,

moved signatures out from nested jaws.
Now thoughts sit sibling, neat and spry —
they hop through streams and never hide. 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Linked Issues check ❓ Inconclusive The PR fixes the Gemini adapter's read/write paths but linked issue #403 requires additional client-side fixes in core types and pipeline that are not addressed in this changeset. Issue #403 explicitly requires changes to ToolCallPart, InternalToolCallState, updateToolCallPart, handleToolCallStartEvent, and buildAssistantMessages in @tanstack/ai core. Clarify whether #403 is fully resolved by this PR or if a separate PR is required for client-side pipeline fixes.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main fix: updating thoughtSignature handling in the Gemini adapter from functionCall to Part level, which is the core change across all modified files.
Description check ✅ Passed The PR description thoroughly explains the problem, provides SDK type definitions, details read/write path changes, and includes comprehensive test verification and issue references.
Out of Scope Changes check ✅ Passed All changes are scoped to the @tanstack/ai-gemini adapter (read/write path fixes) and directly related tests, with no unrelated alterations.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@pemontto
Copy link
Copy Markdown
Author

Re: the pre-merge check about #403 scope.

This PR fixes the server-side adapter (the GeminiTextAdapter read/write paths), which is the direct cause of the 400 INVALID_ARGUMENT: "Function call is missing a thought_signature" errors on Gemini 3.x models.

The client-side providerMetadata pipeline issue (threading through ToolCallPart, InternalToolCallState, StreamProcessor, etc.) is a separate concern tracked in #404. That PR addresses whether providerMetadata survives the client-side UIMessage round-trip. Both fixes are needed for a complete solution, but they're independent: this adapter fix stops the API rejections, and #404 ensures the metadata persists through the client layer.

@mattsoltani
Copy link
Copy Markdown

mattsoltani commented Apr 17, 2026

Can confirm this also fixes the issue we were experiencing missing thought signature

Comment thread packages/typescript/ai-gemini/src/adapters/text.ts Outdated
@nx-cloud
Copy link
Copy Markdown

nx-cloud Bot commented Apr 20, 2026

View your CI Pipeline Execution ↗ for commit 48cf4a2

Command Status Duration Result
nx run-many --targets=build --exclude=examples/** ✅ Succeeded 48s View ↗

☁️ Nx Cloud last updated this comment at 2026-04-20 15:23:42 UTC

@nx-cloud
Copy link
Copy Markdown

nx-cloud Bot commented Apr 20, 2026

View your CI Pipeline Execution ↗ for commit 48cf4a2

Command Status Duration Result
nx run-many --targets=build --exclude=examples/** ✅ Succeeded 48s View ↗

☁️ Nx Cloud last updated this comment at 2026-04-20 15:22:47 UTC

Per review feedback: use the @google/genai typed `Part` interface
directly instead of `as any` casts.

- Read side: `part.thoughtSignature` is properly typed on `Part`, so
  the cast is removed entirely. The Gemini 2.x fallback to
  `functionCall.thoughtSignature` is also removed since the SDK has
  never typed it there and Gemini has always emitted it at Part level.
- Write side: construct a typed `Part` and conditionally assign
  `thoughtSignature`, avoiding the `as Part` cast on a spread literal.

The only remaining `as any` in this area is the pre-existing
functionResponse cast, which is unrelated to this fix.
@pemontto
Copy link
Copy Markdown
Author

Good call, addressed in ab05e4c. The @google/genai SDK actually types thoughtSignature on Part properly, so both as any casts in this area were unnecessary:

  • Read side: removed (part as any).thoughtSignaturepart.thoughtSignature (typed). Also removed the (functionCall as any).thoughtSignature fallback entirely. The SDK has never had thoughtSignature on FunctionCall, so that fallback was based on a misconception from the original PR fix(ai-gemini): preserve thoughtSignature and fix tool call IDs for Gemini 3+ thinking models #401 and was never a real code path.
  • Write side: replaced the } as Part) spread-literal cast with an explicit typed const part: Part = {...} construction and conditional assignment.

Only remaining as any in the Gemini adapter is the pre-existing functionResponse cast (line ~714), which is unrelated to this fix.

All 66 tests still pass, typecheck clean.

@mattsoltani
Copy link
Copy Markdown

mattsoltani commented Apr 22, 2026

@pemontto After some further testing, it seems there are a few more places in the tool call life cycle where the provider meta data should be added to avoid more errors. See the following file locations:

  1. ai/packages/typescript/ai/src/activities/chat/messages.ts#L206
  2. ai/packages/typescript/ai/src/activities/chat/messages.ts#L342
  3. ai/packages/typescript/ai/src/activities/chat/stream/message-updaters.ts#L78
  4. ai/packages/typescript/ai/src/activities/chat/stream/processor.ts#L922

@pemontto
Copy link
Copy Markdown
Author

pemontto commented Apr 22, 2026

Good catch @mattsoltani, those locations are exactly what #404 (by @houmark, opened 2026-03-28) addresses. It threads providerMetadata through ToolCallPart, InternalToolCallState, updateToolCallPart, handleToolCallStart, buildAssistantMessages, and modelMessageToUIMessage, the full client-side pipeline.

That PR has been open for nearly a month without a maintainer review. Could we get it reviewed alongside this one? Without both, the adapter write-side fix here never actually has a signature to write because it's dropped upstream in the pipeline.

If preferred, I'm happy to pull those changes into this PR (with attribution to @houmark) so it's a single comprehensive fix. Just let me know which you'd like.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

providerMetadata lost in client-side UIMessage pipeline — breaks Gemini 3 thoughtSignature on multi-turn tool calls

3 participants