luispater commited on
Commit
2ca7e4b
·
unverified ·
1 Parent(s): 1455b87

refactor(cache, translator): refine signature caching logic and tests, replace session-based logic with model group handling

Browse files
internal/cache/signature_cache.go CHANGED
@@ -3,7 +3,6 @@ package cache
3
  import (
4
  "crypto/sha256"
5
  "encoding/hex"
6
- "fmt"
7
  "strings"
8
  "sync"
9
  "time"
@@ -25,18 +24,18 @@ const (
25
  // MinValidSignatureLen is the minimum length for a signature to be considered valid
26
  MinValidSignatureLen = 50
27
 
28
- // SessionCleanupInterval controls how often stale sessions are purged
29
- SessionCleanupInterval = 10 * time.Minute
30
  )
31
 
32
- // signatureCache stores signatures by sessionId -> textHash -> SignatureEntry
33
  var signatureCache sync.Map
34
 
35
- // sessionCleanupOnce ensures the background cleanup goroutine starts only once
36
- var sessionCleanupOnce sync.Once
37
 
38
- // sessionCache is the inner map type
39
- type sessionCache struct {
40
  mu sync.RWMutex
41
  entries map[string]SignatureEntry
42
  }
@@ -47,36 +46,36 @@ func hashText(text string) string {
47
  return hex.EncodeToString(h[:])[:SignatureTextHashLen]
48
  }
49
 
50
- // getOrCreateSession gets or creates a session cache
51
- func getOrCreateSession(sessionID string) *sessionCache {
52
  // Start background cleanup on first access
53
- sessionCleanupOnce.Do(startSessionCleanup)
54
 
55
- if val, ok := signatureCache.Load(sessionID); ok {
56
- return val.(*sessionCache)
57
  }
58
- sc := &sessionCache{entries: make(map[string]SignatureEntry)}
59
- actual, _ := signatureCache.LoadOrStore(sessionID, sc)
60
- return actual.(*sessionCache)
61
  }
62
 
63
- // startSessionCleanup launches a background goroutine that periodically
64
- // removes sessions where all entries have expired.
65
- func startSessionCleanup() {
66
  go func() {
67
- ticker := time.NewTicker(SessionCleanupInterval)
68
  defer ticker.Stop()
69
  for range ticker.C {
70
- purgeExpiredSessions()
71
  }
72
  }()
73
  }
74
 
75
- // purgeExpiredSessions removes sessions with no valid (non-expired) entries.
76
- func purgeExpiredSessions() {
77
  now := time.Now()
78
  signatureCache.Range(func(key, value any) bool {
79
- sc := value.(*sessionCache)
80
  sc.mu.Lock()
81
  // Remove expired entries
82
  for k, entry := range sc.entries {
@@ -86,7 +85,7 @@ func purgeExpiredSessions() {
86
  }
87
  isEmpty := len(sc.entries) == 0
88
  sc.mu.Unlock()
89
- // Remove session if empty
90
  if isEmpty {
91
  signatureCache.Delete(key)
92
  }
@@ -94,7 +93,7 @@ func purgeExpiredSessions() {
94
  })
95
  }
96
 
97
- // CacheSignature stores a thinking signature for a given session and text.
98
  // Used for Claude models that require signed thinking blocks in multi-turn conversations.
99
  func CacheSignature(modelName, text, signature string) {
100
  if text == "" || signature == "" {
@@ -104,9 +103,9 @@ func CacheSignature(modelName, text, signature string) {
104
  return
105
  }
106
 
107
- text = fmt.Sprintf("%s#%s", GetModelGroup(modelName), text)
108
  textHash := hashText(text)
109
- sc := getOrCreateSession(textHash)
110
  sc.mu.Lock()
111
  defer sc.mu.Unlock()
112
 
@@ -116,26 +115,25 @@ func CacheSignature(modelName, text, signature string) {
116
  }
117
  }
118
 
119
- // GetCachedSignature retrieves a cached signature for a given session and text.
120
  // Returns empty string if not found or expired.
121
  func GetCachedSignature(modelName, text string) string {
122
- family := GetModelGroup(modelName)
123
 
124
  if text == "" {
125
- if family == "gemini" {
126
  return "skip_thought_signature_validator"
127
  }
128
  return ""
129
  }
130
- text = fmt.Sprintf("%s#%s", GetModelGroup(modelName), text)
131
- val, ok := signatureCache.Load(hashText(text))
132
  if !ok {
133
- if family == "gemini" {
134
  return "skip_thought_signature_validator"
135
  }
136
  return ""
137
  }
138
- sc := val.(*sessionCache)
139
 
140
  textHash := hashText(text)
141
 
@@ -145,7 +143,7 @@ func GetCachedSignature(modelName, text string) string {
145
  entry, exists := sc.entries[textHash]
146
  if !exists {
147
  sc.mu.Unlock()
148
- if family == "gemini" {
149
  return "skip_thought_signature_validator"
150
  }
151
  return ""
@@ -153,7 +151,7 @@ func GetCachedSignature(modelName, text string) string {
153
  if now.Sub(entry.Timestamp) > SignatureCacheTTL {
154
  delete(sc.entries, textHash)
155
  sc.mu.Unlock()
156
- if family == "gemini" {
157
  return "skip_thought_signature_validator"
158
  }
159
  return ""
@@ -167,22 +165,17 @@ func GetCachedSignature(modelName, text string) string {
167
  return entry.Signature
168
  }
169
 
170
- // ClearSignatureCache clears signature cache for a specific session or all sessions.
171
- func ClearSignatureCache(sessionID string) {
172
- if sessionID != "" {
173
- signatureCache.Range(func(key, _ any) bool {
174
- kStr, ok := key.(string)
175
- if ok && strings.HasSuffix(kStr, "#"+sessionID) {
176
- signatureCache.Delete(key)
177
- }
178
- return true
179
- })
180
- } else {
181
  signatureCache.Range(func(key, _ any) bool {
182
  signatureCache.Delete(key)
183
  return true
184
  })
 
185
  }
 
 
186
  }
187
 
188
  // HasValidSignature checks if a signature is valid (non-empty and long enough)
 
3
  import (
4
  "crypto/sha256"
5
  "encoding/hex"
 
6
  "strings"
7
  "sync"
8
  "time"
 
24
  // MinValidSignatureLen is the minimum length for a signature to be considered valid
25
  MinValidSignatureLen = 50
26
 
27
+ // CacheCleanupInterval controls how often stale entries are purged
28
+ CacheCleanupInterval = 10 * time.Minute
29
  )
30
 
31
+ // signatureCache stores signatures by model group -> textHash -> SignatureEntry
32
  var signatureCache sync.Map
33
 
34
+ // cacheCleanupOnce ensures the background cleanup goroutine starts only once
35
+ var cacheCleanupOnce sync.Once
36
 
37
+ // groupCache is the inner map type
38
+ type groupCache struct {
39
  mu sync.RWMutex
40
  entries map[string]SignatureEntry
41
  }
 
46
  return hex.EncodeToString(h[:])[:SignatureTextHashLen]
47
  }
48
 
49
+ // getOrCreateGroupCache gets or creates a cache bucket for a model group
50
+ func getOrCreateGroupCache(groupKey string) *groupCache {
51
  // Start background cleanup on first access
52
+ cacheCleanupOnce.Do(startCacheCleanup)
53
 
54
+ if val, ok := signatureCache.Load(groupKey); ok {
55
+ return val.(*groupCache)
56
  }
57
+ sc := &groupCache{entries: make(map[string]SignatureEntry)}
58
+ actual, _ := signatureCache.LoadOrStore(groupKey, sc)
59
+ return actual.(*groupCache)
60
  }
61
 
62
+ // startCacheCleanup launches a background goroutine that periodically
63
+ // removes caches where all entries have expired.
64
+ func startCacheCleanup() {
65
  go func() {
66
+ ticker := time.NewTicker(CacheCleanupInterval)
67
  defer ticker.Stop()
68
  for range ticker.C {
69
+ purgeExpiredCaches()
70
  }
71
  }()
72
  }
73
 
74
+ // purgeExpiredCaches removes caches with no valid (non-expired) entries.
75
+ func purgeExpiredCaches() {
76
  now := time.Now()
77
  signatureCache.Range(func(key, value any) bool {
78
+ sc := value.(*groupCache)
79
  sc.mu.Lock()
80
  // Remove expired entries
81
  for k, entry := range sc.entries {
 
85
  }
86
  isEmpty := len(sc.entries) == 0
87
  sc.mu.Unlock()
88
+ // Remove cache bucket if empty
89
  if isEmpty {
90
  signatureCache.Delete(key)
91
  }
 
93
  })
94
  }
95
 
96
+ // CacheSignature stores a thinking signature for a given model group and text.
97
  // Used for Claude models that require signed thinking blocks in multi-turn conversations.
98
  func CacheSignature(modelName, text, signature string) {
99
  if text == "" || signature == "" {
 
103
  return
104
  }
105
 
106
+ groupKey := GetModelGroup(modelName)
107
  textHash := hashText(text)
108
+ sc := getOrCreateGroupCache(groupKey)
109
  sc.mu.Lock()
110
  defer sc.mu.Unlock()
111
 
 
115
  }
116
  }
117
 
118
+ // GetCachedSignature retrieves a cached signature for a given model group and text.
119
  // Returns empty string if not found or expired.
120
  func GetCachedSignature(modelName, text string) string {
121
+ groupKey := GetModelGroup(modelName)
122
 
123
  if text == "" {
124
+ if groupKey == "gemini" {
125
  return "skip_thought_signature_validator"
126
  }
127
  return ""
128
  }
129
+ val, ok := signatureCache.Load(groupKey)
 
130
  if !ok {
131
+ if groupKey == "gemini" {
132
  return "skip_thought_signature_validator"
133
  }
134
  return ""
135
  }
136
+ sc := val.(*groupCache)
137
 
138
  textHash := hashText(text)
139
 
 
143
  entry, exists := sc.entries[textHash]
144
  if !exists {
145
  sc.mu.Unlock()
146
+ if groupKey == "gemini" {
147
  return "skip_thought_signature_validator"
148
  }
149
  return ""
 
151
  if now.Sub(entry.Timestamp) > SignatureCacheTTL {
152
  delete(sc.entries, textHash)
153
  sc.mu.Unlock()
154
+ if groupKey == "gemini" {
155
  return "skip_thought_signature_validator"
156
  }
157
  return ""
 
165
  return entry.Signature
166
  }
167
 
168
+ // ClearSignatureCache clears signature cache for a specific model group or all groups.
169
+ func ClearSignatureCache(modelName string) {
170
+ if modelName == "" {
 
 
 
 
 
 
 
 
171
  signatureCache.Range(func(key, _ any) bool {
172
  signatureCache.Delete(key)
173
  return true
174
  })
175
+ return
176
  }
177
+ groupKey := GetModelGroup(modelName)
178
+ signatureCache.Delete(groupKey)
179
  }
180
 
181
  // HasValidSignature checks if a signature is valid (non-empty and long enough)
internal/cache/signature_cache_test.go CHANGED
@@ -21,33 +21,33 @@ func TestCacheSignature_BasicStorageAndRetrieval(t *testing.T) {
21
  }
22
  }
23
 
24
- func TestCacheSignature_DifferentSessions(t *testing.T) {
25
  ClearSignatureCache("")
26
 
27
- text := "Same text in different sessions"
28
  sig1 := "signature1_1234567890123456789012345678901234567890123456"
29
  sig2 := "signature2_1234567890123456789012345678901234567890123456"
30
 
31
- CacheSignature("test-model", text, sig1)
32
- CacheSignature("test-model", text, sig2)
33
 
34
- if GetCachedSignature("test-model", text) != sig1 {
35
- t.Error("Session-a signature mismatch")
36
  }
37
- if GetCachedSignature("test-model", text) != sig2 {
38
- t.Error("Session-b signature mismatch")
39
  }
40
  }
41
 
42
  func TestCacheSignature_NotFound(t *testing.T) {
43
  ClearSignatureCache("")
44
 
45
- // Non-existent session
46
  if got := GetCachedSignature("test-model", "some text"); got != "" {
47
- t.Errorf("Expected empty string for nonexistent session, got '%s'", got)
48
  }
49
 
50
- // Existing session but different text
51
  CacheSignature("test-model", "text-a", "sigA12345678901234567890123456789012345678901234567890")
52
  if got := GetCachedSignature("test-model", "text-b"); got != "" {
53
  t.Errorf("Expected empty string for different text, got '%s'", got)
@@ -58,7 +58,6 @@ func TestCacheSignature_EmptyInputs(t *testing.T) {
58
  ClearSignatureCache("")
59
 
60
  // All empty/invalid inputs should be no-ops
61
- CacheSignature("test-model", "text", "sig12345678901234567890123456789012345678901234567890")
62
  CacheSignature("test-model", "", "sig12345678901234567890123456789012345678901234567890")
63
  CacheSignature("test-model", "text", "")
64
  CacheSignature("test-model", "text", "short") // Too short
@@ -81,20 +80,21 @@ func TestCacheSignature_ShortSignatureRejected(t *testing.T) {
81
  }
82
  }
83
 
84
- func TestClearSignatureCache_SpecificSession(t *testing.T) {
85
  ClearSignatureCache("")
86
 
87
- sig := "validSig1234567890123456789012345678901234567890123456"
88
- CacheSignature("test-model", "text", sig)
89
- CacheSignature("test-model", "text", sig)
 
90
 
91
- ClearSignatureCache("session-1")
92
 
93
- if got := GetCachedSignature("test-model", "text"); got != "" {
94
- t.Error("session-1 should be cleared")
95
  }
96
- if got := GetCachedSignature("test-model", "text"); got != sig {
97
- t.Error("session-2 should still exist")
98
  }
99
  }
100
 
@@ -108,10 +108,10 @@ func TestClearSignatureCache_AllSessions(t *testing.T) {
108
  ClearSignatureCache("")
109
 
110
  if got := GetCachedSignature("test-model", "text"); got != "" {
111
- t.Error("session-1 should be cleared")
112
  }
113
  if got := GetCachedSignature("test-model", "text"); got != "" {
114
- t.Error("session-2 should be cleared")
115
  }
116
  }
117
 
 
21
  }
22
  }
23
 
24
+ func TestCacheSignature_DifferentModelGroups(t *testing.T) {
25
  ClearSignatureCache("")
26
 
27
+ text := "Same text across models"
28
  sig1 := "signature1_1234567890123456789012345678901234567890123456"
29
  sig2 := "signature2_1234567890123456789012345678901234567890123456"
30
 
31
+ CacheSignature("claude-sonnet-4-5-thinking", text, sig1)
32
+ CacheSignature("gpt-4o", text, sig2)
33
 
34
+ if GetCachedSignature("claude-sonnet-4-5-thinking", text) != sig1 {
35
+ t.Error("Claude signature mismatch")
36
  }
37
+ if GetCachedSignature("gpt-4o", text) != sig2 {
38
+ t.Error("GPT signature mismatch")
39
  }
40
  }
41
 
42
  func TestCacheSignature_NotFound(t *testing.T) {
43
  ClearSignatureCache("")
44
 
45
+ // Non-existent cache entry
46
  if got := GetCachedSignature("test-model", "some text"); got != "" {
47
+ t.Errorf("Expected empty string for missing entry, got '%s'", got)
48
  }
49
 
50
+ // Existing cache but different text
51
  CacheSignature("test-model", "text-a", "sigA12345678901234567890123456789012345678901234567890")
52
  if got := GetCachedSignature("test-model", "text-b"); got != "" {
53
  t.Errorf("Expected empty string for different text, got '%s'", got)
 
58
  ClearSignatureCache("")
59
 
60
  // All empty/invalid inputs should be no-ops
 
61
  CacheSignature("test-model", "", "sig12345678901234567890123456789012345678901234567890")
62
  CacheSignature("test-model", "text", "")
63
  CacheSignature("test-model", "text", "short") // Too short
 
80
  }
81
  }
82
 
83
+ func TestClearSignatureCache_ModelGroup(t *testing.T) {
84
  ClearSignatureCache("")
85
 
86
+ sigClaude := "validSig1234567890123456789012345678901234567890123456"
87
+ sigGpt := "validSig9876543210987654321098765432109876543210987654"
88
+ CacheSignature("claude-sonnet-4-5-thinking", "text", sigClaude)
89
+ CacheSignature("gpt-4o", "text", sigGpt)
90
 
91
+ ClearSignatureCache("claude-sonnet-4-5-thinking")
92
 
93
+ if got := GetCachedSignature("claude-sonnet-4-5-thinking", "text"); got != "" {
94
+ t.Error("Claude cache should be cleared")
95
  }
96
+ if got := GetCachedSignature("gpt-4o", "text"); got != sigGpt {
97
+ t.Error("GPT cache should still exist")
98
  }
99
  }
100
 
 
108
  ClearSignatureCache("")
109
 
110
  if got := GetCachedSignature("test-model", "text"); got != "" {
111
+ t.Error("cache should be cleared")
112
  }
113
  if got := GetCachedSignature("test-model", "text"); got != "" {
114
+ t.Error("cache should be cleared")
115
  }
116
  }
117
 
internal/translator/antigravity/claude/antigravity_claude_request.go CHANGED
@@ -98,32 +98,38 @@ func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _
98
  // Use GetThinkingText to handle wrapped thinking objects
99
  thinkingText := thinking.GetThinkingText(contentResult)
100
 
101
- // Always try cached signature first (more reliable than client-provided)
102
- // Client may send stale or invalid signatures from different sessions
103
  signature := ""
104
- if thinkingText != "" {
105
- if cachedSig := cache.GetCachedSignature(modelName, thinkingText); cachedSig != "" {
106
- signature = cachedSig
107
- // log.Debugf("Using cached signature for thinking block")
 
 
 
 
 
 
 
 
 
108
  }
109
- }
110
 
111
- // Fallback to client signature only if cache miss and client signature is valid
112
- if signature == "" {
113
- signatureResult := contentResult.Get("signature")
114
- clientSignature := ""
115
- if signatureResult.Exists() && signatureResult.String() != "" {
116
- arrayClientSignatures := strings.SplitN(signatureResult.String(), "#", 2)
117
- if len(arrayClientSignatures) == 2 {
118
- if modelName == arrayClientSignatures[0] {
119
- clientSignature = arrayClientSignatures[1]
120
  }
121
  }
 
 
 
 
122
  }
123
- if cache.HasValidSignature(modelName, clientSignature) {
124
- signature = clientSignature
125
- }
126
- // log.Debugf("Using client-provided signature for thinking block")
127
  }
128
 
129
  // Store for subsequent tool_use in the same message
 
98
  // Use GetThinkingText to handle wrapped thinking objects
99
  thinkingText := thinking.GetThinkingText(contentResult)
100
 
 
 
101
  signature := ""
102
+ signatureResult := contentResult.Get("signature")
103
+ hasClientSignature := signatureResult.Exists() && signatureResult.String() != ""
104
+
105
+ // Only consider cached signatures when the client provided a signature.
106
+ // Unsigned thinking blocks must be dropped.
107
+ if hasClientSignature {
108
+ // Always try cached signature first (more reliable than client-provided)
109
+ // Client may send stale or invalid signatures from other requests
110
+ if thinkingText != "" {
111
+ if cachedSig := cache.GetCachedSignature(modelName, thinkingText); cachedSig != "" {
112
+ signature = cachedSig
113
+ // log.Debugf("Using cached signature for thinking block")
114
+ }
115
  }
 
116
 
117
+ // Fallback to client signature only if cache miss and client signature is valid
118
+ if signature == "" {
119
+ clientSignature := ""
120
+ if signatureResult.Exists() && signatureResult.String() != "" {
121
+ arrayClientSignatures := strings.SplitN(signatureResult.String(), "#", 2)
122
+ if len(arrayClientSignatures) == 2 {
123
+ if modelName == arrayClientSignatures[0] {
124
+ clientSignature = arrayClientSignatures[1]
125
+ }
126
  }
127
  }
128
+ if cache.HasValidSignature(modelName, clientSignature) {
129
+ signature = clientSignature
130
+ }
131
+ // log.Debugf("Using client-provided signature for thinking block")
132
  }
 
 
 
 
133
  }
134
 
135
  // Store for subsequent tool_use in the same message
internal/translator/antigravity/claude/antigravity_claude_request_test.go CHANGED
@@ -78,9 +78,7 @@ func TestConvertClaudeRequestToAntigravity_ThinkingBlocks(t *testing.T) {
78
  validSignature := "abc123validSignature1234567890123456789012345678901234567890"
79
  thinkingText := "Let me think..."
80
 
81
- // Pre-cache the signature (simulating a response from the same session)
82
- // The session ID is derived from the first user message hash
83
- // Since there's no user message in this test, we need to add one
84
  inputJSON := []byte(`{
85
  "model": "claude-sonnet-4-5-thinking",
86
  "messages": [
 
78
  validSignature := "abc123validSignature1234567890123456789012345678901234567890"
79
  thinkingText := "Let me think..."
80
 
81
+ // Pre-cache the signature (simulating a previous response for the same thinking text)
 
 
82
  inputJSON := []byte(`{
83
  "model": "claude-sonnet-4-5-thinking",
84
  "messages": [
internal/translator/antigravity/claude/antigravity_claude_response.go CHANGED
@@ -139,7 +139,7 @@ func ConvertAntigravityResponseToClaude(_ context.Context, _ string, originalReq
139
 
140
  if params.CurrentThinkingText.Len() > 0 {
141
  cache.CacheSignature(modelName, params.CurrentThinkingText.String(), thoughtSignature.String())
142
- // log.Debugf("Cached signature for thinking block (sessionID=%s, textLen=%d)", params.SessionID, params.CurrentThinkingText.Len())
143
  params.CurrentThinkingText.Reset()
144
  }
145
 
 
139
 
140
  if params.CurrentThinkingText.Len() > 0 {
141
  cache.CacheSignature(modelName, params.CurrentThinkingText.String(), thoughtSignature.String())
142
+ // log.Debugf("Cached signature for thinking block (textLen=%d)", params.CurrentThinkingText.Len())
143
  params.CurrentThinkingText.Reset()
144
  }
145
 
internal/translator/antigravity/claude/antigravity_claude_response_test.go CHANGED
@@ -12,10 +12,10 @@ import (
12
  // Signature Caching Tests
13
  // ============================================================================
14
 
15
- func TestConvertAntigravityResponseToClaude_SessionIDDerived(t *testing.T) {
16
  cache.ClearSignatureCache("")
17
 
18
- // Request with user message - should derive session ID
19
  requestJSON := []byte(`{
20
  "messages": [
21
  {"role": "user", "content": [{"type": "text", "text": "Hello world"}]}
@@ -37,10 +37,12 @@ func TestConvertAntigravityResponseToClaude_SessionIDDerived(t *testing.T) {
37
  ctx := context.Background()
38
  ConvertAntigravityResponseToClaude(ctx, "claude-sonnet-4-5-thinking", requestJSON, requestJSON, responseJSON, &param)
39
 
40
- // Verify session ID was set
41
  params := param.(*Params)
42
- if params.SessionID == "" {
43
- t.Error("SessionID should be derived from request")
 
 
 
44
  }
45
  }
46
 
@@ -130,12 +132,8 @@ func TestConvertAntigravityResponseToClaude_SignatureCached(t *testing.T) {
130
  // Process thinking chunk
131
  ConvertAntigravityResponseToClaude(ctx, "claude-sonnet-4-5-thinking", requestJSON, requestJSON, thinkingChunk, &param)
132
  params := param.(*Params)
133
- sessionID := params.SessionID
134
  thinkingText := params.CurrentThinkingText.String()
135
 
136
- if sessionID == "" {
137
- t.Fatal("SessionID should be set")
138
- }
139
  if thinkingText == "" {
140
  t.Fatal("Thinking text should be accumulated")
141
  }
 
12
  // Signature Caching Tests
13
  // ============================================================================
14
 
15
+ func TestConvertAntigravityResponseToClaude_ParamsInitialized(t *testing.T) {
16
  cache.ClearSignatureCache("")
17
 
18
+ // Request with user message - should initialize params
19
  requestJSON := []byte(`{
20
  "messages": [
21
  {"role": "user", "content": [{"type": "text", "text": "Hello world"}]}
 
37
  ctx := context.Background()
38
  ConvertAntigravityResponseToClaude(ctx, "claude-sonnet-4-5-thinking", requestJSON, requestJSON, responseJSON, &param)
39
 
 
40
  params := param.(*Params)
41
+ if !params.HasFirstResponse {
42
+ t.Error("HasFirstResponse should be set after first chunk")
43
+ }
44
+ if params.CurrentThinkingText.Len() == 0 {
45
+ t.Error("Thinking text should be accumulated")
46
  }
47
  }
48
 
 
132
  // Process thinking chunk
133
  ConvertAntigravityResponseToClaude(ctx, "claude-sonnet-4-5-thinking", requestJSON, requestJSON, thinkingChunk, &param)
134
  params := param.(*Params)
 
135
  thinkingText := params.CurrentThinkingText.String()
136
 
 
 
 
137
  if thinkingText == "" {
138
  t.Fatal("Thinking text should be accumulated")
139
  }
internal/translator/antigravity/gemini/antigravity_gemini_request.go CHANGED
@@ -99,36 +99,44 @@ func ConvertGeminiRequestToAntigravity(modelName string, inputRawJSON []byte, _
99
  }
100
 
101
  // Gemini-specific handling for non-Claude models:
 
102
  // - Add skip_thought_signature_validator to functionCall parts so upstream can bypass signature validation.
103
- // - Also mark thinking parts with the same sentinel when present (we keep the parts; we only annotate them).
104
  if !strings.Contains(modelName, "claude") {
105
  const skipSentinel = "skip_thought_signature_validator"
106
 
107
  gjson.GetBytes(rawJSON, "request.contents").ForEach(func(contentIdx, content gjson.Result) bool {
108
- if content.Get("role").String() == "model" {
109
- // First pass: collect indices of thinking parts to mark with skip sentinel
110
- var thinkingIndicesToSkipSignature []int64
111
- content.Get("parts").ForEach(func(partIdx, part gjson.Result) bool {
112
- // Collect indices of thinking blocks to mark with skip sentinel
113
- if part.Get("thought").Bool() {
114
- thinkingIndicesToSkipSignature = append(thinkingIndicesToSkipSignature, partIdx.Int())
115
- }
116
- // Add skip sentinel to functionCall parts
117
- if part.Get("functionCall").Exists() {
118
- existingSig := part.Get("thoughtSignature").String()
119
- if existingSig == "" || len(existingSig) < 50 {
120
- rawJSON, _ = sjson.SetBytes(rawJSON, fmt.Sprintf("request.contents.%d.parts.%d.thoughtSignature", contentIdx.Int(), partIdx.Int()), skipSentinel)
 
 
 
 
 
 
 
 
 
 
 
121
  }
122
  }
123
- return true
124
- })
125
-
126
- // Add skip_thought_signature_validator sentinel to thinking blocks in reverse order to preserve indices
127
- for i := len(thinkingIndicesToSkipSignature) - 1; i >= 0; i-- {
128
- idx := thinkingIndicesToSkipSignature[i]
129
- rawJSON, _ = sjson.SetBytes(rawJSON, fmt.Sprintf("request.contents.%d.parts.%d.thoughtSignature", contentIdx.Int(), idx), skipSentinel)
130
  }
 
 
131
  }
 
 
132
  return true
133
  })
134
  }
 
99
  }
100
 
101
  // Gemini-specific handling for non-Claude models:
102
+ // - Remove thinking parts entirely.
103
  // - Add skip_thought_signature_validator to functionCall parts so upstream can bypass signature validation.
 
104
  if !strings.Contains(modelName, "claude") {
105
  const skipSentinel = "skip_thought_signature_validator"
106
 
107
  gjson.GetBytes(rawJSON, "request.contents").ForEach(func(contentIdx, content gjson.Result) bool {
108
+ if content.Get("role").String() != "model" {
109
+ return true
110
+ }
111
+ partsResult := content.Get("parts")
112
+ if !partsResult.IsArray() {
113
+ return true
114
+ }
115
+
116
+ parts := partsResult.Array()
117
+ newParts := make([]interface{}, 0, len(parts))
118
+ for _, part := range parts {
119
+ if part.Get("thought").Bool() {
120
+ continue
121
+ }
122
+
123
+ partRaw := part.Raw
124
+ if part.Get("functionCall").Exists() {
125
+ existingSig := part.Get("thoughtSignature").String()
126
+ if existingSig == "" || len(existingSig) < 50 {
127
+ updatedPart, errSet := sjson.Set(partRaw, "thoughtSignature", skipSentinel)
128
+ if errSet != nil {
129
+ log.WithError(errSet).Debug("failed to set thoughtSignature on functionCall part")
130
+ } else {
131
+ partRaw = updatedPart
132
  }
133
  }
 
 
 
 
 
 
 
134
  }
135
+
136
+ newParts = append(newParts, gjson.Parse(partRaw).Value())
137
  }
138
+
139
+ rawJSON, _ = sjson.SetBytes(rawJSON, fmt.Sprintf("request.contents.%d.parts", contentIdx.Int()), newParts)
140
  return true
141
  })
142
  }