diff --git a/internal/translator/antigravity/claude/antigravity_claude_request.go b/internal/translator/antigravity/claude/antigravity_claude_request.go index e6c74bdd..8c1a38c5 100644 --- a/internal/translator/antigravity/claude/antigravity_claude_request.go +++ b/internal/translator/antigravity/claude/antigravity_claude_request.go @@ -431,6 +431,33 @@ func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _ out, _ = sjson.SetRaw(out, "request.tools", toolsJSON) } + // tool_choice + toolChoiceResult := gjson.GetBytes(rawJSON, "tool_choice") + if toolChoiceResult.Exists() { + toolChoiceType := "" + toolChoiceName := "" + if toolChoiceResult.IsObject() { + toolChoiceType = toolChoiceResult.Get("type").String() + toolChoiceName = toolChoiceResult.Get("name").String() + } else if toolChoiceResult.Type == gjson.String { + toolChoiceType = toolChoiceResult.String() + } + + switch toolChoiceType { + case "auto": + out, _ = sjson.Set(out, "request.toolConfig.functionCallingConfig.mode", "AUTO") + case "none": + out, _ = sjson.Set(out, "request.toolConfig.functionCallingConfig.mode", "NONE") + case "any": + out, _ = sjson.Set(out, "request.toolConfig.functionCallingConfig.mode", "ANY") + case "tool": + out, _ = sjson.Set(out, "request.toolConfig.functionCallingConfig.mode", "ANY") + if toolChoiceName != "" { + out, _ = sjson.Set(out, "request.toolConfig.functionCallingConfig.allowedFunctionNames", []string{toolChoiceName}) + } + } + } + // Map Anthropic thinking -> Gemini thinkingBudget/include_thoughts when type==enabled if t := gjson.GetBytes(rawJSON, "thinking"); enableThoughtTranslate && t.Exists() && t.IsObject() { switch t.Get("type").String() { diff --git a/internal/translator/antigravity/claude/antigravity_claude_request_test.go b/internal/translator/antigravity/claude/antigravity_claude_request_test.go index 53a24339..39dc493d 100644 --- a/internal/translator/antigravity/claude/antigravity_claude_request_test.go +++ b/internal/translator/antigravity/claude/antigravity_claude_request_test.go @@ -193,6 +193,42 @@ func TestConvertClaudeRequestToAntigravity_ToolDeclarations(t *testing.T) { } } +func TestConvertClaudeRequestToAntigravity_ToolChoice_SpecificTool(t *testing.T) { + inputJSON := []byte(`{ + "model": "gemini-3-flash-preview", + "messages": [ + { + "role": "user", + "content": [ + {"type": "text", "text": "hi"} + ] + } + ], + "tools": [ + { + "name": "json", + "description": "A JSON tool", + "input_schema": { + "type": "object", + "properties": {} + } + } + ], + "tool_choice": {"type": "tool", "name": "json"} + }`) + + output := ConvertClaudeRequestToAntigravity("gemini-3-flash-preview", inputJSON, false) + outputStr := string(output) + + if got := gjson.Get(outputStr, "request.toolConfig.functionCallingConfig.mode").String(); got != "ANY" { + t.Fatalf("Expected toolConfig.functionCallingConfig.mode 'ANY', got '%s'", got) + } + allowed := gjson.Get(outputStr, "request.toolConfig.functionCallingConfig.allowedFunctionNames").Array() + if len(allowed) != 1 || allowed[0].String() != "json" { + t.Fatalf("Expected allowedFunctionNames ['json'], got %s", gjson.Get(outputStr, "request.toolConfig.functionCallingConfig.allowedFunctionNames").Raw) + } +} + func TestConvertClaudeRequestToAntigravity_ToolUse(t *testing.T) { inputJSON := []byte(`{ "model": "claude-3-5-sonnet-20240620", diff --git a/internal/translator/gemini-cli/claude/gemini-cli_claude_request.go b/internal/translator/gemini-cli/claude/gemini-cli_claude_request.go index 076e09db..e3753b03 100644 --- a/internal/translator/gemini-cli/claude/gemini-cli_claude_request.go +++ b/internal/translator/gemini-cli/claude/gemini-cli_claude_request.go @@ -172,6 +172,33 @@ func ConvertClaudeRequestToCLI(modelName string, inputRawJSON []byte, _ bool) [] } } + // tool_choice + toolChoiceResult := gjson.GetBytes(rawJSON, "tool_choice") + if toolChoiceResult.Exists() { + toolChoiceType := "" + toolChoiceName := "" + if toolChoiceResult.IsObject() { + toolChoiceType = toolChoiceResult.Get("type").String() + toolChoiceName = toolChoiceResult.Get("name").String() + } else if toolChoiceResult.Type == gjson.String { + toolChoiceType = toolChoiceResult.String() + } + + switch toolChoiceType { + case "auto": + out, _ = sjson.Set(out, "request.toolConfig.functionCallingConfig.mode", "AUTO") + case "none": + out, _ = sjson.Set(out, "request.toolConfig.functionCallingConfig.mode", "NONE") + case "any": + out, _ = sjson.Set(out, "request.toolConfig.functionCallingConfig.mode", "ANY") + case "tool": + out, _ = sjson.Set(out, "request.toolConfig.functionCallingConfig.mode", "ANY") + if toolChoiceName != "" { + out, _ = sjson.Set(out, "request.toolConfig.functionCallingConfig.allowedFunctionNames", []string{toolChoiceName}) + } + } + } + // Map Anthropic thinking -> Gemini CLI thinkingConfig when enabled // Translator only does format conversion, ApplyThinking handles model capability validation. if t := gjson.GetBytes(rawJSON, "thinking"); t.Exists() && t.IsObject() { diff --git a/internal/translator/gemini-cli/claude/gemini-cli_claude_request_test.go b/internal/translator/gemini-cli/claude/gemini-cli_claude_request_test.go new file mode 100644 index 00000000..10364e75 --- /dev/null +++ b/internal/translator/gemini-cli/claude/gemini-cli_claude_request_test.go @@ -0,0 +1,42 @@ +package claude + +import ( + "testing" + + "github.com/tidwall/gjson" +) + +func TestConvertClaudeRequestToCLI_ToolChoice_SpecificTool(t *testing.T) { + inputJSON := []byte(`{ + "model": "gemini-3-flash-preview", + "messages": [ + { + "role": "user", + "content": [ + {"type": "text", "text": "hi"} + ] + } + ], + "tools": [ + { + "name": "json", + "description": "A JSON tool", + "input_schema": { + "type": "object", + "properties": {} + } + } + ], + "tool_choice": {"type": "tool", "name": "json"} + }`) + + output := ConvertClaudeRequestToCLI("gemini-3-flash-preview", inputJSON, false) + + if got := gjson.GetBytes(output, "request.toolConfig.functionCallingConfig.mode").String(); got != "ANY" { + t.Fatalf("Expected request.toolConfig.functionCallingConfig.mode 'ANY', got '%s'", got) + } + allowed := gjson.GetBytes(output, "request.toolConfig.functionCallingConfig.allowedFunctionNames").Array() + if len(allowed) != 1 || allowed[0].String() != "json" { + t.Fatalf("Expected allowedFunctionNames ['json'], got %s", gjson.GetBytes(output, "request.toolConfig.functionCallingConfig.allowedFunctionNames").Raw) + } +} diff --git a/internal/translator/gemini/claude/gemini_claude_request.go b/internal/translator/gemini/claude/gemini_claude_request.go index 0e367c0d..ff276ce3 100644 --- a/internal/translator/gemini/claude/gemini_claude_request.go +++ b/internal/translator/gemini/claude/gemini_claude_request.go @@ -153,6 +153,33 @@ func ConvertClaudeRequestToGemini(modelName string, inputRawJSON []byte, _ bool) } } + // tool_choice + toolChoiceResult := gjson.GetBytes(rawJSON, "tool_choice") + if toolChoiceResult.Exists() { + toolChoiceType := "" + toolChoiceName := "" + if toolChoiceResult.IsObject() { + toolChoiceType = toolChoiceResult.Get("type").String() + toolChoiceName = toolChoiceResult.Get("name").String() + } else if toolChoiceResult.Type == gjson.String { + toolChoiceType = toolChoiceResult.String() + } + + switch toolChoiceType { + case "auto": + out, _ = sjson.Set(out, "toolConfig.functionCallingConfig.mode", "AUTO") + case "none": + out, _ = sjson.Set(out, "toolConfig.functionCallingConfig.mode", "NONE") + case "any": + out, _ = sjson.Set(out, "toolConfig.functionCallingConfig.mode", "ANY") + case "tool": + out, _ = sjson.Set(out, "toolConfig.functionCallingConfig.mode", "ANY") + if toolChoiceName != "" { + out, _ = sjson.Set(out, "toolConfig.functionCallingConfig.allowedFunctionNames", []string{toolChoiceName}) + } + } + } + // Map Anthropic thinking -> Gemini thinking config when enabled // Translator only does format conversion, ApplyThinking handles model capability validation. if t := gjson.GetBytes(rawJSON, "thinking"); t.Exists() && t.IsObject() { diff --git a/internal/translator/gemini/claude/gemini_claude_request_test.go b/internal/translator/gemini/claude/gemini_claude_request_test.go new file mode 100644 index 00000000..e242c42c --- /dev/null +++ b/internal/translator/gemini/claude/gemini_claude_request_test.go @@ -0,0 +1,42 @@ +package claude + +import ( + "testing" + + "github.com/tidwall/gjson" +) + +func TestConvertClaudeRequestToGemini_ToolChoice_SpecificTool(t *testing.T) { + inputJSON := []byte(`{ + "model": "gemini-3-flash-preview", + "messages": [ + { + "role": "user", + "content": [ + {"type": "text", "text": "hi"} + ] + } + ], + "tools": [ + { + "name": "json", + "description": "A JSON tool", + "input_schema": { + "type": "object", + "properties": {} + } + } + ], + "tool_choice": {"type": "tool", "name": "json"} + }`) + + output := ConvertClaudeRequestToGemini("gemini-3-flash-preview", inputJSON, false) + + if got := gjson.GetBytes(output, "toolConfig.functionCallingConfig.mode").String(); got != "ANY" { + t.Fatalf("Expected toolConfig.functionCallingConfig.mode 'ANY', got '%s'", got) + } + allowed := gjson.GetBytes(output, "toolConfig.functionCallingConfig.allowedFunctionNames").Array() + if len(allowed) != 1 || allowed[0].String() != "json" { + t.Fatalf("Expected allowedFunctionNames ['json'], got %s", gjson.GetBytes(output, "toolConfig.functionCallingConfig.allowedFunctionNames").Raw) + } +}