From a2f8f59192525eb5b3e6f153b9496d2cd102afbb Mon Sep 17 00:00:00 2001 From: sowar1987 <178468696@qq.com> Date: Wed, 21 Jan 2026 09:09:40 +0800 Subject: [PATCH 1/4] Fix Gemini function-calling INVALID_ARGUMENT by relaxing Gemini tool validation and cleaning schema --- .../runtime/executor/antigravity_executor.go | 19 +++- .../gemini-cli_openai_request.go | 8 +- .../gemini-cli_openai_response.go | 6 +- .../gemini-cli/gemini_gemini-cli_request.go | 13 --- .../chat-completions/gemini_openai_request.go | 8 +- .../gemini_openai_response.go | 6 +- .../gemini_openai-responses_request.go | 10 +- internal/util/gemini_schema.go | 97 ++++++++++++++++++- 8 files changed, 138 insertions(+), 29 deletions(-) diff --git a/internal/runtime/executor/antigravity_executor.go b/internal/runtime/executor/antigravity_executor.go index 897004fb..e17d525c 100644 --- a/internal/runtime/executor/antigravity_executor.go +++ b/internal/runtime/executor/antigravity_executor.go @@ -1214,6 +1214,17 @@ func (e *AntigravityExecutor) buildRequest(ctx context.Context, auth *cliproxyau // const->enum conversion, and flattening of types/anyOf. strJSON = util.CleanJSONSchemaForAntigravity(strJSON) + payload = []byte(strJSON) + } else { + strJSON := string(payload) + paths := make([]string, 0) + util.Walk(gjson.Parse(strJSON), "", "parametersJsonSchema", &paths) + for _, p := range paths { + strJSON, _ = util.RenameKey(strJSON, p, p[:len(p)-len("parametersJsonSchema")]+"parameters") + } + // Clean tool schemas for Gemini to remove unsupported JSON Schema keywords + // without adding empty-schema placeholders. + strJSON = util.CleanJSONSchemaForGemini(strJSON) payload = []byte(strJSON) } @@ -1405,7 +1416,13 @@ func geminiToAntigravity(modelName string, payload []byte, projectID string) []b template, _ = sjson.Set(template, "request.sessionId", generateStableSessionID(payload)) template, _ = sjson.Delete(template, "request.safetySettings") - // template, _ = sjson.Set(template, "request.toolConfig.functionCallingConfig.mode", "VALIDATED") + if toolConfig := gjson.Get(template, "toolConfig"); toolConfig.Exists() && !gjson.Get(template, "request.toolConfig").Exists() { + template, _ = sjson.SetRaw(template, "request.toolConfig", toolConfig.Raw) + template, _ = sjson.Delete(template, "toolConfig") + } + if strings.Contains(modelName, "claude") { + template, _ = sjson.Set(template, "request.toolConfig.functionCallingConfig.mode", "VALIDATED") + } if strings.Contains(modelName, "claude") || strings.Contains(modelName, "gemini-3-pro-high") { gjson.Get(template, "request.tools").ForEach(func(key, tool gjson.Result) bool { diff --git a/internal/translator/gemini-cli/openai/chat-completions/gemini-cli_openai_request.go b/internal/translator/gemini-cli/openai/chat-completions/gemini-cli_openai_request.go index 85669689..cf3cbaa4 100644 --- a/internal/translator/gemini-cli/openai/chat-completions/gemini-cli_openai_request.go +++ b/internal/translator/gemini-cli/openai/chat-completions/gemini-cli_openai_request.go @@ -249,7 +249,8 @@ func ConvertOpenAIRequestToGeminiCLI(modelName string, inputRawJSON []byte, _ bo fid := tc.Get("id").String() fname := tc.Get("function.name").String() fargs := tc.Get("function.arguments").String() - node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".functionCall.name", fname) + node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".functionCall.id", fid) + node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".functionCall.name", fname) node, _ = sjson.SetRawBytes(node, "parts."+itoa(p)+".functionCall.args", []byte(fargs)) node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".thoughtSignature", geminiCLIFunctionThoughtSignature) p++ @@ -264,12 +265,13 @@ func ConvertOpenAIRequestToGeminiCLI(modelName string, inputRawJSON []byte, _ bo pp := 0 for _, fid := range fIDs { if name, ok := tcID2Name[fid]; ok { - toolNode, _ = sjson.SetBytes(toolNode, "parts."+itoa(pp)+".functionResponse.name", name) + toolNode, _ = sjson.SetBytes(toolNode, "parts."+itoa(pp)+".functionResponse.id", fid) + toolNode, _ = sjson.SetBytes(toolNode, "parts."+itoa(pp)+".functionResponse.name", name) resp := toolResponses[fid] if resp == "" { resp = "{}" } - toolNode, _ = sjson.SetBytes(toolNode, "parts."+itoa(pp)+".functionResponse.response.result", []byte(resp)) + toolNode, _ = sjson.SetBytes(toolNode, "parts."+itoa(pp)+".functionResponse.response.output", []byte(resp)) pp++ } } diff --git a/internal/translator/gemini-cli/openai/chat-completions/gemini-cli_openai_response.go b/internal/translator/gemini-cli/openai/chat-completions/gemini-cli_openai_response.go index 5a1faf51..d429f7fe 100644 --- a/internal/translator/gemini-cli/openai/chat-completions/gemini-cli_openai_response.go +++ b/internal/translator/gemini-cli/openai/chat-completions/gemini-cli_openai_response.go @@ -149,7 +149,11 @@ func ConvertCliResponseToOpenAI(_ context.Context, _ string, originalRequestRawJ functionCallTemplate := `{"id": "","index": 0,"type": "function","function": {"name": "","arguments": ""}}` fcName := functionCallResult.Get("name").String() - functionCallTemplate, _ = sjson.Set(functionCallTemplate, "id", fmt.Sprintf("%s-%d-%d", fcName, time.Now().UnixNano(), atomic.AddUint64(&functionCallIDCounter, 1))) + fcID := functionCallResult.Get("id").String() + if fcID == "" { + fcID = fmt.Sprintf("%s-%d-%d", fcName, time.Now().UnixNano(), atomic.AddUint64(&functionCallIDCounter, 1)) + } + functionCallTemplate, _ = sjson.Set(functionCallTemplate, "id", fcID) functionCallTemplate, _ = sjson.Set(functionCallTemplate, "index", functionCallIndex) functionCallTemplate, _ = sjson.Set(functionCallTemplate, "function.name", fcName) if fcArgsResult := functionCallResult.Get("args"); fcArgsResult.Exists() { diff --git a/internal/translator/gemini/gemini-cli/gemini_gemini-cli_request.go b/internal/translator/gemini/gemini-cli/gemini_gemini-cli_request.go index 3b70bd3e..a917e807 100644 --- a/internal/translator/gemini/gemini-cli/gemini_gemini-cli_request.go +++ b/internal/translator/gemini/gemini-cli/gemini_gemini-cli_request.go @@ -46,19 +46,6 @@ func ConvertGeminiCLIRequestToGemini(_ string, inputRawJSON []byte, _ bool) []by } } - gjson.GetBytes(rawJSON, "contents").ForEach(func(key, content gjson.Result) bool { - if content.Get("role").String() == "model" { - content.Get("parts").ForEach(func(partKey, part gjson.Result) bool { - if part.Get("functionCall").Exists() { - rawJSON, _ = sjson.SetBytes(rawJSON, fmt.Sprintf("contents.%d.parts.%d.thoughtSignature", key.Int(), partKey.Int()), "skip_thought_signature_validator") - } else if part.Get("thoughtSignature").Exists() { - rawJSON, _ = sjson.SetBytes(rawJSON, fmt.Sprintf("contents.%d.parts.%d.thoughtSignature", key.Int(), partKey.Int()), "skip_thought_signature_validator") - } - return true - }) - } - return true - }) return common.AttachDefaultSafetySettings(rawJSON, "safetySettings") } diff --git a/internal/translator/gemini/openai/chat-completions/gemini_openai_request.go b/internal/translator/gemini/openai/chat-completions/gemini_openai_request.go index ba8b47e3..2a41e1a6 100644 --- a/internal/translator/gemini/openai/chat-completions/gemini_openai_request.go +++ b/internal/translator/gemini/openai/chat-completions/gemini_openai_request.go @@ -255,7 +255,8 @@ func ConvertOpenAIRequestToGemini(modelName string, inputRawJSON []byte, _ bool) fid := tc.Get("id").String() fname := tc.Get("function.name").String() fargs := tc.Get("function.arguments").String() - node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".functionCall.name", fname) + node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".functionCall.id", fid) + node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".functionCall.name", fname) node, _ = sjson.SetRawBytes(node, "parts."+itoa(p)+".functionCall.args", []byte(fargs)) node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".thoughtSignature", geminiFunctionThoughtSignature) p++ @@ -270,12 +271,13 @@ func ConvertOpenAIRequestToGemini(modelName string, inputRawJSON []byte, _ bool) pp := 0 for _, fid := range fIDs { if name, ok := tcID2Name[fid]; ok { - toolNode, _ = sjson.SetBytes(toolNode, "parts."+itoa(pp)+".functionResponse.name", name) + toolNode, _ = sjson.SetBytes(toolNode, "parts."+itoa(pp)+".functionResponse.id", fid) + toolNode, _ = sjson.SetBytes(toolNode, "parts."+itoa(pp)+".functionResponse.name", name) resp := toolResponses[fid] if resp == "" { resp = "{}" } - toolNode, _ = sjson.SetBytes(toolNode, "parts."+itoa(pp)+".functionResponse.response.result", []byte(resp)) + toolNode, _ = sjson.SetBytes(toolNode, "parts."+itoa(pp)+".functionResponse.response.output", []byte(resp)) pp++ } } diff --git a/internal/translator/gemini/openai/chat-completions/gemini_openai_response.go b/internal/translator/gemini/openai/chat-completions/gemini_openai_response.go index 9cce35f9..92156c7f 100644 --- a/internal/translator/gemini/openai/chat-completions/gemini_openai_response.go +++ b/internal/translator/gemini/openai/chat-completions/gemini_openai_response.go @@ -187,7 +187,11 @@ func ConvertGeminiResponseToOpenAI(_ context.Context, _ string, originalRequestR functionCallTemplate := `{"id": "","index": 0,"type": "function","function": {"name": "","arguments": ""}}` fcName := functionCallResult.Get("name").String() - functionCallTemplate, _ = sjson.Set(functionCallTemplate, "id", fmt.Sprintf("%s-%d-%d", fcName, time.Now().UnixNano(), atomic.AddUint64(&functionCallIDCounter, 1))) + fcID := functionCallResult.Get("id").String() + if fcID == "" { + fcID = fmt.Sprintf("%s-%d-%d", fcName, time.Now().UnixNano(), atomic.AddUint64(&functionCallIDCounter, 1)) + } + functionCallTemplate, _ = sjson.Set(functionCallTemplate, "id", fcID) functionCallTemplate, _ = sjson.Set(functionCallTemplate, "index", functionCallIndex) functionCallTemplate, _ = sjson.Set(functionCallTemplate, "function.name", fcName) if fcArgsResult := functionCallResult.Get("args"); fcArgsResult.Exists() { diff --git a/internal/translator/gemini/openai/responses/gemini_openai-responses_request.go b/internal/translator/gemini/openai/responses/gemini_openai-responses_request.go index 5277b71b..adeba903 100644 --- a/internal/translator/gemini/openai/responses/gemini_openai-responses_request.go +++ b/internal/translator/gemini/openai/responses/gemini_openai-responses_request.go @@ -290,11 +290,11 @@ func ConvertOpenAIResponsesRequestToGemini(modelName string, inputRawJSON []byte // Set the raw JSON output directly (preserves string encoding) if outputRaw != "" && outputRaw != "null" { output := gjson.Parse(outputRaw) - if output.Type == gjson.JSON { - functionResponse, _ = sjson.SetRaw(functionResponse, "functionResponse.response.result", output.Raw) - } else { - functionResponse, _ = sjson.Set(functionResponse, "functionResponse.response.result", outputRaw) - } + if output.Type == gjson.JSON { + functionResponse, _ = sjson.SetRaw(functionResponse, "functionResponse.response.output", output.Raw) + } else { + functionResponse, _ = sjson.Set(functionResponse, "functionResponse.response.output", outputRaw) + } } functionContent, _ = sjson.SetRaw(functionContent, "parts.-1", functionResponse) out, _ = sjson.SetRaw(out, "contents.-1", functionContent) diff --git a/internal/util/gemini_schema.go b/internal/util/gemini_schema.go index c7cb0f40..c22e98c9 100644 --- a/internal/util/gemini_schema.go +++ b/internal/util/gemini_schema.go @@ -16,6 +16,93 @@ var gjsonPathKeyReplacer = strings.NewReplacer(".", "\\.", "*", "\\*", "?", "\\? // It handles unsupported keywords, type flattening, and schema simplification while preserving // semantic information as description hints. func CleanJSONSchemaForAntigravity(jsonStr string) string { + return cleanJSONSchema(jsonStr, true) +} + +func removeKeywords(jsonStr string, keywords []string) string { + for _, key := range keywords { + for _, p := range findPaths(jsonStr, key) { + if isPropertyDefinition(trimSuffix(p, "."+key)) { + continue + } + jsonStr, _ = sjson.Delete(jsonStr, p) + } + } + return jsonStr +} + +// removePlaceholderFields removes placeholder-only properties ("_" and "reason") and their required entries. +func removePlaceholderFields(jsonStr string) string { + // Remove "_" placeholder properties. + paths := findPaths(jsonStr, "_") + sortByDepth(paths) + for _, p := range paths { + if !strings.HasSuffix(p, ".properties._") { + continue + } + jsonStr, _ = sjson.Delete(jsonStr, p) + parentPath := trimSuffix(p, ".properties._") + reqPath := joinPath(parentPath, "required") + req := gjson.Get(jsonStr, reqPath) + if req.IsArray() { + var filtered []string + for _, r := range req.Array() { + if r.String() != "_" { + filtered = append(filtered, r.String()) + } + } + if len(filtered) == 0 { + jsonStr, _ = sjson.Delete(jsonStr, reqPath) + } else { + jsonStr, _ = sjson.Set(jsonStr, reqPath, filtered) + } + } + } + + // Remove placeholder-only "reason" objects. + reasonPaths := findPaths(jsonStr, "reason") + sortByDepth(reasonPaths) + for _, p := range reasonPaths { + if !strings.HasSuffix(p, ".properties.reason") { + continue + } + parentPath := trimSuffix(p, ".properties.reason") + props := gjson.Get(jsonStr, joinPath(parentPath, "properties")) + if !props.IsObject() || len(props.Map()) != 1 { + continue + } + desc := gjson.Get(jsonStr, p+".description").String() + if desc != "Brief explanation of why you are calling this tool" { + continue + } + jsonStr, _ = sjson.Delete(jsonStr, p) + reqPath := joinPath(parentPath, "required") + req := gjson.Get(jsonStr, reqPath) + if req.IsArray() { + var filtered []string + for _, r := range req.Array() { + if r.String() != "reason" { + filtered = append(filtered, r.String()) + } + } + if len(filtered) == 0 { + jsonStr, _ = sjson.Delete(jsonStr, reqPath) + } else { + jsonStr, _ = sjson.Set(jsonStr, reqPath, filtered) + } + } + } + + return jsonStr +} + +// CleanJSONSchemaForGemini transforms a JSON schema to be compatible with Gemini tool calling. +// It removes unsupported keywords and simplifies schemas, without adding empty-schema placeholders. +func CleanJSONSchemaForGemini(jsonStr string) string { + return cleanJSONSchema(jsonStr, false) +} + +func cleanJSONSchema(jsonStr string, addPlaceholder bool) string { // Phase 1: Convert and add hints jsonStr = convertRefsToHints(jsonStr) jsonStr = convertConstToEnum(jsonStr) @@ -31,10 +118,16 @@ func CleanJSONSchemaForAntigravity(jsonStr string) string { // Phase 3: Cleanup jsonStr = removeUnsupportedKeywords(jsonStr) + if !addPlaceholder { + // Gemini schema cleanup: remove nullable/title and placeholder-only fields. + jsonStr = removeKeywords(jsonStr, []string{"nullable", "title"}) + jsonStr = removePlaceholderFields(jsonStr) + } jsonStr = cleanupRequiredFields(jsonStr) - // Phase 4: Add placeholder for empty object schemas (Claude VALIDATED mode requirement) - jsonStr = addEmptySchemaPlaceholder(jsonStr) + if addPlaceholder { + jsonStr = addEmptySchemaPlaceholder(jsonStr) + } return jsonStr } From 22ce65ac72c65c10ec1d5a6eb5a9635cd02e19e1 Mon Sep 17 00:00:00 2001 From: sowar1987 <178468696@qq.com> Date: Wed, 21 Jan 2026 14:23:00 +0800 Subject: [PATCH 2/4] test: update signature cache tests Revert gemini translator changes for scheme A Co-Authored-By: Warp --- internal/cache/signature_cache_test.go | 108 +++++++++--------- .../gemini-cli_openai_request.go | 8 +- .../gemini-cli_openai_response.go | 6 +- .../gemini-cli/gemini_gemini-cli_request.go | 13 +++ .../chat-completions/gemini_openai_request.go | 8 +- .../gemini_openai_response.go | 6 +- .../gemini_openai-responses_request.go | 10 +- 7 files changed, 81 insertions(+), 78 deletions(-) diff --git a/internal/cache/signature_cache_test.go b/internal/cache/signature_cache_test.go index 368d3195..7b413473 100644 --- a/internal/cache/signature_cache_test.go +++ b/internal/cache/signature_cache_test.go @@ -4,18 +4,20 @@ import ( "testing" "time" ) +const testModelName = "claude-sonnet-4-5" func TestCacheSignature_BasicStorageAndRetrieval(t *testing.T) { ClearSignatureCache("") + sessionID := "test-session-1" text := "This is some thinking text content" signature := "abc123validSignature1234567890123456789012345678901234567890" // Store signature - CacheSignature("test-model", text, signature) + CacheSignature(testModelName, sessionID, text, signature) // Retrieve signature - retrieved := GetCachedSignature("test-model", text) + retrieved := GetCachedSignature(testModelName, sessionID, text) if retrieved != signature { t.Errorf("Expected signature '%s', got '%s'", signature, retrieved) } @@ -27,29 +29,28 @@ func TestCacheSignature_DifferentModelGroups(t *testing.T) { text := "Same text across models" sig1 := "signature1_1234567890123456789012345678901234567890123456" sig2 := "signature2_1234567890123456789012345678901234567890123456" + CacheSignature(testModelName, "session-a", text, sig1) + CacheSignature(testModelName, "session-b", text, sig2) - CacheSignature("claude-sonnet-4-5-thinking", text, sig1) - CacheSignature("gpt-4o", text, sig2) - - if GetCachedSignature("claude-sonnet-4-5-thinking", text) != sig1 { - t.Error("Claude signature mismatch") + if GetCachedSignature(testModelName, "session-a", text) != sig1 { + t.Error("Session-a signature mismatch") } - if GetCachedSignature("gpt-4o", text) != sig2 { - t.Error("GPT signature mismatch") + if GetCachedSignature(testModelName, "session-b", text) != sig2 { + t.Error("Session-b signature mismatch") } } func TestCacheSignature_NotFound(t *testing.T) { ClearSignatureCache("") - // Non-existent cache entry - if got := GetCachedSignature("test-model", "some text"); got != "" { - t.Errorf("Expected empty string for missing entry, got '%s'", got) + // Non-existent session + if got := GetCachedSignature(testModelName, "nonexistent", "some text"); got != "" { + t.Errorf("Expected empty string for nonexistent session, got '%s'", got) } - // Existing cache but different text - CacheSignature("test-model", "text-a", "sigA12345678901234567890123456789012345678901234567890") - if got := GetCachedSignature("test-model", "text-b"); got != "" { + // Existing session but different text + CacheSignature(testModelName, "session-x", "text-a", "sigA12345678901234567890123456789012345678901234567890") + if got := GetCachedSignature(testModelName, "session-x", "text-b"); got != "" { t.Errorf("Expected empty string for different text, got '%s'", got) } } @@ -58,11 +59,12 @@ func TestCacheSignature_EmptyInputs(t *testing.T) { ClearSignatureCache("") // All empty/invalid inputs should be no-ops - CacheSignature("test-model", "", "sig12345678901234567890123456789012345678901234567890") - CacheSignature("test-model", "text", "") - CacheSignature("test-model", "text", "short") // Too short + CacheSignature(testModelName, "", "text", "sig12345678901234567890123456789012345678901234567890") + CacheSignature(testModelName, "session", "", "sig12345678901234567890123456789012345678901234567890") + CacheSignature(testModelName, "session", "text", "") + CacheSignature(testModelName, "session", "text", "short") // Too short - if got := GetCachedSignature("test-model", "text"); got != "" { + if got := GetCachedSignature(testModelName, "session", "text"); got != "" { t.Errorf("Expected empty after invalid cache attempts, got '%s'", got) } } @@ -70,12 +72,12 @@ func TestCacheSignature_EmptyInputs(t *testing.T) { func TestCacheSignature_ShortSignatureRejected(t *testing.T) { ClearSignatureCache("") + sessionID := "test-short-sig" text := "Some text" shortSig := "abc123" // Less than 50 chars + CacheSignature(testModelName, sessionID, text, shortSig) - CacheSignature("test-model", text, shortSig) - - if got := GetCachedSignature("test-model", text); got != "" { + if got := GetCachedSignature(testModelName, sessionID, text); got != "" { t.Errorf("Short signature should be rejected, got '%s'", got) } } @@ -83,18 +85,17 @@ func TestCacheSignature_ShortSignatureRejected(t *testing.T) { func TestClearSignatureCache_ModelGroup(t *testing.T) { ClearSignatureCache("") - sigClaude := "validSig1234567890123456789012345678901234567890123456" - sigGpt := "validSig9876543210987654321098765432109876543210987654" - CacheSignature("claude-sonnet-4-5-thinking", "text", sigClaude) - CacheSignature("gpt-4o", "text", sigGpt) + sig := "validSig1234567890123456789012345678901234567890123456" + CacheSignature(testModelName, "session-1", "text", sig) + CacheSignature(testModelName, "session-2", "text", sig) - ClearSignatureCache("claude-sonnet-4-5-thinking") + ClearSignatureCache(GetModelGroup(testModelName) + "#session-1") - if got := GetCachedSignature("claude-sonnet-4-5-thinking", "text"); got != "" { - t.Error("Claude cache should be cleared") + if got := GetCachedSignature(testModelName, "session-1", "text"); got != "" { + t.Error("session-1 should be cleared") } - if got := GetCachedSignature("gpt-4o", "text"); got != sigGpt { - t.Error("GPT cache should still exist") + if got := GetCachedSignature(testModelName, "session-2", "text"); got != sig { + t.Error("session-2 should still exist") } } @@ -102,16 +103,16 @@ func TestClearSignatureCache_AllSessions(t *testing.T) { ClearSignatureCache("") sig := "validSig1234567890123456789012345678901234567890123456" - CacheSignature("test-model", "text", sig) - CacheSignature("test-model", "text", sig) + CacheSignature(testModelName, "session-1", "text", sig) + CacheSignature(testModelName, "session-2", "text", sig) ClearSignatureCache("") - if got := GetCachedSignature("test-model", "text"); got != "" { - t.Error("cache should be cleared") + if got := GetCachedSignature(testModelName, "session-1", "text"); got != "" { + t.Error("session-1 should be cleared") } - if got := GetCachedSignature("test-model", "text"); got != "" { - t.Error("cache should be cleared") + if got := GetCachedSignature(testModelName, "session-2", "text"); got != "" { + t.Error("session-2 should be cleared") } } @@ -130,7 +131,7 @@ func TestHasValidSignature(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - result := HasValidSignature("claude-sonnet-4-5-thinking", tt.signature) + result := HasValidSignature(tt.signature) if result != tt.expected { t.Errorf("HasValidSignature(%q) = %v, expected %v", tt.signature, result, tt.expected) } @@ -141,19 +142,20 @@ func TestHasValidSignature(t *testing.T) { func TestCacheSignature_TextHashCollisionResistance(t *testing.T) { ClearSignatureCache("") + sessionID := "hash-test-session" + // Different texts should produce different hashes text1 := "First thinking text" text2 := "Second thinking text" sig1 := "signature1_1234567890123456789012345678901234567890123456" sig2 := "signature2_1234567890123456789012345678901234567890123456" + CacheSignature(testModelName, sessionID, text1, sig1) + CacheSignature(testModelName, sessionID, text2, sig2) - CacheSignature("test-model", text1, sig1) - CacheSignature("test-model", text2, sig2) - - if GetCachedSignature("test-model", text1) != sig1 { + if GetCachedSignature(testModelName, sessionID, text1) != sig1 { t.Error("text1 signature mismatch") } - if GetCachedSignature("test-model", text2) != sig2 { + if GetCachedSignature(testModelName, sessionID, text2) != sig2 { t.Error("text2 signature mismatch") } } @@ -161,12 +163,12 @@ func TestCacheSignature_TextHashCollisionResistance(t *testing.T) { func TestCacheSignature_UnicodeText(t *testing.T) { ClearSignatureCache("") + sessionID := "unicode-session" text := "한글 텍스트와 이모지 🎉 그리고 特殊文字" sig := "unicodeSig123456789012345678901234567890123456789012345" + CacheSignature(testModelName, sessionID, text, sig) - CacheSignature("test-model", text, sig) - - if got := GetCachedSignature("test-model", text); got != sig { + if got := GetCachedSignature(testModelName, sessionID, text); got != sig { t.Errorf("Unicode text signature retrieval failed, got '%s'", got) } } @@ -174,14 +176,14 @@ func TestCacheSignature_UnicodeText(t *testing.T) { func TestCacheSignature_Overwrite(t *testing.T) { ClearSignatureCache("") + sessionID := "overwrite-session" text := "Same text" sig1 := "firstSignature12345678901234567890123456789012345678901" sig2 := "secondSignature1234567890123456789012345678901234567890" + CacheSignature(testModelName, sessionID, text, sig1) + CacheSignature(testModelName, sessionID, text, sig2) // Overwrite - CacheSignature("test-model", text, sig1) - CacheSignature("test-model", text, sig2) // Overwrite - - if got := GetCachedSignature("test-model", text); got != sig2 { + if got := GetCachedSignature(testModelName, sessionID, text); got != sig2 { t.Errorf("Expected overwritten signature '%s', got '%s'", sig2, got) } } @@ -193,13 +195,13 @@ func TestCacheSignature_ExpirationLogic(t *testing.T) { // This test verifies the expiration check exists // In a real scenario, we'd mock time.Now() + sessionID := "expiration-test" text := "text" sig := "validSig1234567890123456789012345678901234567890123456" - - CacheSignature("test-model", text, sig) + CacheSignature(testModelName, sessionID, text, sig) // Fresh entry should be retrievable - if got := GetCachedSignature("test-model", text); got != sig { + if got := GetCachedSignature(testModelName, sessionID, text); got != sig { t.Errorf("Fresh entry should be retrievable, got '%s'", got) } diff --git a/internal/translator/gemini-cli/openai/chat-completions/gemini-cli_openai_request.go b/internal/translator/gemini-cli/openai/chat-completions/gemini-cli_openai_request.go index cf3cbaa4..85669689 100644 --- a/internal/translator/gemini-cli/openai/chat-completions/gemini-cli_openai_request.go +++ b/internal/translator/gemini-cli/openai/chat-completions/gemini-cli_openai_request.go @@ -249,8 +249,7 @@ func ConvertOpenAIRequestToGeminiCLI(modelName string, inputRawJSON []byte, _ bo fid := tc.Get("id").String() fname := tc.Get("function.name").String() fargs := tc.Get("function.arguments").String() - node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".functionCall.id", fid) - node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".functionCall.name", fname) + node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".functionCall.name", fname) node, _ = sjson.SetRawBytes(node, "parts."+itoa(p)+".functionCall.args", []byte(fargs)) node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".thoughtSignature", geminiCLIFunctionThoughtSignature) p++ @@ -265,13 +264,12 @@ func ConvertOpenAIRequestToGeminiCLI(modelName string, inputRawJSON []byte, _ bo pp := 0 for _, fid := range fIDs { if name, ok := tcID2Name[fid]; ok { - toolNode, _ = sjson.SetBytes(toolNode, "parts."+itoa(pp)+".functionResponse.id", fid) - toolNode, _ = sjson.SetBytes(toolNode, "parts."+itoa(pp)+".functionResponse.name", name) + toolNode, _ = sjson.SetBytes(toolNode, "parts."+itoa(pp)+".functionResponse.name", name) resp := toolResponses[fid] if resp == "" { resp = "{}" } - toolNode, _ = sjson.SetBytes(toolNode, "parts."+itoa(pp)+".functionResponse.response.output", []byte(resp)) + toolNode, _ = sjson.SetBytes(toolNode, "parts."+itoa(pp)+".functionResponse.response.result", []byte(resp)) pp++ } } diff --git a/internal/translator/gemini-cli/openai/chat-completions/gemini-cli_openai_response.go b/internal/translator/gemini-cli/openai/chat-completions/gemini-cli_openai_response.go index d429f7fe..5a1faf51 100644 --- a/internal/translator/gemini-cli/openai/chat-completions/gemini-cli_openai_response.go +++ b/internal/translator/gemini-cli/openai/chat-completions/gemini-cli_openai_response.go @@ -149,11 +149,7 @@ func ConvertCliResponseToOpenAI(_ context.Context, _ string, originalRequestRawJ functionCallTemplate := `{"id": "","index": 0,"type": "function","function": {"name": "","arguments": ""}}` fcName := functionCallResult.Get("name").String() - fcID := functionCallResult.Get("id").String() - if fcID == "" { - fcID = fmt.Sprintf("%s-%d-%d", fcName, time.Now().UnixNano(), atomic.AddUint64(&functionCallIDCounter, 1)) - } - functionCallTemplate, _ = sjson.Set(functionCallTemplate, "id", fcID) + functionCallTemplate, _ = sjson.Set(functionCallTemplate, "id", fmt.Sprintf("%s-%d-%d", fcName, time.Now().UnixNano(), atomic.AddUint64(&functionCallIDCounter, 1))) functionCallTemplate, _ = sjson.Set(functionCallTemplate, "index", functionCallIndex) functionCallTemplate, _ = sjson.Set(functionCallTemplate, "function.name", fcName) if fcArgsResult := functionCallResult.Get("args"); fcArgsResult.Exists() { diff --git a/internal/translator/gemini/gemini-cli/gemini_gemini-cli_request.go b/internal/translator/gemini/gemini-cli/gemini_gemini-cli_request.go index a917e807..3b70bd3e 100644 --- a/internal/translator/gemini/gemini-cli/gemini_gemini-cli_request.go +++ b/internal/translator/gemini/gemini-cli/gemini_gemini-cli_request.go @@ -46,6 +46,19 @@ func ConvertGeminiCLIRequestToGemini(_ string, inputRawJSON []byte, _ bool) []by } } + gjson.GetBytes(rawJSON, "contents").ForEach(func(key, content gjson.Result) bool { + if content.Get("role").String() == "model" { + content.Get("parts").ForEach(func(partKey, part gjson.Result) bool { + if part.Get("functionCall").Exists() { + rawJSON, _ = sjson.SetBytes(rawJSON, fmt.Sprintf("contents.%d.parts.%d.thoughtSignature", key.Int(), partKey.Int()), "skip_thought_signature_validator") + } else if part.Get("thoughtSignature").Exists() { + rawJSON, _ = sjson.SetBytes(rawJSON, fmt.Sprintf("contents.%d.parts.%d.thoughtSignature", key.Int(), partKey.Int()), "skip_thought_signature_validator") + } + return true + }) + } + return true + }) return common.AttachDefaultSafetySettings(rawJSON, "safetySettings") } diff --git a/internal/translator/gemini/openai/chat-completions/gemini_openai_request.go b/internal/translator/gemini/openai/chat-completions/gemini_openai_request.go index 2a41e1a6..ba8b47e3 100644 --- a/internal/translator/gemini/openai/chat-completions/gemini_openai_request.go +++ b/internal/translator/gemini/openai/chat-completions/gemini_openai_request.go @@ -255,8 +255,7 @@ func ConvertOpenAIRequestToGemini(modelName string, inputRawJSON []byte, _ bool) fid := tc.Get("id").String() fname := tc.Get("function.name").String() fargs := tc.Get("function.arguments").String() - node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".functionCall.id", fid) - node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".functionCall.name", fname) + node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".functionCall.name", fname) node, _ = sjson.SetRawBytes(node, "parts."+itoa(p)+".functionCall.args", []byte(fargs)) node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".thoughtSignature", geminiFunctionThoughtSignature) p++ @@ -271,13 +270,12 @@ func ConvertOpenAIRequestToGemini(modelName string, inputRawJSON []byte, _ bool) pp := 0 for _, fid := range fIDs { if name, ok := tcID2Name[fid]; ok { - toolNode, _ = sjson.SetBytes(toolNode, "parts."+itoa(pp)+".functionResponse.id", fid) - toolNode, _ = sjson.SetBytes(toolNode, "parts."+itoa(pp)+".functionResponse.name", name) + toolNode, _ = sjson.SetBytes(toolNode, "parts."+itoa(pp)+".functionResponse.name", name) resp := toolResponses[fid] if resp == "" { resp = "{}" } - toolNode, _ = sjson.SetBytes(toolNode, "parts."+itoa(pp)+".functionResponse.response.output", []byte(resp)) + toolNode, _ = sjson.SetBytes(toolNode, "parts."+itoa(pp)+".functionResponse.response.result", []byte(resp)) pp++ } } diff --git a/internal/translator/gemini/openai/chat-completions/gemini_openai_response.go b/internal/translator/gemini/openai/chat-completions/gemini_openai_response.go index 92156c7f..9cce35f9 100644 --- a/internal/translator/gemini/openai/chat-completions/gemini_openai_response.go +++ b/internal/translator/gemini/openai/chat-completions/gemini_openai_response.go @@ -187,11 +187,7 @@ func ConvertGeminiResponseToOpenAI(_ context.Context, _ string, originalRequestR functionCallTemplate := `{"id": "","index": 0,"type": "function","function": {"name": "","arguments": ""}}` fcName := functionCallResult.Get("name").String() - fcID := functionCallResult.Get("id").String() - if fcID == "" { - fcID = fmt.Sprintf("%s-%d-%d", fcName, time.Now().UnixNano(), atomic.AddUint64(&functionCallIDCounter, 1)) - } - functionCallTemplate, _ = sjson.Set(functionCallTemplate, "id", fcID) + functionCallTemplate, _ = sjson.Set(functionCallTemplate, "id", fmt.Sprintf("%s-%d-%d", fcName, time.Now().UnixNano(), atomic.AddUint64(&functionCallIDCounter, 1))) functionCallTemplate, _ = sjson.Set(functionCallTemplate, "index", functionCallIndex) functionCallTemplate, _ = sjson.Set(functionCallTemplate, "function.name", fcName) if fcArgsResult := functionCallResult.Get("args"); fcArgsResult.Exists() { diff --git a/internal/translator/gemini/openai/responses/gemini_openai-responses_request.go b/internal/translator/gemini/openai/responses/gemini_openai-responses_request.go index adeba903..5277b71b 100644 --- a/internal/translator/gemini/openai/responses/gemini_openai-responses_request.go +++ b/internal/translator/gemini/openai/responses/gemini_openai-responses_request.go @@ -290,11 +290,11 @@ func ConvertOpenAIResponsesRequestToGemini(modelName string, inputRawJSON []byte // Set the raw JSON output directly (preserves string encoding) if outputRaw != "" && outputRaw != "null" { output := gjson.Parse(outputRaw) - if output.Type == gjson.JSON { - functionResponse, _ = sjson.SetRaw(functionResponse, "functionResponse.response.output", output.Raw) - } else { - functionResponse, _ = sjson.Set(functionResponse, "functionResponse.response.output", outputRaw) - } + if output.Type == gjson.JSON { + functionResponse, _ = sjson.SetRaw(functionResponse, "functionResponse.response.result", output.Raw) + } else { + functionResponse, _ = sjson.Set(functionResponse, "functionResponse.response.result", outputRaw) + } } functionContent, _ = sjson.SetRaw(functionContent, "parts.-1", functionResponse) out, _ = sjson.SetRaw(out, "contents.-1", functionContent) From 269a1c5452a88a1f9d4117a48f7e7240ac845ced Mon Sep 17 00:00:00 2001 From: sowar1987 <178468696@qq.com> Date: Wed, 21 Jan 2026 14:59:30 +0800 Subject: [PATCH 3/4] refactor: reuse placeholder reason description Co-Authored-By: Warp --- internal/util/gemini_schema.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/internal/util/gemini_schema.go b/internal/util/gemini_schema.go index c22e98c9..ddcee040 100644 --- a/internal/util/gemini_schema.go +++ b/internal/util/gemini_schema.go @@ -12,6 +12,8 @@ import ( var gjsonPathKeyReplacer = strings.NewReplacer(".", "\\.", "*", "\\*", "?", "\\?") +const placeholderReasonDescription = "Brief explanation of why you are calling this tool" + // CleanJSONSchemaForAntigravity transforms a JSON schema to be compatible with Antigravity API. // It handles unsupported keywords, type flattening, and schema simplification while preserving // semantic information as description hints. @@ -72,7 +74,7 @@ func removePlaceholderFields(jsonStr string) string { continue } desc := gjson.Get(jsonStr, p+".description").String() - if desc != "Brief explanation of why you are calling this tool" { + if desc != placeholderReasonDescription { continue } jsonStr, _ = sjson.Delete(jsonStr, p) @@ -502,7 +504,7 @@ func addEmptySchemaPlaceholder(jsonStr string) string { // Add placeholder "reason" property reasonPath := joinPath(propsPath, "reason") jsonStr, _ = sjson.Set(jsonStr, reasonPath+".type", "string") - jsonStr, _ = sjson.Set(jsonStr, reasonPath+".description", "Brief explanation of why you are calling this tool") + jsonStr, _ = sjson.Set(jsonStr, reasonPath+".description", placeholderReasonDescription) // Add to required array jsonStr, _ = sjson.Set(jsonStr, reqPath, []string{"reason"}) From 9c2992bfb2988c422bb753f71cb9e49acc4aea44 Mon Sep 17 00:00:00 2001 From: sowar1987 <178468696@qq.com> Date: Wed, 21 Jan 2026 15:55:07 +0800 Subject: [PATCH 4/4] test: align signature cache tests with cache behavior Co-Authored-By: Warp --- internal/cache/signature_cache_test.go | 111 ++++++++++++------------- 1 file changed, 55 insertions(+), 56 deletions(-) diff --git a/internal/cache/signature_cache_test.go b/internal/cache/signature_cache_test.go index 7b413473..83408159 100644 --- a/internal/cache/signature_cache_test.go +++ b/internal/cache/signature_cache_test.go @@ -4,20 +4,20 @@ import ( "testing" "time" ) + const testModelName = "claude-sonnet-4-5" func TestCacheSignature_BasicStorageAndRetrieval(t *testing.T) { ClearSignatureCache("") - sessionID := "test-session-1" text := "This is some thinking text content" signature := "abc123validSignature1234567890123456789012345678901234567890" // Store signature - CacheSignature(testModelName, sessionID, text, signature) + CacheSignature(testModelName, text, signature) // Retrieve signature - retrieved := GetCachedSignature(testModelName, sessionID, text) + retrieved := GetCachedSignature(testModelName, text) if retrieved != signature { t.Errorf("Expected signature '%s', got '%s'", signature, retrieved) } @@ -29,14 +29,16 @@ func TestCacheSignature_DifferentModelGroups(t *testing.T) { text := "Same text across models" sig1 := "signature1_1234567890123456789012345678901234567890123456" sig2 := "signature2_1234567890123456789012345678901234567890123456" - CacheSignature(testModelName, "session-a", text, sig1) - CacheSignature(testModelName, "session-b", text, sig2) - if GetCachedSignature(testModelName, "session-a", text) != sig1 { - t.Error("Session-a signature mismatch") + geminiModel := "gemini-3-pro-preview" + CacheSignature(testModelName, text, sig1) + CacheSignature(geminiModel, text, sig2) + + if GetCachedSignature(testModelName, text) != sig1 { + t.Error("Claude signature mismatch") } - if GetCachedSignature(testModelName, "session-b", text) != sig2 { - t.Error("Session-b signature mismatch") + if GetCachedSignature(geminiModel, text) != sig2 { + t.Error("Gemini signature mismatch") } } @@ -44,13 +46,13 @@ func TestCacheSignature_NotFound(t *testing.T) { ClearSignatureCache("") // Non-existent session - if got := GetCachedSignature(testModelName, "nonexistent", "some text"); got != "" { + if got := GetCachedSignature(testModelName, "some text"); got != "" { t.Errorf("Expected empty string for nonexistent session, got '%s'", got) } // Existing session but different text - CacheSignature(testModelName, "session-x", "text-a", "sigA12345678901234567890123456789012345678901234567890") - if got := GetCachedSignature(testModelName, "session-x", "text-b"); got != "" { + CacheSignature(testModelName, "text-a", "sigA12345678901234567890123456789012345678901234567890") + if got := GetCachedSignature(testModelName, "text-b"); got != "" { t.Errorf("Expected empty string for different text, got '%s'", got) } } @@ -59,12 +61,11 @@ func TestCacheSignature_EmptyInputs(t *testing.T) { ClearSignatureCache("") // All empty/invalid inputs should be no-ops - CacheSignature(testModelName, "", "text", "sig12345678901234567890123456789012345678901234567890") - CacheSignature(testModelName, "session", "", "sig12345678901234567890123456789012345678901234567890") - CacheSignature(testModelName, "session", "text", "") - CacheSignature(testModelName, "session", "text", "short") // Too short + CacheSignature(testModelName, "", "sig12345678901234567890123456789012345678901234567890") + CacheSignature(testModelName, "text", "") + CacheSignature(testModelName, "text", "short") // Too short - if got := GetCachedSignature(testModelName, "session", "text"); got != "" { + if got := GetCachedSignature(testModelName, "text"); got != "" { t.Errorf("Expected empty after invalid cache attempts, got '%s'", got) } } @@ -72,12 +73,12 @@ func TestCacheSignature_EmptyInputs(t *testing.T) { func TestCacheSignature_ShortSignatureRejected(t *testing.T) { ClearSignatureCache("") - sessionID := "test-short-sig" text := "Some text" shortSig := "abc123" // Less than 50 chars - CacheSignature(testModelName, sessionID, text, shortSig) - if got := GetCachedSignature(testModelName, sessionID, text); got != "" { + CacheSignature(testModelName, text, shortSig) + + if got := GetCachedSignature(testModelName, text); got != "" { t.Errorf("Short signature should be rejected, got '%s'", got) } } @@ -86,16 +87,13 @@ func TestClearSignatureCache_ModelGroup(t *testing.T) { ClearSignatureCache("") sig := "validSig1234567890123456789012345678901234567890123456" - CacheSignature(testModelName, "session-1", "text", sig) - CacheSignature(testModelName, "session-2", "text", sig) + CacheSignature(testModelName, "text", sig) + CacheSignature(testModelName, "text-2", sig) - ClearSignatureCache(GetModelGroup(testModelName) + "#session-1") + ClearSignatureCache("session-1") - if got := GetCachedSignature(testModelName, "session-1", "text"); got != "" { - t.Error("session-1 should be cleared") - } - if got := GetCachedSignature(testModelName, "session-2", "text"); got != sig { - t.Error("session-2 should still exist") + if got := GetCachedSignature(testModelName, "text"); got != sig { + t.Error("signature should remain when clearing unknown session") } } @@ -103,35 +101,37 @@ func TestClearSignatureCache_AllSessions(t *testing.T) { ClearSignatureCache("") sig := "validSig1234567890123456789012345678901234567890123456" - CacheSignature(testModelName, "session-1", "text", sig) - CacheSignature(testModelName, "session-2", "text", sig) + CacheSignature(testModelName, "text", sig) + CacheSignature(testModelName, "text-2", sig) ClearSignatureCache("") - if got := GetCachedSignature(testModelName, "session-1", "text"); got != "" { - t.Error("session-1 should be cleared") + if got := GetCachedSignature(testModelName, "text"); got != "" { + t.Error("text should be cleared") } - if got := GetCachedSignature(testModelName, "session-2", "text"); got != "" { - t.Error("session-2 should be cleared") + if got := GetCachedSignature(testModelName, "text-2"); got != "" { + t.Error("text-2 should be cleared") } } func TestHasValidSignature(t *testing.T) { tests := []struct { name string + modelName string signature string expected bool }{ - {"valid long signature", "abc123validSignature1234567890123456789012345678901234567890", true}, - {"exactly 50 chars", "12345678901234567890123456789012345678901234567890", true}, - {"49 chars - invalid", "1234567890123456789012345678901234567890123456789", false}, - {"empty string", "", false}, - {"short signature", "abc", false}, + {"valid long signature", testModelName, "abc123validSignature1234567890123456789012345678901234567890", true}, + {"exactly 50 chars", testModelName, "12345678901234567890123456789012345678901234567890", true}, + {"49 chars - invalid", testModelName, "1234567890123456789012345678901234567890123456789", false}, + {"empty string", testModelName, "", false}, + {"short signature", testModelName, "abc", false}, + {"gemini sentinel", "gemini-3-pro-preview", "skip_thought_signature_validator", true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - result := HasValidSignature(tt.signature) + result := HasValidSignature(tt.modelName, tt.signature) if result != tt.expected { t.Errorf("HasValidSignature(%q) = %v, expected %v", tt.signature, result, tt.expected) } @@ -142,20 +142,19 @@ func TestHasValidSignature(t *testing.T) { func TestCacheSignature_TextHashCollisionResistance(t *testing.T) { ClearSignatureCache("") - sessionID := "hash-test-session" - // Different texts should produce different hashes text1 := "First thinking text" text2 := "Second thinking text" sig1 := "signature1_1234567890123456789012345678901234567890123456" sig2 := "signature2_1234567890123456789012345678901234567890123456" - CacheSignature(testModelName, sessionID, text1, sig1) - CacheSignature(testModelName, sessionID, text2, sig2) - if GetCachedSignature(testModelName, sessionID, text1) != sig1 { + CacheSignature(testModelName, text1, sig1) + CacheSignature(testModelName, text2, sig2) + + if GetCachedSignature(testModelName, text1) != sig1 { t.Error("text1 signature mismatch") } - if GetCachedSignature(testModelName, sessionID, text2) != sig2 { + if GetCachedSignature(testModelName, text2) != sig2 { t.Error("text2 signature mismatch") } } @@ -163,12 +162,12 @@ func TestCacheSignature_TextHashCollisionResistance(t *testing.T) { func TestCacheSignature_UnicodeText(t *testing.T) { ClearSignatureCache("") - sessionID := "unicode-session" text := "한글 텍스트와 이모지 🎉 그리고 特殊文字" sig := "unicodeSig123456789012345678901234567890123456789012345" - CacheSignature(testModelName, sessionID, text, sig) - if got := GetCachedSignature(testModelName, sessionID, text); got != sig { + CacheSignature(testModelName, text, sig) + + if got := GetCachedSignature(testModelName, text); got != sig { t.Errorf("Unicode text signature retrieval failed, got '%s'", got) } } @@ -176,14 +175,14 @@ func TestCacheSignature_UnicodeText(t *testing.T) { func TestCacheSignature_Overwrite(t *testing.T) { ClearSignatureCache("") - sessionID := "overwrite-session" text := "Same text" sig1 := "firstSignature12345678901234567890123456789012345678901" sig2 := "secondSignature1234567890123456789012345678901234567890" - CacheSignature(testModelName, sessionID, text, sig1) - CacheSignature(testModelName, sessionID, text, sig2) // Overwrite - if got := GetCachedSignature(testModelName, sessionID, text); got != sig2 { + CacheSignature(testModelName, text, sig1) + CacheSignature(testModelName, text, sig2) // Overwrite + + if got := GetCachedSignature(testModelName, text); got != sig2 { t.Errorf("Expected overwritten signature '%s', got '%s'", sig2, got) } } @@ -195,13 +194,13 @@ func TestCacheSignature_ExpirationLogic(t *testing.T) { // This test verifies the expiration check exists // In a real scenario, we'd mock time.Now() - sessionID := "expiration-test" text := "text" sig := "validSig1234567890123456789012345678901234567890123456" - CacheSignature(testModelName, sessionID, text, sig) + + CacheSignature(testModelName, text, sig) // Fresh entry should be retrievable - if got := GetCachedSignature(testModelName, sessionID, text); got != sig { + if got := GetCachedSignature(testModelName, text); got != sig { t.Errorf("Fresh entry should be retrievable, got '%s'", got) }