Unified Agent + reusable Go agent core.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix(gemini): harden tool call thought signature handling

Lyric a937fb4a 2829a893

+120
+59
providers/uniai/client.go
··· 2 2 3 3 import ( 4 4 "context" 5 + "crypto/sha256" 6 + "encoding/base64" 5 7 "encoding/json" 6 8 "fmt" 7 9 "strconv" ··· 122 124 } 123 125 124 126 toolCalls := toLLMToolCalls(resp.ToolCalls) 127 + if strings.EqualFold(strings.TrimSpace(c.provider), "gemini") { 128 + toolCalls = ensureGeminiToolCallThoughtSignatures(toolCalls) 129 + } 125 130 126 131 return llm.Result{ 127 132 Text: resp.Text, ··· 332 337 return nil 333 338 } 334 339 return out 340 + } 341 + 342 + func ensureGeminiToolCallThoughtSignatures(calls []llm.ToolCall) []llm.ToolCall { 343 + if len(calls) == 0 { 344 + return calls 345 + } 346 + 347 + out := append([]llm.ToolCall(nil), calls...) 348 + lastSig := "" 349 + for i := range out { 350 + sig := strings.TrimSpace(out[i].ThoughtSignature) 351 + if sig == "" { 352 + _, decoded := splitGeminiToolCallIDAndThoughtSignature(out[i].ID) 353 + sig = decoded 354 + } 355 + if sig == "" { 356 + sig = lastSig 357 + } 358 + if sig == "" { 359 + sig = synthesizeGeminiThoughtSignature(out[i]) 360 + } 361 + out[i].ThoughtSignature = sig 362 + if sig != "" { 363 + lastSig = sig 364 + } 365 + } 366 + return out 367 + } 368 + 369 + func splitGeminiToolCallIDAndThoughtSignature(callID string) (string, string) { 370 + callID = strings.TrimSpace(callID) 371 + if callID == "" { 372 + return "", "" 373 + } 374 + idx := strings.LastIndex(callID, "|ts:") 375 + if idx <= 0 || idx+4 >= len(callID) { 376 + return callID, "" 377 + } 378 + encoded := callID[idx+4:] 379 + decoded, err := base64.RawURLEncoding.DecodeString(encoded) 380 + if err != nil { 381 + return callID, "" 382 + } 383 + baseID := strings.TrimSpace(callID[:idx]) 384 + if baseID == "" { 385 + return callID, "" 386 + } 387 + return baseID, string(decoded) 388 + } 389 + 390 + func synthesizeGeminiThoughtSignature(call llm.ToolCall) string { 391 + seed := strings.TrimSpace(call.ID) + "\n" + strings.TrimSpace(call.Name) + "\n" + strings.TrimSpace(call.RawArguments) 392 + sum := sha256.Sum256([]byte(seed)) 393 + return fmt.Sprintf("mmts_%x", sum[:8]) 335 394 } 336 395 337 396 func shouldRetryWithoutResponseFormat(err error) bool {
+61
providers/uniai/client_test.go
··· 94 94 t.Fatalf("expected exact raw arguments %q, got %q", origArgs, uniaiCalls[0].Function.Arguments) 95 95 } 96 96 } 97 + 98 + func TestEnsureGeminiToolCallThoughtSignaturesDecodeFromID(t *testing.T) { 99 + rawID := "call_1|ts:c2lnX2Zyb21faWQ" 100 + calls := []llm.ToolCall{{ 101 + ID: rawID, 102 + Name: "read_file", 103 + RawArguments: `{"path":"/tmp/a.txt"}`, 104 + }} 105 + 106 + out := ensureGeminiToolCallThoughtSignatures(calls) 107 + if len(out) != 1 { 108 + t.Fatalf("expected 1 call, got %d", len(out)) 109 + } 110 + if out[0].ThoughtSignature != "sig_from_id" { 111 + t.Fatalf("expected signature decoded from id, got %q", out[0].ThoughtSignature) 112 + } 113 + } 114 + 115 + func TestEnsureGeminiToolCallThoughtSignaturesCarryForward(t *testing.T) { 116 + calls := []llm.ToolCall{ 117 + { 118 + ID: "call_1", 119 + Name: "read_file", 120 + RawArguments: `{"path":"a"}`, 121 + ThoughtSignature: "sig_1", 122 + }, 123 + { 124 + ID: "call_2", 125 + Name: "read_file", 126 + RawArguments: `{"path":"b"}`, 127 + }, 128 + } 129 + 130 + out := ensureGeminiToolCallThoughtSignatures(calls) 131 + if len(out) != 2 { 132 + t.Fatalf("expected 2 calls, got %d", len(out)) 133 + } 134 + if out[1].ThoughtSignature != "sig_1" { 135 + t.Fatalf("expected second call to inherit prior signature, got %q", out[1].ThoughtSignature) 136 + } 137 + } 138 + 139 + func TestEnsureGeminiToolCallThoughtSignaturesSynthesize(t *testing.T) { 140 + calls := []llm.ToolCall{{ 141 + ID: "call_42", 142 + Name: "read_file", 143 + RawArguments: `{"path":"a"}`, 144 + }} 145 + 146 + out1 := ensureGeminiToolCallThoughtSignatures(calls) 147 + out2 := ensureGeminiToolCallThoughtSignatures(calls) 148 + if len(out1) != 1 || len(out2) != 1 { 149 + t.Fatalf("unexpected output lengths: %d, %d", len(out1), len(out2)) 150 + } 151 + if out1[0].ThoughtSignature == "" { 152 + t.Fatalf("expected synthesized signature") 153 + } 154 + if out1[0].ThoughtSignature != out2[0].ThoughtSignature { 155 + t.Fatalf("expected deterministic synthesized signature, got %q vs %q", out1[0].ThoughtSignature, out2[0].ThoughtSignature) 156 + } 157 + }