mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-03 04:50:52 +08:00
fix(antigravity): prevent corrupted thought signature when switching models
When switching from Claude models (e.g., Opus 4.5) to Gemini models (e.g., Flash) mid-conversation via Antigravity OAuth, the client-provided thinking signatures from Claude would cause "Corrupted thought signature" errors since they are incompatible with Gemini API. Changes: - Remove fallback to client-provided signatures in thinking block handling - Only use cached signatures (from same-session Gemini responses) - Skip thinking blocks without valid cached signatures - tool_use blocks continue to use skip_thought_signature_validator when no valid signature is available This ensures cross-model switching works correctly while preserving signature validation for same-model conversations. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -123,11 +123,6 @@ func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _
|
|||||||
if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "thinking" {
|
if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "thinking" {
|
||||||
// Use GetThinkingText to handle wrapped thinking objects
|
// Use GetThinkingText to handle wrapped thinking objects
|
||||||
thinkingText := util.GetThinkingText(contentResult)
|
thinkingText := util.GetThinkingText(contentResult)
|
||||||
signatureResult := contentResult.Get("signature")
|
|
||||||
clientSignature := ""
|
|
||||||
if signatureResult.Exists() && signatureResult.String() != "" {
|
|
||||||
clientSignature = signatureResult.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Always try cached signature first (more reliable than client-provided)
|
// Always try cached signature first (more reliable than client-provided)
|
||||||
// Client may send stale or invalid signatures from different sessions
|
// Client may send stale or invalid signatures from different sessions
|
||||||
@@ -139,11 +134,11 @@ func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback to client signature only if cache miss and client signature is valid
|
// NOTE: We do NOT fallback to client signature anymore.
|
||||||
if signature == "" && cache.HasValidSignature(clientSignature) {
|
// Client signatures from Claude models are incompatible with Antigravity/Gemini API.
|
||||||
signature = clientSignature
|
// When switching between models (e.g., Claude Opus -> Gemini Flash), the Claude
|
||||||
// log.Debugf("Using client-provided signature for thinking block")
|
// signatures will cause "Corrupted thought signature" errors.
|
||||||
}
|
// If we have no cached signature, the thinking block will be skipped below.
|
||||||
|
|
||||||
// Store for subsequent tool_use in the same message
|
// Store for subsequent tool_use in the same message
|
||||||
if cache.HasValidSignature(signature) {
|
if cache.HasValidSignature(signature) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/cache"
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -75,28 +76,42 @@ func TestConvertClaudeRequestToAntigravity_RoleMapping(t *testing.T) {
|
|||||||
func TestConvertClaudeRequestToAntigravity_ThinkingBlocks(t *testing.T) {
|
func TestConvertClaudeRequestToAntigravity_ThinkingBlocks(t *testing.T) {
|
||||||
// Valid signature must be at least 50 characters
|
// Valid signature must be at least 50 characters
|
||||||
validSignature := "abc123validSignature1234567890123456789012345678901234567890"
|
validSignature := "abc123validSignature1234567890123456789012345678901234567890"
|
||||||
|
thinkingText := "Let me think..."
|
||||||
|
|
||||||
|
// Pre-cache the signature (simulating a response from the same session)
|
||||||
|
// The session ID is derived from the first user message hash
|
||||||
|
// Since there's no user message in this test, we need to add one
|
||||||
inputJSON := []byte(`{
|
inputJSON := []byte(`{
|
||||||
"model": "claude-sonnet-4-5-thinking",
|
"model": "claude-sonnet-4-5-thinking",
|
||||||
"messages": [
|
"messages": [
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": [{"type": "text", "text": "Test user message"}]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"role": "assistant",
|
"role": "assistant",
|
||||||
"content": [
|
"content": [
|
||||||
{"type": "thinking", "thinking": "Let me think...", "signature": "` + validSignature + `"},
|
{"type": "thinking", "thinking": "` + thinkingText + `", "signature": "` + validSignature + `"},
|
||||||
{"type": "text", "text": "Answer"}
|
{"type": "text", "text": "Answer"}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}`)
|
}`)
|
||||||
|
|
||||||
|
// Derive session ID and cache the signature
|
||||||
|
sessionID := deriveSessionID(inputJSON)
|
||||||
|
cache.CacheSignature(sessionID, thinkingText, validSignature)
|
||||||
|
defer cache.ClearSignatureCache(sessionID)
|
||||||
|
|
||||||
output := ConvertClaudeRequestToAntigravity("claude-sonnet-4-5-thinking", inputJSON, false)
|
output := ConvertClaudeRequestToAntigravity("claude-sonnet-4-5-thinking", inputJSON, false)
|
||||||
outputStr := string(output)
|
outputStr := string(output)
|
||||||
|
|
||||||
// Check thinking block conversion
|
// Check thinking block conversion (now in contents.1 due to user message)
|
||||||
firstPart := gjson.Get(outputStr, "request.contents.0.parts.0")
|
firstPart := gjson.Get(outputStr, "request.contents.1.parts.0")
|
||||||
if !firstPart.Get("thought").Bool() {
|
if !firstPart.Get("thought").Bool() {
|
||||||
t.Error("thinking block should have thought: true")
|
t.Error("thinking block should have thought: true")
|
||||||
}
|
}
|
||||||
if firstPart.Get("text").String() != "Let me think..." {
|
if firstPart.Get("text").String() != thinkingText {
|
||||||
t.Error("thinking text mismatch")
|
t.Error("thinking text mismatch")
|
||||||
}
|
}
|
||||||
if firstPart.Get("thoughtSignature").String() != validSignature {
|
if firstPart.Get("thoughtSignature").String() != validSignature {
|
||||||
@@ -227,13 +242,19 @@ func TestConvertClaudeRequestToAntigravity_ToolUse(t *testing.T) {
|
|||||||
|
|
||||||
func TestConvertClaudeRequestToAntigravity_ToolUse_WithSignature(t *testing.T) {
|
func TestConvertClaudeRequestToAntigravity_ToolUse_WithSignature(t *testing.T) {
|
||||||
validSignature := "abc123validSignature1234567890123456789012345678901234567890"
|
validSignature := "abc123validSignature1234567890123456789012345678901234567890"
|
||||||
|
thinkingText := "Let me think..."
|
||||||
|
|
||||||
inputJSON := []byte(`{
|
inputJSON := []byte(`{
|
||||||
"model": "claude-sonnet-4-5-thinking",
|
"model": "claude-sonnet-4-5-thinking",
|
||||||
"messages": [
|
"messages": [
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": [{"type": "text", "text": "Test user message"}]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"role": "assistant",
|
"role": "assistant",
|
||||||
"content": [
|
"content": [
|
||||||
{"type": "thinking", "thinking": "Let me think...", "signature": "` + validSignature + `"},
|
{"type": "thinking", "thinking": "` + thinkingText + `", "signature": "` + validSignature + `"},
|
||||||
{
|
{
|
||||||
"type": "tool_use",
|
"type": "tool_use",
|
||||||
"id": "call_123",
|
"id": "call_123",
|
||||||
@@ -245,11 +266,16 @@ func TestConvertClaudeRequestToAntigravity_ToolUse_WithSignature(t *testing.T) {
|
|||||||
]
|
]
|
||||||
}`)
|
}`)
|
||||||
|
|
||||||
|
// Derive session ID and cache the signature
|
||||||
|
sessionID := deriveSessionID(inputJSON)
|
||||||
|
cache.CacheSignature(sessionID, thinkingText, validSignature)
|
||||||
|
defer cache.ClearSignatureCache(sessionID)
|
||||||
|
|
||||||
output := ConvertClaudeRequestToAntigravity("claude-sonnet-4-5-thinking", inputJSON, false)
|
output := ConvertClaudeRequestToAntigravity("claude-sonnet-4-5-thinking", inputJSON, false)
|
||||||
outputStr := string(output)
|
outputStr := string(output)
|
||||||
|
|
||||||
// Check function call has the signature from the preceding thinking block
|
// Check function call has the signature from the preceding thinking block (now in contents.1)
|
||||||
part := gjson.Get(outputStr, "request.contents.0.parts.1")
|
part := gjson.Get(outputStr, "request.contents.1.parts.1")
|
||||||
if part.Get("functionCall.name").String() != "get_weather" {
|
if part.Get("functionCall.name").String() != "get_weather" {
|
||||||
t.Errorf("Expected functionCall, got %s", part.Raw)
|
t.Errorf("Expected functionCall, got %s", part.Raw)
|
||||||
}
|
}
|
||||||
@@ -261,24 +287,35 @@ func TestConvertClaudeRequestToAntigravity_ToolUse_WithSignature(t *testing.T) {
|
|||||||
func TestConvertClaudeRequestToAntigravity_ReorderThinking(t *testing.T) {
|
func TestConvertClaudeRequestToAntigravity_ReorderThinking(t *testing.T) {
|
||||||
// Case: text block followed by thinking block -> should be reordered to thinking first
|
// Case: text block followed by thinking block -> should be reordered to thinking first
|
||||||
validSignature := "abc123validSignature1234567890123456789012345678901234567890"
|
validSignature := "abc123validSignature1234567890123456789012345678901234567890"
|
||||||
|
thinkingText := "Planning..."
|
||||||
|
|
||||||
inputJSON := []byte(`{
|
inputJSON := []byte(`{
|
||||||
"model": "claude-sonnet-4-5-thinking",
|
"model": "claude-sonnet-4-5-thinking",
|
||||||
"messages": [
|
"messages": [
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": [{"type": "text", "text": "Test user message"}]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"role": "assistant",
|
"role": "assistant",
|
||||||
"content": [
|
"content": [
|
||||||
{"type": "text", "text": "Here is the plan."},
|
{"type": "text", "text": "Here is the plan."},
|
||||||
{"type": "thinking", "thinking": "Planning...", "signature": "` + validSignature + `"}
|
{"type": "thinking", "thinking": "` + thinkingText + `", "signature": "` + validSignature + `"}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}`)
|
}`)
|
||||||
|
|
||||||
|
// Derive session ID and cache the signature
|
||||||
|
sessionID := deriveSessionID(inputJSON)
|
||||||
|
cache.CacheSignature(sessionID, thinkingText, validSignature)
|
||||||
|
defer cache.ClearSignatureCache(sessionID)
|
||||||
|
|
||||||
output := ConvertClaudeRequestToAntigravity("claude-sonnet-4-5-thinking", inputJSON, false)
|
output := ConvertClaudeRequestToAntigravity("claude-sonnet-4-5-thinking", inputJSON, false)
|
||||||
outputStr := string(output)
|
outputStr := string(output)
|
||||||
|
|
||||||
// Verify order: Thinking block MUST be first
|
// Verify order: Thinking block MUST be first (now in contents.1 due to user message)
|
||||||
parts := gjson.Get(outputStr, "request.contents.0.parts").Array()
|
parts := gjson.Get(outputStr, "request.contents.1.parts").Array()
|
||||||
if len(parts) != 2 {
|
if len(parts) != 2 {
|
||||||
t.Fatalf("Expected 2 parts, got %d", len(parts))
|
t.Fatalf("Expected 2 parts, got %d", len(parts))
|
||||||
}
|
}
|
||||||
@@ -460,6 +497,9 @@ func TestConvertClaudeRequestToAntigravity_TrailingUnsignedThinking_Removed(t *t
|
|||||||
|
|
||||||
func TestConvertClaudeRequestToAntigravity_TrailingSignedThinking_Kept(t *testing.T) {
|
func TestConvertClaudeRequestToAntigravity_TrailingSignedThinking_Kept(t *testing.T) {
|
||||||
// Last assistant message ends with signed thinking block - should be kept
|
// Last assistant message ends with signed thinking block - should be kept
|
||||||
|
validSignature := "abc123validSignature1234567890123456789012345678901234567890"
|
||||||
|
thinkingText := "Valid thinking..."
|
||||||
|
|
||||||
inputJSON := []byte(`{
|
inputJSON := []byte(`{
|
||||||
"model": "claude-sonnet-4-5-thinking",
|
"model": "claude-sonnet-4-5-thinking",
|
||||||
"messages": [
|
"messages": [
|
||||||
@@ -471,12 +511,17 @@ func TestConvertClaudeRequestToAntigravity_TrailingSignedThinking_Kept(t *testin
|
|||||||
"role": "assistant",
|
"role": "assistant",
|
||||||
"content": [
|
"content": [
|
||||||
{"type": "text", "text": "Here is my answer"},
|
{"type": "text", "text": "Here is my answer"},
|
||||||
{"type": "thinking", "thinking": "Valid thinking...", "signature": "abc123validSignature1234567890123456789012345678901234567890"}
|
{"type": "thinking", "thinking": "` + thinkingText + `", "signature": "` + validSignature + `"}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}`)
|
}`)
|
||||||
|
|
||||||
|
// Derive session ID and cache the signature
|
||||||
|
sessionID := deriveSessionID(inputJSON)
|
||||||
|
cache.CacheSignature(sessionID, thinkingText, validSignature)
|
||||||
|
defer cache.ClearSignatureCache(sessionID)
|
||||||
|
|
||||||
output := ConvertClaudeRequestToAntigravity("claude-sonnet-4-5-thinking", inputJSON, false)
|
output := ConvertClaudeRequestToAntigravity("claude-sonnet-4-5-thinking", inputJSON, false)
|
||||||
outputStr := string(output)
|
outputStr := string(output)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user