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-<uuid>). 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.
This commit is contained in:
@@ -12,6 +12,7 @@ import (
|
|||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/thinking"
|
"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/translator/gemini/common"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
"github.com/tidwall/sjson"
|
"github.com/tidwall/sjson"
|
||||||
)
|
)
|
||||||
@@ -68,6 +69,10 @@ func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _
|
|||||||
contentsJSON := "[]"
|
contentsJSON := "[]"
|
||||||
hasContents := false
|
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")
|
messagesResult := gjson.GetBytes(rawJSON, "messages")
|
||||||
if messagesResult.IsArray() {
|
if messagesResult.IsArray() {
|
||||||
messageResults := messagesResult.Array()
|
messageResults := messagesResult.Array()
|
||||||
@@ -170,6 +175,10 @@ func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _
|
|||||||
argsResult := contentResult.Get("input")
|
argsResult := contentResult.Get("input")
|
||||||
functionID := contentResult.Get("id").String()
|
functionID := contentResult.Get("id").String()
|
||||||
|
|
||||||
|
if functionID != "" && functionName != "" {
|
||||||
|
toolNameByID[functionID] = functionName
|
||||||
|
}
|
||||||
|
|
||||||
// Handle both object and string input formats
|
// Handle both object and string input formats
|
||||||
var argsRaw string
|
var argsRaw string
|
||||||
if argsResult.IsObject() {
|
if argsResult.IsObject() {
|
||||||
@@ -206,10 +215,19 @@ func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _
|
|||||||
} else if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "tool_result" {
|
} else if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "tool_result" {
|
||||||
toolCallID := contentResult.Get("tool_use_id").String()
|
toolCallID := contentResult.Get("tool_use_id").String()
|
||||||
if toolCallID != "" {
|
if toolCallID != "" {
|
||||||
funcName := toolCallID
|
funcName, ok := toolNameByID[toolCallID]
|
||||||
toolCallIDs := strings.Split(toolCallID, "-")
|
if !ok {
|
||||||
if len(toolCallIDs) > 1 {
|
// Fallback: derive a semantic name from the ID by stripping
|
||||||
funcName = strings.Join(toolCallIDs[0:len(toolCallIDs)-2], "-")
|
// 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")
|
functionResponseResult := contentResult.Get("content")
|
||||||
|
|
||||||
|
|||||||
@@ -365,6 +365,17 @@ func TestConvertClaudeRequestToAntigravity_ToolResult(t *testing.T) {
|
|||||||
inputJSON := []byte(`{
|
inputJSON := []byte(`{
|
||||||
"model": "claude-3-5-sonnet-20240620",
|
"model": "claude-3-5-sonnet-20240620",
|
||||||
"messages": [
|
"messages": [
|
||||||
|
{
|
||||||
|
"role": "assistant",
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "tool_use",
|
||||||
|
"id": "get_weather-call-123",
|
||||||
|
"name": "get_weather",
|
||||||
|
"input": {"location": "Paris"}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"role": "user",
|
"role": "user",
|
||||||
"content": [
|
"content": [
|
||||||
@@ -382,13 +393,177 @@ func TestConvertClaudeRequestToAntigravity_ToolResult(t *testing.T) {
|
|||||||
outputStr := string(output)
|
outputStr := string(output)
|
||||||
|
|
||||||
// Check function response conversion
|
// 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() {
|
if !funcResp.Exists() {
|
||||||
t.Error("functionResponse should exist")
|
t.Error("functionResponse should exist")
|
||||||
}
|
}
|
||||||
if funcResp.Get("id").String() != "get_weather-call-123" {
|
if funcResp.Get("id").String() != "get_weather-call-123" {
|
||||||
t.Errorf("Expected function id, got '%s'", funcResp.Get("id").String())
|
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) {
|
func TestConvertClaudeRequestToAntigravity_ThinkingConfig(t *testing.T) {
|
||||||
|
|||||||
Reference in New Issue
Block a user