Skip to content

fix(translator): preserve thoughtSignature in non-stream gemini to claude conversion - #190

Open
warelik wants to merge 7 commits into
kaitranntt:mainfrom
warelik:fix/gemini-claude-nonstream-signature
Open

fix(translator): preserve thoughtSignature in non-stream gemini to claude conversion#190
warelik wants to merge 7 commits into
kaitranntt:mainfrom
warelik:fix/gemini-claude-nonstream-signature

Conversation

@warelik

@warelik warelik commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Problem

  1. Missing signature in non-streaming thinking blocks: In ConvertGeminiResponseToClaudeNonStream, thinking blocks were constructed without preserving Gemini's thoughtSignature / thought_signature. In multi-turn conversations with Anthropic API, returning thinking blocks lacking their signature causes HTTP 400 (Invalid signature in thinking block).
  2. Dropped signatures in streaming responses: In ConvertGeminiResponseToClaude, signatures arriving with ResponseType == 0 (before thinking text), ResponseType == 1 (visible text), or attached to functionCall parts were dropped.
  3. Carrier block and signature isolation: When signatures were attached to visible text or tool calls, earlier implementations either swallowed visible text into thinking blocks or failed to isolate signatures across distinct content blocks.

Fix

  • internal/translator/gemini/claude/gemini_claude_response.go:62: In ConvertGeminiResponseToClaudeNonStream, captured thinkingSignature strictly from parts where isThought is true, resetting signatures upon flushing empty thoughts and emitting signature on the output Claude thinking block.
  • internal/translator/gemini/claude/gemini_claude_response.go:210: In ConvertGeminiResponseToClaude, introduced carrier block emission for standalone signatures, visible text with signatures, and tool calls with signatures via appendPartSignature, isolating thinking blocks per signature and ensuring CurrentThinkingSigned state is properly reset.

Tests

  • TestConvertGeminiResponseToClaudeNonStream_ThoughtSignature: Verifies non-stream thinking block preserves signature.
  • TestConvertGeminiResponseToClaudeNonStream_TextWithThoughtSignatureWithoutThoughtFlag: Verifies visible text with signature remains a text block.
  • TestConvertGeminiResponseToClaudeNonStream_FunctionCallWithThoughtSignature: Verifies tool use with signature emits tool_use without phantom thinking block.
  • TestConvertGeminiResponseToClaude_VisibleTextWithThoughtSignatureEmitsCarrierAndText: Verifies stream visible text with signature emits a carrier thinking block with signature delta followed by the text block.
  • TestConvertGeminiResponseToClaude_FunctionCallWithThoughtSignatureEmitsCarrierAndTool: Verifies stream tool calls with signatures emit carrier thinking block followed by tool_use.
  • TestConvertGeminiResponseToClaude_MultipleSignedThoughtChunksSplitBlocks: Verifies signed streaming thinking chunks cleanly isolate into separate blocks.

Reverse bite-check

Reverting non-stream signature handling in internal/translator/gemini/claude/gemini_claude_response.go reproduces the missing signature failure:

=== RUN   TestConvertGeminiResponseToClaudeNonStream_ThoughtSignature
    gemini_claude_response_test.go:93: expected signature sig-test in thinking block, got: {"id":"resp-test","type":"message","role":"assistant","model":"gemini-test","content":[{"type":"thinking","thinking":"thinking text"},{"type":"text","text":"hello world"}],"stop_reason":"end_turn","stop_sequence":null}
--- FAIL: TestConvertGeminiResponseToClaudeNonStream_ThoughtSignature (0.00s)
FAIL
FAIL	github.com/router-for-me/CLIProxyAPI/v7/internal/translator/gemini/claude	0.385s
FAIL

Reverting streaming carrier signature emission reproduces the failure where visible text with signatures is swallowed or fails carrier creation:

=== RUN   TestConvertGeminiResponseToClaude_VisibleTextWithThoughtSignatureEmitsCarrierAndText
    gemini_claude_response_test.go:236: expected text content_block_start at index 1, got: event: message_start
        data: {"type":"message_start","message":{"id":"resp-test","type":"message","role":"assistant","content":[],"model":"gemini-test","stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":0,"output_tokens":0}}}
        
        
        event: content_block_start
        data: {"type":"content_block_start","index":0,"content_block":{"type":"thinking","thinking":""}}
        
        
        event: content_block_delta
        data: {"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":"Tokyo: 20C"}}
        
        
        event: content_block_delta
        data: {"type":"content_block_delta","index":0,"delta":{"type":"signature_delta","signature":"sig-carrier"}}
        
        
        event: content_block_stop
        data: {"type":"content_block_stop","index":0}
        
        
        event: message_delta
        data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":10,"output_tokens":5}}
        
        
        event: message_stop
        data: {"type":"message_stop"}
        
        
--- FAIL: TestConvertGeminiResponseToClaude_VisibleTextWithThoughtSignatureEmitsCarrierAndText (0.00s)
FAIL
FAIL	github.com/router-for-me/CLIProxyAPI/v7/internal/translator/gemini/claude	0.267s
FAIL

Verification

