diff --git a/internal/cache/signature_cache.go b/internal/cache/signature_cache.go index dee1b13b..ee8ad0b2 100644 --- a/internal/cache/signature_cache.go +++ b/internal/cache/signature_cache.go @@ -3,6 +3,7 @@ package cache import ( "crypto/sha256" "encoding/hex" + "fmt" "sync" "time" ) @@ -94,7 +95,7 @@ func purgeExpiredSessions() { // CacheSignature stores a thinking signature for a given session and text. // Used for Claude models that require signed thinking blocks in multi-turn conversations. -func CacheSignature(sessionID, text, signature string) { +func CacheSignature(modelName, sessionID, text, signature string) { if sessionID == "" || text == "" || signature == "" { return } @@ -102,7 +103,7 @@ func CacheSignature(sessionID, text, signature string) { return } - sc := getOrCreateSession(sessionID) + sc := getOrCreateSession(fmt.Sprintf("%s#%s", modelName, sessionID)) textHash := hashText(text) sc.mu.Lock() @@ -116,12 +117,12 @@ func CacheSignature(sessionID, text, signature string) { // GetCachedSignature retrieves a cached signature for a given session and text. // Returns empty string if not found or expired. -func GetCachedSignature(sessionID, text string) string { +func GetCachedSignature(modelName, sessionID, text string) string { if sessionID == "" || text == "" { return "" } - val, ok := signatureCache.Load(sessionID) + val, ok := signatureCache.Load(fmt.Sprintf("%s#%s", modelName, sessionID)) if !ok { return "" } diff --git a/internal/translator/antigravity/claude/antigravity_claude_request.go b/internal/translator/antigravity/claude/antigravity_claude_request.go index 87772d8c..5b6ffe22 100644 --- a/internal/translator/antigravity/claude/antigravity_claude_request.go +++ b/internal/translator/antigravity/claude/antigravity_claude_request.go @@ -69,6 +69,7 @@ func deriveSessionID(rawJSON []byte) string { // Returns: // - []byte: The transformed request data in Gemini CLI API format func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _ bool) []byte { + enableThoughtTranslate := true rawJSON := bytes.Clone(inputRawJSON) // Derive session ID for signature caching @@ -132,27 +133,34 @@ func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _ if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "thinking" { // Use GetThinkingText to handle wrapped thinking objects thinkingText := thinking.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) // Client may send stale or invalid signatures from different sessions signature := "" if sessionID != "" && thinkingText != "" { - if cachedSig := cache.GetCachedSignature(sessionID, thinkingText); cachedSig != "" { + if cachedSig := cache.GetCachedSignature(modelName, sessionID, thinkingText); cachedSig != "" { signature = cachedSig // log.Debugf("Using cached signature for thinking block") } } - // NOTE: We do NOT fallback to client signature anymore. - // Client signatures from Claude models are incompatible with Antigravity/Gemini API. - // When switching between models (e.g., Claude Opus -> Gemini Flash), the Claude - // signatures will cause "Corrupted thought signature" errors. - // If we have no cached signature, the thinking block will be skipped below. + // Fallback to client signature only if cache miss and client signature is valid + if signature == "" { + signatureResult := contentResult.Get("signature") + clientSignature := "" + if signatureResult.Exists() && signatureResult.String() != "" { + arrayClientSignatures := strings.SplitN(signatureResult.String(), "#", 2) + if len(arrayClientSignatures) == 2 { + if modelName == arrayClientSignatures[0] { + clientSignature = arrayClientSignatures[1] + } + } + } + if cache.HasValidSignature(clientSignature) { + signature = clientSignature + } + // log.Debugf("Using client-provided signature for thinking block") + } // Store for subsequent tool_use in the same message if cache.HasValidSignature(signature) { @@ -167,6 +175,7 @@ func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _ // Converting to text would break this requirement if isUnsigned { // log.Debugf("Dropping unsigned thinking block (no valid signature)") + enableThoughtTranslate = false continue } @@ -394,7 +403,7 @@ func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _ } // Map Anthropic thinking -> Gemini thinkingBudget/include_thoughts when type==enabled - if t := gjson.GetBytes(rawJSON, "thinking"); t.Exists() && t.IsObject() { + if t := gjson.GetBytes(rawJSON, "thinking"); enableThoughtTranslate && t.Exists() && t.IsObject() { if t.Get("type").String() == "enabled" { if b := t.Get("budget_tokens"); b.Exists() && b.Type == gjson.Number { budget := int(b.Int()) diff --git a/internal/translator/antigravity/claude/antigravity_claude_response.go b/internal/translator/antigravity/claude/antigravity_claude_response.go index 1672a835..c32918d6 100644 --- a/internal/translator/antigravity/claude/antigravity_claude_response.go +++ b/internal/translator/antigravity/claude/antigravity_claude_response.go @@ -73,6 +73,7 @@ func ConvertAntigravityResponseToClaude(_ context.Context, _ string, originalReq SessionID: deriveSessionID(originalRequestRawJSON), } } + modelName := gjson.GetBytes(requestRawJSON, "model").String() params := (*param).(*Params) @@ -139,13 +140,13 @@ func ConvertAntigravityResponseToClaude(_ context.Context, _ string, originalReq // log.Debug("Branch: signature_delta") if params.SessionID != "" && params.CurrentThinkingText.Len() > 0 { - cache.CacheSignature(params.SessionID, params.CurrentThinkingText.String(), thoughtSignature.String()) + cache.CacheSignature(modelName, params.SessionID, params.CurrentThinkingText.String(), thoughtSignature.String()) // log.Debugf("Cached signature for thinking block (sessionID=%s, textLen=%d)", params.SessionID, params.CurrentThinkingText.Len()) params.CurrentThinkingText.Reset() } output = output + "event: content_block_delta\n" - data, _ := sjson.Set(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"signature_delta","signature":""}}`, params.ResponseIndex), "delta.signature", thoughtSignature.String()) + data, _ := sjson.Set(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"signature_delta","signature":""}}`, params.ResponseIndex), "delta.signature", fmt.Sprintf("%s#%s", modelName, thoughtSignature.String())) output = output + fmt.Sprintf("data: %s\n\n\n", data) params.HasContent = true } else if params.ResponseType == 2 { // Continue existing thinking block if already in thinking state @@ -372,7 +373,7 @@ func resolveStopReason(params *Params) string { // - string: A Claude-compatible JSON response. func ConvertAntigravityResponseToClaudeNonStream(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) string { _ = originalRequestRawJSON - _ = requestRawJSON + modelName := gjson.GetBytes(requestRawJSON, "model").String() root := gjson.ParseBytes(rawJSON) promptTokens := root.Get("response.usageMetadata.promptTokenCount").Int() @@ -437,7 +438,7 @@ func ConvertAntigravityResponseToClaudeNonStream(_ context.Context, _ string, or block := `{"type":"thinking","thinking":""}` block, _ = sjson.Set(block, "thinking", thinkingBuilder.String()) if thinkingSignature != "" { - block, _ = sjson.Set(block, "signature", thinkingSignature) + block, _ = sjson.Set(block, "signature", fmt.Sprintf("%s#%s", modelName, thinkingSignature)) } responseJSON, _ = sjson.SetRaw(responseJSON, "content.-1", block) thinkingBuilder.Reset()