fix(translate): re-encode non-ASCII thoughtSignature through base64 at JSON emit - #811
Open
steventohme wants to merge 2 commits into
Open
fix(translate): re-encode non-ASCII thoughtSignature through base64 at JSON emit#811steventohme wants to merge 2 commits into
steventohme wants to merge 2 commits into
Conversation
…t JSON emit Google's thought_signature field is typed bytes; the JSON-rest layer auto-base64-decodes the string value on the wire. Upstream values arrive base64url-encoded ASCII and pass through cleanly. Carrier round-trips (embedSignatureInID on response -> ... -> extractSignatureFromID on the next request) recover the original raw bytes as a Go string whose UTF-8 representation includes U+FFFD replacement chars (Go's `for _, c := range s` over a non-UTF-8 string replaces every invalid byte sequence). When that string then went through sse.WriteJSONString / pw.Str, Google's base64 decoder rejected the result at the next turn. Found while onboarding gemini-3.6-flash/3.5-flash-lite: every multi-turn Claude Code session through any Gemini 3.x model had been silently failing this way. Same report class as Google GenAI SDK #711, langchain-google #1570, and gemini-cli #8003/#8011 — multi-SDK coverage of the same wire-shape contract. encodeSignatureForJSON re-encodes values containing non-ASCII bytes through base64 at the three emit sites (OpenAI->Gemini tool-call parts, Anthropic text parts, Anthropic tool-call parts) so the wire shape survives. Plain ASCII values (the test fixtures + the upstream base64 strings themselves) bypass the rewrap since they wire as-is. Test coverage spans both the helper unit test and an end-to-end multi-turn PrepareGemini test using a non-UTF-8 signature and asserting the wire value base64-decodes back to the original bytes.
|
Claude finished @steventohme's task —— View job
Posted 4 comment-length suggestions (advisory, won't block merge). |
workweave-bot
left a comment
Collaborator
There was a problem hiding this comment.
Advisory only — comment-length nits. Won't block merge.
What T-Rex did
Reviews (1): Last reviewed commit: "fix(translate): re-encode non-ASCII thou..." | Re-trigger Greptile |
…ents Fixed (greptile P1 'ASCII Raw Bytes Skip Encoding'): - encodeSignatureForJSON now passes a value through only when it is already valid base64 (std/URL, padded/raw); anything else — including all-ASCII raw bytes like \x00\x01ABC — is re-encoded as base64url. The previous >=0x80 heuristic let ASCII raw bytes reach the wire unencoded. - Added ascii-raw-bytes case to TestEncodeSignatureForJSON; padded ASCII test fixtures (ANTHROPIC_SIG_00, OPAQUE_GEMINI_SIG000, SIG_ROUNDTRIP000) to valid base64 lengths so wire-form pass-through stays asserted. Applied from workweave-bot review (comment-length nits): - emit_gemini.go: encodeSignatureForJSON doc comment 10 -> 5 lines - thought_signature_carrier_external_test.go: test doc 9 -> 3 lines - tool_call_id_internal_test.go: regression comment 10 -> 3 lines, moved-test note 5 -> 2 lines Also fixes the CI gofmt failure in tool_call_id_internal_test.go. Co-authored-by: Cursor <cursoragent@cursor.com>
|
Claude finished @steventohme's task —— View job
All added comment blocks are ≤ 5 lines and address genuine non-obvious WHYs. Prior suggestions from the first run were applied in 678345d. Nothing to flag. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes the second-turn 400 INVALID_ARGUMENT that blocks every multi-turn Claude Code session against any Gemini 3.x model. Found 2026-07-21 while onboarding gemini-3.6-flash + gemini-3.5-flash-lite; reproduces identically against the already-deployed gemini-3.1-pro-preview too.
Root cause
Same multi-SDK report class — Google GenAI SDK #711, langchain-google #1570, gemini-cli #8003, #8011 — all about Google's
thought_signaturebytes field and its base64 wire shape.Upstream flow is OK end-to-end:
thought_signatureas a base64url-encoded ASCII string in the JSON.embedSignatureInIDsmugs those bytes into the tool-callidfield — base64-encodes the bytes of the (already-ASCII) base64 string, surfaces as__thought__<base64>suffix.idcarries the same suffix unchanged across the boundary.extractSignatureFromIDdoesDecodeStringonce, returning the original raw bytes of the upstream signature — exactly what Google gave us, just unpacked from the carrier.The bug is at the JSON emit boundary. Step 4 returns those raw bytes as a Go
string. Thensse.WriteJSONString(called bypw.Str) does:A real Gemini
thought_signaturecontains byte sequences that aren't valid UTF-8 — every such sequence silently gets replaced withU+FFFD. On the next turn Google sees a string of�+ accented/non-ASCII chars, fails to base64-decode it, and returnsInvalid value at 'contents[1].parts[0].thought_signature' (TYPE_BYTES), Base64 decoding failed for ....Fix
encodeSignatureForJSONre-encodes values containing non-ASCII bytes (c >= 0x80) throughbase64.RawURLEncodingat the threepw.Str(sig)emit sites. Plain-ASCII values (test fixtures like"ANTHROPIC_SIG", and also the upstream-delivered base64 values themselves which never trip the carrier round-trip) bypass the rewrap since they wire safely as-is.The non-ASCII round-trip is therefore base64s once for the carrier, then base64s again for the JSON wire: at Google, the JSON-rest layer auto-decodes the outer base64 to bytes — net zero extra encodings on the contract. Same upstream value reaches Google on the second turn that was served on the first.
Tests
TestEncodeSignatureForJSON_PreservesNonASCIISignatureBytes— table-driven unit test on the helper covering the four cases (ASCII pass-through, base64url pass-through, non-ASCII raw bytes, mixed printable/non-ASCII).TestPrepareGemini_ThoughtSignatureCarrierSurvivesMultiTurn— end-to-end repro throughPrepareGemini. Builds a non-UTF-8 signature, embeds it in a tool-use id, exercises the full multi-turn translate path, asserts the emittedthought_signatureJSON value base64-decodes back to the exact original bytes.TestSanityCarrierRoundTripLocked— locks the carrier convention (delimiter string + decode path) so the regression test isn't silently testing the wrong shape.Existing signature round-trip tests (
TestPrepareGemini_FromAnthropic_SystemAndToolUseRoundTripsSignature,TestPrepareGemini_FromAnthropic_ToolUseSignatureSurvivesUnknownFieldStripping,TestSanitizeToolUseID_PreservesLongThoughtSignatureID) all still pass — they use ASCII fixtures that the helper correctly bypasses.go build ./...,go vet ./..., and the fullgo test ./...(every package) pass.Workaround for clients in the meantime
If you can't ship this immediately and need to unblock real multi-turn traffic against Gemini 3.x on your local branch, dropping the body through
echo -n "$sig" | xxd -p | xxd -p -r | base64 -w0(one decode-then-re-encode round-trip) before writing has been used as a manual workaround in test scripts.🤖 Generated with Claude Code