From ff03dc6a2cd302b1fc9cf476b0bce244f61c4670 Mon Sep 17 00:00:00 2001 From: sususu98 Date: Mon, 16 Mar 2026 10:00:05 +0800 Subject: [PATCH] fix(antigravity): resolve empty functionResponse.name for toolu_* tool_use_id format The Claude-to-Gemini translator derived function names by splitting tool_use_id on "-", which produced empty strings for IDs with exactly 2 segments (e.g. toolu_tool-). Replace the string-splitting heuristic with a lookup map built from tool_use blocks during the main processing loop, with fallback to the raw ID on miss. --- .../claude/antigravity_claude_request.go | 26 ++- .../claude/antigravity_claude_request_test.go | 177 +++++++++++++++++- 2 files changed, 198 insertions(+), 5 deletions(-) diff --git a/internal/translator/antigravity/claude/antigravity_claude_request.go b/internal/translator/antigravity/claude/antigravity_claude_request.go index 3a6ba4b5..bbe4498e 100644 --- a/internal/translator/antigravity/claude/antigravity_claude_request.go +++ b/internal/translator/antigravity/claude/antigravity_claude_request.go @@ -12,6 +12,7 @@ import ( "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking" "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/common" "github.com/router-for-me/CLIProxyAPI/v6/internal/util" + log "github.com/sirupsen/logrus" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) @@ -68,6 +69,10 @@ func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _ contentsJSON := "[]" hasContents := false + // tool_use_id → tool_name lookup, populated incrementally during the main loop. + // Claude's tool_result references tool_use by ID; Gemini requires functionResponse.name. + toolNameByID := make(map[string]string) + messagesResult := gjson.GetBytes(rawJSON, "messages") if messagesResult.IsArray() { messageResults := messagesResult.Array() @@ -170,6 +175,10 @@ func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _ argsResult := contentResult.Get("input") functionID := contentResult.Get("id").String() + if functionID != "" && functionName != "" { + toolNameByID[functionID] = functionName + } + // Handle both object and string input formats var argsRaw string if argsResult.IsObject() { @@ -206,10 +215,19 @@ func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _ } else if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "tool_result" { toolCallID := contentResult.Get("tool_use_id").String() if toolCallID != "" { - funcName := toolCallID - toolCallIDs := strings.Split(toolCallID, "-") - if len(toolCallIDs) > 1 { - funcName = strings.Join(toolCallIDs[0:len(toolCallIDs)-2], "-") + funcName, ok := toolNameByID[toolCallID] + if !ok { + // Fallback: derive a semantic name from the ID by stripping + // the last two dash-separated segments (e.g. "get_weather-call-123" → "get_weather"). + // Only use the raw ID as a last resort when the heuristic produces an empty string. + parts := strings.Split(toolCallID, "-") + if len(parts) > 2 { + funcName = strings.Join(parts[:len(parts)-2], "-") + } + if funcName == "" { + funcName = toolCallID + } + log.Warnf("antigravity claude request: tool_result references unknown tool_use_id=%s, derived function name=%s", toolCallID, funcName) } functionResponseResult := contentResult.Get("content") diff --git a/internal/translator/antigravity/claude/antigravity_claude_request_test.go b/internal/translator/antigravity/claude/antigravity_claude_request_test.go index 696240ef..df84ac54 100644 --- a/internal/translator/antigravity/claude/antigravity_claude_request_test.go +++ b/internal/translator/antigravity/claude/antigravity_claude_request_test.go @@ -365,6 +365,17 @@ func TestConvertClaudeRequestToAntigravity_ToolResult(t *testing.T) { inputJSON := []byte(`{ "model": "claude-3-5-sonnet-20240620", "messages": [ + { + "role": "assistant", + "content": [ + { + "type": "tool_use", + "id": "get_weather-call-123", + "name": "get_weather", + "input": {"location": "Paris"} + } + ] + }, { "role": "user", "content": [ @@ -382,13 +393,177 @@ func TestConvertClaudeRequestToAntigravity_ToolResult(t *testing.T) { outputStr := string(output) // Check function response conversion - funcResp := gjson.Get(outputStr, "request.contents.0.parts.0.functionResponse") + funcResp := gjson.Get(outputStr, "request.contents.1.parts.0.functionResponse") if !funcResp.Exists() { t.Error("functionResponse should exist") } if funcResp.Get("id").String() != "get_weather-call-123" { t.Errorf("Expected function id, got '%s'", funcResp.Get("id").String()) } + if funcResp.Get("name").String() != "get_weather" { + t.Errorf("Expected function name 'get_weather', got '%s'", funcResp.Get("name").String()) + } +} + +func TestConvertClaudeRequestToAntigravity_ToolResultName_TouluFormat(t *testing.T) { + inputJSON := []byte(`{ + "model": "claude-haiku-4-5-20251001", + "messages": [ + { + "role": "assistant", + "content": [ + { + "type": "tool_use", + "id": "toolu_tool-48fca351f12844eabf49dad8b63886d2", + "name": "Glob", + "input": {"pattern": "**/*.py"} + }, + { + "type": "tool_use", + "id": "toolu_tool-cf2d061f75f845c49aacc18ee75ee708", + "name": "Bash", + "input": {"command": "ls"} + } + ] + }, + { + "role": "user", + "content": [ + { + "type": "tool_result", + "tool_use_id": "toolu_tool-48fca351f12844eabf49dad8b63886d2", + "content": "file1.py\nfile2.py" + }, + { + "type": "tool_result", + "tool_use_id": "toolu_tool-cf2d061f75f845c49aacc18ee75ee708", + "content": "total 10" + } + ] + } + ] + }`) + + output := ConvertClaudeRequestToAntigravity("claude-haiku-4-5-20251001", inputJSON, false) + outputStr := string(output) + + funcResp0 := gjson.Get(outputStr, "request.contents.1.parts.0.functionResponse") + if !funcResp0.Exists() { + t.Fatal("first functionResponse should exist") + } + if got := funcResp0.Get("name").String(); got != "Glob" { + t.Errorf("Expected name 'Glob' for toolu_ format, got '%s'", got) + } + + funcResp1 := gjson.Get(outputStr, "request.contents.1.parts.1.functionResponse") + if !funcResp1.Exists() { + t.Fatal("second functionResponse should exist") + } + if got := funcResp1.Get("name").String(); got != "Bash" { + t.Errorf("Expected name 'Bash' for toolu_ format, got '%s'", got) + } +} + +func TestConvertClaudeRequestToAntigravity_ToolResultName_CustomFormat(t *testing.T) { + inputJSON := []byte(`{ + "model": "claude-haiku-4-5-20251001", + "messages": [ + { + "role": "assistant", + "content": [ + { + "type": "tool_use", + "id": "Read-1773420180464065165-1327", + "name": "Read", + "input": {"file_path": "/tmp/test.py"} + } + ] + }, + { + "role": "user", + "content": [ + { + "type": "tool_result", + "tool_use_id": "Read-1773420180464065165-1327", + "content": "file content here" + } + ] + } + ] + }`) + + output := ConvertClaudeRequestToAntigravity("claude-haiku-4-5-20251001", inputJSON, false) + outputStr := string(output) + + funcResp := gjson.Get(outputStr, "request.contents.1.parts.0.functionResponse") + if !funcResp.Exists() { + t.Fatal("functionResponse should exist") + } + if got := funcResp.Get("name").String(); got != "Read" { + t.Errorf("Expected name 'Read', got '%s'", got) + } +} + +func TestConvertClaudeRequestToAntigravity_ToolResultName_NoMatchingToolUse_Heuristic(t *testing.T) { + inputJSON := []byte(`{ + "model": "claude-sonnet-4-5", + "messages": [ + { + "role": "user", + "content": [ + { + "type": "tool_result", + "tool_use_id": "get_weather-call-123", + "content": "22C sunny" + } + ] + } + ] + }`) + + output := ConvertClaudeRequestToAntigravity("claude-sonnet-4-5", inputJSON, false) + outputStr := string(output) + + funcResp := gjson.Get(outputStr, "request.contents.0.parts.0.functionResponse") + if !funcResp.Exists() { + t.Fatal("functionResponse should exist") + } + if got := funcResp.Get("name").String(); got != "get_weather" { + t.Errorf("Expected heuristic-derived name 'get_weather', got '%s'", got) + } +} + +func TestConvertClaudeRequestToAntigravity_ToolResultName_NoMatchingToolUse_RawID(t *testing.T) { + inputJSON := []byte(`{ + "model": "claude-sonnet-4-5", + "messages": [ + { + "role": "user", + "content": [ + { + "type": "tool_result", + "tool_use_id": "toolu_tool-48fca351f12844eabf49dad8b63886d2", + "content": "result data" + } + ] + } + ] + }`) + + output := ConvertClaudeRequestToAntigravity("claude-sonnet-4-5", inputJSON, false) + outputStr := string(output) + + funcResp := gjson.Get(outputStr, "request.contents.0.parts.0.functionResponse") + if !funcResp.Exists() { + t.Fatal("functionResponse should exist") + } + got := funcResp.Get("name").String() + if got == "" { + t.Error("functionResponse.name must not be empty") + } + if got != "toolu_tool-48fca351f12844eabf49dad8b63886d2" { + t.Errorf("Expected raw ID as last-resort name, got '%s'", got) + } } func TestConvertClaudeRequestToAntigravity_ThinkingConfig(t *testing.T) {