feat(translator): improve signature handling by associating with model name in cache functions

This commit is contained in:
Luis Pater
2026-01-20 13:31:36 +08:00
parent 6184c43319
commit 020e61d0da
3 changed files with 31 additions and 20 deletions

View File

@@ -3,6 +3,7 @@ package cache
import ( import (
"crypto/sha256" "crypto/sha256"
"encoding/hex" "encoding/hex"
"fmt"
"sync" "sync"
"time" "time"
) )
@@ -94,7 +95,7 @@ func purgeExpiredSessions() {
// CacheSignature stores a thinking signature for a given session and text. // CacheSignature stores a thinking signature for a given session and text.
// Used for Claude models that require signed thinking blocks in multi-turn conversations. // 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 == "" { if sessionID == "" || text == "" || signature == "" {
return return
} }
@@ -102,7 +103,7 @@ func CacheSignature(sessionID, text, signature string) {
return return
} }
sc := getOrCreateSession(sessionID) sc := getOrCreateSession(fmt.Sprintf("%s#%s", modelName, sessionID))
textHash := hashText(text) textHash := hashText(text)
sc.mu.Lock() sc.mu.Lock()
@@ -116,12 +117,12 @@ func CacheSignature(sessionID, text, signature string) {
// GetCachedSignature retrieves a cached signature for a given session and text. // GetCachedSignature retrieves a cached signature for a given session and text.
// Returns empty string if not found or expired. // Returns empty string if not found or expired.
func GetCachedSignature(sessionID, text string) string { func GetCachedSignature(modelName, sessionID, text string) string {
if sessionID == "" || text == "" { if sessionID == "" || text == "" {
return "" return ""
} }
val, ok := signatureCache.Load(sessionID) val, ok := signatureCache.Load(fmt.Sprintf("%s#%s", modelName, sessionID))
if !ok { if !ok {
return "" return ""
} }

View File

@@ -69,6 +69,7 @@ func deriveSessionID(rawJSON []byte) string {
// Returns: // Returns:
// - []byte: The transformed request data in Gemini CLI API format // - []byte: The transformed request data in Gemini CLI API format
func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _ bool) []byte { func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _ bool) []byte {
enableThoughtTranslate := true
rawJSON := bytes.Clone(inputRawJSON) rawJSON := bytes.Clone(inputRawJSON)
// Derive session ID for signature caching // Derive session ID for signature caching
@@ -132,27 +133,34 @@ 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 := thinking.GetThinkingText(contentResult) 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) // 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
signature := "" signature := ""
if sessionID != "" && thinkingText != "" { if sessionID != "" && thinkingText != "" {
if cachedSig := cache.GetCachedSignature(sessionID, thinkingText); cachedSig != "" { if cachedSig := cache.GetCachedSignature(modelName, sessionID, thinkingText); cachedSig != "" {
signature = cachedSig signature = cachedSig
// log.Debugf("Using cached signature for thinking block") // log.Debugf("Using cached signature for thinking block")
} }
} }
// NOTE: We do NOT fallback to client signature anymore. // Fallback to client signature only if cache miss and client signature is valid
// Client signatures from Claude models are incompatible with Antigravity/Gemini API. if signature == "" {
// When switching between models (e.g., Claude Opus -> Gemini Flash), the Claude signatureResult := contentResult.Get("signature")
// signatures will cause "Corrupted thought signature" errors. clientSignature := ""
// If we have no cached signature, the thinking block will be skipped below. 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 // Store for subsequent tool_use in the same message
if cache.HasValidSignature(signature) { if cache.HasValidSignature(signature) {
@@ -167,6 +175,7 @@ func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _
// Converting to text would break this requirement // Converting to text would break this requirement
if isUnsigned { if isUnsigned {
// log.Debugf("Dropping unsigned thinking block (no valid signature)") // log.Debugf("Dropping unsigned thinking block (no valid signature)")
enableThoughtTranslate = false
continue continue
} }
@@ -394,7 +403,7 @@ func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _
} }
// Map Anthropic thinking -> Gemini thinkingBudget/include_thoughts when type==enabled // 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 t.Get("type").String() == "enabled" {
if b := t.Get("budget_tokens"); b.Exists() && b.Type == gjson.Number { if b := t.Get("budget_tokens"); b.Exists() && b.Type == gjson.Number {
budget := int(b.Int()) budget := int(b.Int())

View File

@@ -73,6 +73,7 @@ func ConvertAntigravityResponseToClaude(_ context.Context, _ string, originalReq
SessionID: deriveSessionID(originalRequestRawJSON), SessionID: deriveSessionID(originalRequestRawJSON),
} }
} }
modelName := gjson.GetBytes(requestRawJSON, "model").String()
params := (*param).(*Params) params := (*param).(*Params)
@@ -139,13 +140,13 @@ func ConvertAntigravityResponseToClaude(_ context.Context, _ string, originalReq
// log.Debug("Branch: signature_delta") // log.Debug("Branch: signature_delta")
if params.SessionID != "" && params.CurrentThinkingText.Len() > 0 { 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()) // log.Debugf("Cached signature for thinking block (sessionID=%s, textLen=%d)", params.SessionID, params.CurrentThinkingText.Len())
params.CurrentThinkingText.Reset() params.CurrentThinkingText.Reset()
} }
output = output + "event: content_block_delta\n" 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) output = output + fmt.Sprintf("data: %s\n\n\n", data)
params.HasContent = true params.HasContent = true
} else if params.ResponseType == 2 { // Continue existing thinking block if already in thinking state } 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. // - string: A Claude-compatible JSON response.
func ConvertAntigravityResponseToClaudeNonStream(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) string { func ConvertAntigravityResponseToClaudeNonStream(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) string {
_ = originalRequestRawJSON _ = originalRequestRawJSON
_ = requestRawJSON modelName := gjson.GetBytes(requestRawJSON, "model").String()
root := gjson.ParseBytes(rawJSON) root := gjson.ParseBytes(rawJSON)
promptTokens := root.Get("response.usageMetadata.promptTokenCount").Int() promptTokens := root.Get("response.usageMetadata.promptTokenCount").Int()
@@ -437,7 +438,7 @@ func ConvertAntigravityResponseToClaudeNonStream(_ context.Context, _ string, or
block := `{"type":"thinking","thinking":""}` block := `{"type":"thinking","thinking":""}`
block, _ = sjson.Set(block, "thinking", thinkingBuilder.String()) block, _ = sjson.Set(block, "thinking", thinkingBuilder.String())
if thinkingSignature != "" { 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) responseJSON, _ = sjson.SetRaw(responseJSON, "content.-1", block)
thinkingBuilder.Reset() thinkingBuilder.Reset()