mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-09-10 01:51:30 +08:00
fix(gemini): cache trailing text thought signatures instead of emitting carriers
- Cache trailing thought signatures associated with preceding text blocks via best-effort cache. - Suppress emitting detached thinking carrier blocks after visible text in streaming and non-streaming responses. Closes: #5272
This commit is contained in:
@@ -206,6 +206,10 @@ func ConvertAntigravityResponseToClaude(ctx context.Context, _ string, originalR
|
||||
appendThinkingSignature(signature, direction, targetKind)
|
||||
return false
|
||||
}
|
||||
if direction == geminiClaudeCarrierPrevious && targetKind == geminiClaudeCarrierText {
|
||||
cache.CacheSignatureBestEffort(ctx, modelName, "", signature)
|
||||
return false
|
||||
}
|
||||
closeCurrentBlock()
|
||||
startEmptyThinkingBlock()
|
||||
appendCarrierSignature(signature, direction, targetKind)
|
||||
@@ -686,6 +690,8 @@ func ConvertAntigravityResponseToClaudeNonStream(_ context.Context, _ string, or
|
||||
thinkingSignatureDirection = geminiClaudeCarrierStandalone
|
||||
thinkingSignatureTargetKind = geminiClaudeCarrierText
|
||||
flushThinking()
|
||||
} else if hasSemanticContent && lastSemanticKind == geminiClaudeCarrierText {
|
||||
cache.CacheSignatureBestEffort(context.Background(), modelName, "", signature)
|
||||
} else if hasSemanticContent {
|
||||
appendSignatureCarrier(signature, geminiClaudeCarrierPrevious, lastSemanticKind)
|
||||
} else {
|
||||
@@ -708,6 +714,8 @@ func ConvertAntigravityResponseToClaudeNonStream(_ context.Context, _ string, or
|
||||
if text.Exists() && text.String() != "" {
|
||||
appendSignatureCarrier(signature, geminiClaudeCarrierNext, geminiClaudeCarrierText)
|
||||
visibleSignatureCarrier = true
|
||||
} else if hasSemanticContent && lastSemanticKind == geminiClaudeCarrierText {
|
||||
cache.CacheSignatureBestEffort(context.Background(), modelName, "", signature)
|
||||
} else if hasSemanticContent {
|
||||
appendSignatureCarrier(signature, geminiClaudeCarrierPrevious, lastSemanticKind)
|
||||
} else {
|
||||
|
||||
@@ -843,15 +843,11 @@ func TestConvertAntigravityResponseToClaude_DetachedGeminiSignatureAfterVisibleT
|
||||
if !strings.Contains(outputText, `"content_block":{"type":"text","text":""}`) {
|
||||
t.Fatalf("missing visible text block: %s", outputText)
|
||||
}
|
||||
if !strings.Contains(outputText, `"content_block":{"type":"thinking","thinking":""}`) {
|
||||
t.Fatalf("missing detached thinking carrier: %s", outputText)
|
||||
if strings.Contains(outputText, `"content_block":{"type":"thinking"`) {
|
||||
t.Fatalf("trailing thinking carrier must not be emitted after visible text: %s", outputText)
|
||||
}
|
||||
carrierSignature := encodeGeminiClaudeCarrierSignature(validSignature, geminiClaudeCarrierPrevious, geminiClaudeCarrierText)
|
||||
if !strings.Contains(outputText, `"type":"signature_delta","signature":"`+carrierSignature+`"`) {
|
||||
t.Fatalf("missing detached Gemini signature: %s", outputText)
|
||||
}
|
||||
if got := strings.Count(outputText, `"type":"content_block_stop"`); got != 2 {
|
||||
t.Fatalf("content block stops = %d, want text + detached thinking; output=%s", got, outputText)
|
||||
if got := strings.Count(outputText, `"type":"content_block_stop"`); got != 1 {
|
||||
t.Fatalf("content block stops = %d, want 1 text stop; output=%s", got, outputText)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -948,13 +944,14 @@ func TestConvertAntigravityResponseToClaudeNonStream_PreviousCarrierDoesNotCross
|
||||
responseJSON := []byte(`{"response":{"candidates":[{"content":{"parts":[{"text":"A","thoughtSignature":"` + signature1 + `"},{"text":"","thoughtSignature":"` + signature2 + `"},{"text":"B"}]},"finishReason":"STOP"}],"modelVersion":"gemini-3.6-flash","responseId":"previous-carrier-boundary"}}`)
|
||||
|
||||
output := ConvertAntigravityResponseToClaudeNonStream(context.Background(), "gemini-3.6-flash-high", requestJSON, requestJSON, responseJSON, nil)
|
||||
replayRequest := []byte(`{"model":"gemini-3.6-flash-high","messages":[{"role":"assistant","content":[]}]}`)
|
||||
replayRequest, _ = sjson.SetRawBytes(replayRequest, "messages.0.content", []byte(gjson.GetBytes(output, "content").Raw))
|
||||
replayRequest = StripInvalidGeminiSignatureThinkingBlocks(replayRequest)
|
||||
translated := ConvertClaudeRequestToAntigravity("gemini-3.6-flash-high", replayRequest, false)
|
||||
parts := gjson.GetBytes(translated, "request.contents.0.parts").Array()
|
||||
if len(parts) != 3 || parts[0].Get("text").String() != "A" || parts[0].Get("thoughtSignature").String() != signature1 || !parts[1].Get("text").Exists() || parts[1].Get("text").String() != "" || parts[1].Get("thoughtSignature").String() != signature2 || parts[2].Get("text").String() != "B" || parts[2].Get("thoughtSignature").String() != "" {
|
||||
t.Fatalf("previous carrier crossed following text: output=%s translated=%s", output, translated)
|
||||
if got := gjson.GetBytes(output, "content.#").Int(); got != 3 {
|
||||
t.Fatalf("content blocks = %d, want leading carrier + 2 text blocks; output=%s", got, output)
|
||||
}
|
||||
if got := gjson.GetBytes(output, "content.1.text").String(); got != "A" {
|
||||
t.Fatalf("first text block = %q; output=%s", got, output)
|
||||
}
|
||||
if got := gjson.GetBytes(output, "content.2.text").String(); got != "B" {
|
||||
t.Fatalf("second text block = %q; output=%s", got, output)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1032,31 +1029,15 @@ func TestConvertAntigravityResponseToClaude_PreservesConsecutiveDetachedCarriers
|
||||
|
||||
nonStream := ConvertAntigravityResponseToClaudeNonStream(context.Background(), "gemini-3.6-flash-high", requestJSON, requestJSON, responseJSON, nil)
|
||||
content := gjson.GetBytes(nonStream, "content").Array()
|
||||
wantCarrier1 := encodeGeminiClaudeCarrierSignature(sig1, geminiClaudeCarrierPrevious, geminiClaudeCarrierText)
|
||||
wantCarrier2 := encodeGeminiClaudeCarrierSignature(sig2, geminiClaudeCarrierPrevious, geminiClaudeCarrierText)
|
||||
if len(content) != 3 || content[1].Get("signature").String() != wantCarrier1 || content[2].Get("signature").String() != wantCarrier2 {
|
||||
t.Fatalf("non-stream carriers were merged: %s", nonStream)
|
||||
}
|
||||
replayRequest := []byte(`{"model":"gemini-3.6-flash-high","messages":[{"role":"assistant","content":[]},{"role":"user","content":[{"type":"text","text":"continue"}]}]}`)
|
||||
replayRequest, _ = sjson.SetRawBytes(replayRequest, "messages.0.content", []byte(gjson.GetBytes(nonStream, "content").Raw))
|
||||
replayRequest = StripInvalidGeminiSignatureThinkingBlocks(replayRequest)
|
||||
translated := ConvertClaudeRequestToAntigravity("gemini-3.6-flash-high", replayRequest, false)
|
||||
parts := gjson.GetBytes(translated, "request.contents.0.parts").Array()
|
||||
if len(parts) != 2 || parts[0].Get("thoughtSignature").String() != sig1 || parts[1].Get("thoughtSignature").String() != sig2 {
|
||||
t.Fatalf("consecutive carriers did not round-trip in order: %s", translated)
|
||||
}
|
||||
if parts[0].Get("text").String() != "visible" || !parts[1].Get("text").Exists() || parts[1].Get("text").String() != "" {
|
||||
t.Fatalf("consecutive carrier targets malformed: %s", translated)
|
||||
if len(content) != 1 || content[0].Get("text").String() != "visible" {
|
||||
t.Fatalf("want 1 visible text block, got: %s", nonStream)
|
||||
}
|
||||
|
||||
var param any
|
||||
stream := bytes.Join(ConvertAntigravityResponseToClaude(context.Background(), "gemini-3.6-flash-high", requestJSON, requestJSON, responseJSON, ¶m), nil)
|
||||
streamText := string(stream)
|
||||
if got := strings.Count(streamText, `"content_block":{"type":"thinking"`); got != 2 {
|
||||
t.Fatalf("stream thinking carrier count = %d, want 2; output=%s", got, stream)
|
||||
}
|
||||
if got := strings.Count(streamText, `"type":"signature_delta"`); got != 2 {
|
||||
t.Fatalf("stream signature count = %d, want 2; output=%s", got, stream)
|
||||
if got := strings.Count(streamText, `"content_block":{"type":"thinking"`); got != 0 {
|
||||
t.Fatalf("trailing thinking carrier must not be emitted, got: %s", streamText)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1097,19 +1078,12 @@ func TestConvertAntigravityResponseToClaudeNonStream_DetachedGeminiSignatureAfte
|
||||
requestJSON := []byte(`{"model":"gemini-3.6-flash-high"}`)
|
||||
responseJSON := []byte(`{"response":{"candidates":[{"content":{"parts":[{"text":"visible answer"},{"text":"","thoughtSignature":"` + validSignature + `"}]},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":10,"candidatesTokenCount":2,"thoughtsTokenCount":3,"totalTokenCount":15},"modelVersion":"gemini-3.6-flash","responseId":"resp-detached"}}`)
|
||||
output := ConvertAntigravityResponseToClaudeNonStream(context.Background(), "gemini-3.6-flash-high", requestJSON, requestJSON, responseJSON, nil)
|
||||
if got := gjson.GetBytes(output, "content.#").Int(); got != 2 {
|
||||
t.Fatalf("content blocks = %d, want text + detached thinking; output=%s", got, output)
|
||||
if got := gjson.GetBytes(output, "content.#").Int(); got != 1 {
|
||||
t.Fatalf("content blocks = %d, want 1 text block; output=%s", got, output)
|
||||
}
|
||||
if got := gjson.GetBytes(output, "content.0.text").String(); got != "visible answer" {
|
||||
t.Fatalf("visible text = %q; output=%s", got, output)
|
||||
}
|
||||
if got := gjson.GetBytes(output, "content.1.type").String(); got != "thinking" {
|
||||
t.Fatalf("detached block type = %q; output=%s", got, output)
|
||||
}
|
||||
wantCarrier := encodeGeminiClaudeCarrierSignature(validSignature, geminiClaudeCarrierPrevious, geminiClaudeCarrierText)
|
||||
if got := gjson.GetBytes(output, "content.1.signature").String(); got != wantCarrier {
|
||||
t.Fatalf("detached signature = %q, want %q; output=%s", got, wantCarrier, output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertAntigravityResponseToClaude_TrailingFunctionCarrierRoundTrip(t *testing.T) {
|
||||
@@ -1143,6 +1117,25 @@ func TestConvertAntigravityResponseToClaude_TrailingFunctionCarrierRoundTrip(t *
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertAntigravityResponseToClaudeStream_TrailingFunctionCarrierRoundTrip(t *testing.T) {
|
||||
nativeSignature := testGeminiEPrefixSignature(t)
|
||||
requestJSON := []byte(`{"model":"gemini-3.6-flash-high","tools":[{"name":"run_command","input_schema":{"type":"object","properties":{"command":{"type":"string"}}}}]}`)
|
||||
chunk1 := []byte(`{"response":{"candidates":[{"content":{"parts":[{"functionCall":{"id":"native-call-1","name":"run_command","args":{"command":"true"}}}]}}],"modelVersion":"gemini-3.6-flash","responseId":"tool-trailing-carrier"}}`)
|
||||
chunk2 := []byte(`{"response":{"candidates":[{"content":{"parts":[{"text":"","thoughtSignature":"` + nativeSignature + `"}]},"finishReason":"STOP"}],"modelVersion":"gemini-3.6-flash","responseId":"tool-trailing-carrier"}}`)
|
||||
|
||||
var param any
|
||||
var output []byte
|
||||
output = append(output, bytes.Join(ConvertAntigravityResponseToClaude(context.Background(), "gemini-3.6-flash-high", requestJSON, requestJSON, chunk1, ¶m), nil)...)
|
||||
output = append(output, bytes.Join(ConvertAntigravityResponseToClaude(context.Background(), "gemini-3.6-flash-high", requestJSON, requestJSON, chunk2, ¶m), nil)...)
|
||||
output = append(output, bytes.Join(ConvertAntigravityResponseToClaude(context.Background(), "gemini-3.6-flash-high", requestJSON, requestJSON, []byte("[DONE]"), ¶m), nil)...)
|
||||
|
||||
outputText := string(output)
|
||||
wantCarrier := encodeGeminiClaudeCarrierSignature(nativeSignature, geminiClaudeCarrierPrevious, geminiClaudeCarrierFunction)
|
||||
if !strings.Contains(outputText, `"type":"signature_delta","signature":"`+wantCarrier+`"`) {
|
||||
t.Fatalf("missing trailing function carrier in stream: %s", outputText)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertAntigravityResponseToClaude_DirectionalTextCarriersRoundTrip(t *testing.T) {
|
||||
signature := testGeminiEPrefixSignature(t)
|
||||
requestJSON := []byte(`{"model":"gemini-3.6-flash-high"}`)
|
||||
@@ -1155,7 +1148,6 @@ func TestConvertAntigravityResponseToClaude_DirectionalTextCarriersRoundTrip(t *
|
||||
carrierIndex int
|
||||
}{
|
||||
{name: "signed first part", parts: `[{"text":"A","thoughtSignature":"` + signature + `"},{"text":"B"}]`, wantFirstSignature: signature, wantDirection: geminiClaudeCarrierNext},
|
||||
{name: "trailing carrier before next part", parts: `[{"text":"A"},{"text":"","thoughtSignature":"` + signature + `"},{"text":"B"}]`, wantFirstSignature: signature, wantDirection: geminiClaudeCarrierPrevious, carrierIndex: 1},
|
||||
{name: "signed second part", parts: `[{"text":"A"},{"text":"B","thoughtSignature":"` + signature + `"}]`, wantSecondSignature: signature, wantDirection: geminiClaudeCarrierNext, carrierIndex: 1},
|
||||
}
|
||||
for _, testCase := range testCases {
|
||||
|
||||
Reference in New Issue
Block a user