TMPDIR=/Users/warelik/.cache/gotmp GOCACHE=/Users/warelik/.cache/gocache go build ./...
TMPDIR=/Users/warelik/.cache/gotmp GOCACHE=/Users/warelik/.cache/gocache go vet ./internal/translator/gemini/claude/...
gofmt -l internal/translator/gemini/claude/gemini_claude_response.go internal/translator/gemini/claude/gemini_claude_response_test.go
TMPDIR=/Users/warelik/.cache/gotmp GOCACHE=/Users/warelik/.cache/gocache go test -v ./internal/translator/gemini/claude/...

Output:

ok  	github.com/router-for-me/CLIProxyAPI/v7/internal/translator/gemini/claude	0.279s

Tooling note

The jbcontext semantic-search CLI is installed but non-functional in this environment
(jbcontext search fails with the OS keychain is not accessible). The equivalent
review passes were performed with the repository's own tooling and manual inspection
instead; this is disclosed for transparency about how the change was reviewed.

W ARELIK added 3 commits August 20, 2026 13:13
…aude conversion

Extract thoughtSignature / thought_signature and include signature in Claude thinking content blocks in ConvertGeminiResponseToClaudeNonStream.

Classify parts with thoughtSignature as thinking to prevent reasoning text leakage and ensure parity with streaming converter.

Fixes HTTP 400 (Invalid signature in thinking block) on multi-turn conversations with extended thinking.

Refs router-for-me/CLIProxyAPI#5106
Do not classify non-thought parts carrying thoughtSignature as thinking to prevent swallowing visible text.

Bind thinkingSignature strictly to parts where thought: true is set so functionCall signatures do not overwrite thinking signatures.

Guard flushThinking to avoid emitting phantom empty thinking blocks on functionCall carriers.

Handle signature-only finish chunks in streaming path without opening empty text blocks.

Add regression tests for carrier text parts, functionCall carriers, signature precedence, and streaming parity.

Refs router-for-me/CLIProxyAPI#5106
W ARELIK added 2 commits August 20, 2026 18:33
Align Gemini->Claude streaming signature handling with antigravity reference.

Open thinking block for standalone signature when none is active or when previous block is signed.
@warelik

warelik commented Aug 20, 2026

Copy link
Copy Markdown
Contributor Author

Follow-up: Preserve streaming thinking signatures across all response states

Following an audit of the streaming signature preservation logic, this follow-up aligns Gemini->Claude streaming translation with the reference implementation in internal/translator/antigravity/claude/antigravity_claude_response.go:201-213.

Changes:

  1. Signature preservation regardless of ResponseType: Standalone signatures arriving before any thinking text (ResponseType == 0, e.g. includeThoughts=false) or after visible text (ResponseType == 1) now start a thinking block and emit signature_delta instead of being dropped.
  2. Consecutive signature block isolation: Standalone signatures arriving when the current thinking block is already signed close the previous block and start a new thinking block ({"type":"thinking","thinking":""}), preserving individual signatures as distinct carrier blocks.
  3. PR description update: Corrected the PR body to reflect that signature-only thinking blocks are intentional carrier structures matching the antigravity translator.

New regression tests:

  • TestConvertGeminiResponseToClaude_SignatureBeforeThinkingTextEmitsSignatureDelta
  • TestConvertGeminiResponseToClaude_ThreeConsecutiveSignaturesSplitBlocks

…and tool calls

Route thought signatures through carrier thinking blocks in streaming Gemini-to-Claude conversion when attached to visible text or tool calls.
@warelik

warelik commented Aug 20, 2026

Copy link
Copy Markdown
Contributor Author

Round Two Follow-Up Update: Stream Carrier Thinking Blocks for Visible Text and Tool Call Signatures

We have completed round two of follow-ups on PR #190:

  1. Root Cause Resolved:

    • In streaming responses (ConvertGeminiResponseToClaude), when Gemini returned a thoughtSignature on visible text (thought: false or omitted) or on a functionCall part, the signature was previously dropped because appendSignatureDelta was not called.
    • Refactored ConvertGeminiResponseToClaude with a unified appendPartSignature helper matching the reference architecture in internal/translator/antigravity/claude/antigravity_claude_response.go:
      • Emits carrier thinking blocks ({"type":"thinking","thinking":""}) with signature_delta when signatures arrive outside an active unsigned thinking block.
      • Covers standalone signatures, visible-text carrier signatures, and function call signatures across all streaming phases.
  2. Test Coverage & Verification:

    • Added 3 new regression tests in internal/translator/gemini/claude/gemini_claude_response_test.go:
      • TestConvertGeminiResponseToClaude_VisibleTextWithThoughtSignatureEmitsCarrierAndText
      • TestConvertGeminiResponseToClaude_FunctionCallWithThoughtSignatureEmitsCarrierAndTool
      • TestConvertGeminiResponseToClaude_MultiPartMixedThoughtVisibleToolSignatures
    • Performed reverse bite-checks against unpatched code, capturing exact failure outputs for all 3 cases before verifying all tests pass green with the fix.
    • Verified that all previous regression tests and all package tests across internal/translator/... pass with zero failures.

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.

1 participant