fix(google): carry Gemini thought signatures through the Responses round trip - #1781
Conversation
…und trip Gemini validates a thought signature against the exact part that carried the function call. The signature was observed on the response and then dropped: the adapter event carried only id and name, the Responses function_call item had nowhere to put it, and the parser rebuilt the tool call without it. A same-process replay cache hid this for streaming turns, which is why the loss looked intermittent - history replay and previous_response_id had no cache to fall back on. Carry it with the individual tool call at every hop instead. One shared seam owns the wire shape, the Google adapter attaches metadata from the originating part and prefers it on the way back out, and both synthetic loops copy it per call so parallel calls can never share or swap a signature. Signatures are opaque here: never parsed, merged, re-encoded, or synthesized, and refused above the same 64 KiB ceiling the replay cache already enforces. Closes #1735 Co-authored-by: chilung-cgu <chilung-cgu@users.noreply.github.com>
|
✅ Deterministic PR hygiene checks passed. |
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (11)
📝 WalkthroughWalkthroughThe change preserves Google Gemini ChangesGemini signature round trip
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant GoogleAdapter
participant Bridge
participant ResponsesParser
participant ResponsesHistory
GoogleAdapter->>Bridge: emit tool_call_start with thoughtSignature
Bridge->>ResponsesHistory: write function_call extra_content
ResponsesHistory->>ResponsesParser: provide function_call history
ResponsesParser->>GoogleAdapter: rebuild tool call with provider metadata
GoogleAdapter->>GoogleAdapter: serialize thoughtSignature in next request
Possibly related PRs
Suggested reviewers: ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d8b01b53b2
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| ...(currentToolCall.namespace ? { namespace: currentToolCall.namespace } : {}), | ||
| // Provider-opaque metadata (issue #1735) rides the item so a client that replays | ||
| // this history can hand the signature back on the part it belongs to. | ||
| ...(responsesExtraContentFromProviderMetadata(currentToolCall.providerMetadata) ?? {}), |
There was a problem hiding this comment.
Preserve signatures on custom and tool-search calls
When Gemini invokes a declared custom tool such as apply_patch, or the client-side tool_search tool, the bridge selects one of the preceding branches, so this metadata spread never runs; the buffered path has the same omission. These tools are exposed to Gemini as function declarations and their calls can therefore carry a required thoughtSignature, but the resulting custom_tool_call/tool_search_call item and parser discard it. A direct-Gemini follow-up or cold history replay consequently sends the call back unsigned and can be rejected upstream. Attach the bounded metadata to these item variants as well and restore it when parsing them into OcxToolCall.
AGENTS.md reference: src/AGENTS.md:L19-L19
Useful? React with 👍 / 👎.
Summary
Gemini validates a thought signature against the exact part that carried the function call. The signature was observed on the response and then dropped: the adapter event carried only
idandname, the Responsesfunction_callitem had nowhere to put it, and the parser rebuilt the tool call without it. A same-process replay cache hid this for streaming turns, which is why the loss looked intermittent — history replay andprevious_response_idhad no cache to fall back on.This supersedes #1772 by @chilung-cgu, which repaired one web-search reconstruction. Per the maintainer roadmap, a web-search-only patch is not sufficient because every other tool loop rebuilds calls the same way.
The metadata now travels with the individual tool call at every hop:
OcxProviderOpaqueToolCallMetadataonOcxToolCallandtool_call_startsrc/responses/provider-opaque-metadata.ts— the one seam owning the wire shapeextra_content.google.thought_signaturethoughtSignaturefield kept as fallbackValues are opaque: never parsed, merged, re-encoded, or synthesized, and refused above the same 64 KiB ceiling the replay cache already enforces.
Closes #1735
Follow-ups deliberately not in this PR
Two adjacent defects were found during review and are recorded in
devlog/_plan/260815_roadmap_closeout/050_gemini_thought_signature.mdrather than bundled here: Google-issuedfunctionCall.idis replaced by a syntheticcall_*(ids still match each other, so pairing works), and the web-search loop interleaves parallel calls asFC1, FR1, FC2, FR2where Google wantsFC1, FC2, FR1, FR2.Verification
Run on the Linux validation host against this exact head:
bun x tsc --noEmitclean. New regressions cover the cold history replay path (no cache warmed), parallel calls keeping distinct signatures, unsigned history staying unsigned, and malformed/unknown/oversized payloads carrying nothing.Checklist
bun x tsc --noEmitcleanSummary by CodeRabbit
New Features
Bug Fixes