Skip to content

fix(gemini-oauth): preserve thoughtSignature in Cloud Code SSE handler (#3214)#3215

Open
abbyshekit wants to merge 1 commit intomainfrom
fix/gemini-cloud-code-thought-signature
Open

fix(gemini-oauth): preserve thoughtSignature in Cloud Code SSE handler (#3214)#3215
abbyshekit wants to merge 1 commit intomainfrom
fix/gemini-cloud-code-thought-signature

Conversation

@abbyshekit
Copy link
Copy Markdown

Summary

Closes #3214. Re-fixes the Gemini 3.x `INVALID_ARGUMENT — Function call is missing a thought_signature in functionCall parts` error that the prior fixes #1565 and #1752 were intended to address but did not, because both fixes worked on the request-builder layer while the upstream SSE parser was still dropping the real signature on the floor.

Root cause (verbatim from the reporter)

@thomasmaerz traced it to `src/llm/gemini_oauth.rs` lines 1441-1444, where the Cloud Code SSE part-extractor copied only `functionCall` and silently discarded `thoughtSignature` (which Gemini delivers as a sibling key). The synthetic sentinel `"skip_thought_signature_validator"` injected by `ensure_thought_signatures()` then replaced every real signature, and Gemini's INVALID_ARGUMENT validator rejected the request.

The SSE shape Cloud Code delivers:

```json
{
"functionCall": {"name": "list_dir", "args": {}},
"thoughtSignature": ""
}
```

Fix

Extract `thoughtSignature` as a sibling of `functionCall` in the SSE part-handler. Refactored the inline extraction into a small helper `extract_function_call_part` so the contract is unit-testable without standing up a mock SSE stream.

The roundtrip + synthetic-fallback layers added by #1565 / #1752 are unchanged — they now have the real signature to work with for thinking-mode parts, and the synthetic fallback still applies when no signature was delivered.

Tests

Three regression tests, all passing:

Test plan

  • `cargo test test_extract_function_call_part` — 3/3 pass
  • Manual repro: install `ironclaw-v0.27.0-8-g749fe9da` (or HEAD with this PR), configure `gemini_oauth` provider with `gemini-3-flash-preview`, ask a question requiring a tool call (e.g. `ls .`). Pre-PR: HTTP 400 INVALID_ARGUMENT after the first tool call. Post-PR: tool calls succeed.

🤖 Generated with Claude Code

#3214)

Closes #3214.

Gemini 3.x rejects tool-call follow-ups with HTTP 400 INVALID_ARGUMENT:

    Function call is missing a thought_signature in functionCall parts.

despite the prior fixes in #1565 (sentinel injection on every part)
and #1752 (full roundtrip + synthetic fallback) being present in HEAD.
@thomasmaerz traced it: the upstream Cloud Code SSE handler in
src/llm/gemini_oauth.rs was silently discarding `thoughtSignature`
when extracting `functionCall` parts. The synthetic sentinel
"skip_thought_signature_validator" then replaced every real signature
on the request side, and Gemini's INVALID_ARGUMENT validator (added
for 3.x) rejected the request.

Fix: extract `thoughtSignature` as a sibling of `functionCall` in the
SSE part-handler. The roundtrip and synthetic-fallback layers added
by #1565/#1752 are unchanged — they now have the real signature to
work with for thinking-mode parts, and the synthetic fallback still
kicks in when no signature was delivered.

Refactored the inline part-extraction loop into a small helper
(`extract_function_call_part`) so the behavior is unit-testable
without spinning up a mock SSE stream. Three regression tests pin
the contract:

- _preserves_thought_signature: the #3214 happy path
- _omits_signature_when_absent: the no-signature case keeps working
  unchanged (request-side fallback handles it)
- _returns_none_for_text_only_part: defensive — text-only parts
  must not be misclassified as functionCall parts

3/3 pass on `cargo test`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added scope: llm LLM integration size: M 50-199 changed lines risk: low Changes to docs, tests, or low-risk modules contributor: new First-time contributor labels May 2, 2026
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces the extract_function_call_part helper function in src/llm/gemini_oauth.rs to ensure that thoughtSignature is preserved when extracting functionCall objects from Gemini SSE streams. This change addresses issue #3214, preventing INVALID_ARGUMENT errors in Gemini 3.x by maintaining the link between function calls and their associated signatures. Comprehensive unit tests have been added to verify the extraction logic for various scenarios, including cases with and without signatures. I have no feedback to provide.

@thomasmaerz
Copy link
Copy Markdown

Heads up — the underlying issue report was incorrect

I filed issue #3214 which this PR addresses, but I've since confirmed the issue was authored with the wrong provider and auth path. My actual setup uses llm_backend=gemini (OpenAI-compatible endpoint with API key), not gemini_oauth / Cloud Code SSE.

I'd suggest not merging this until the actual reproduction case is established against gemini_oauth. I'm sorry for the noise from the incorrect issue report.

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

Labels

contributor: new First-time contributor risk: low Changes to docs, tests, or low-risk modules scope: llm LLM integration size: M 50-199 changed lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: thoughtSignature dropped in Cloud Code SSE handler — prior fixes (#1565, #1752) incomplete

2 participants