From 791a42ad8ba355366b842803dfee2445d7039eea Mon Sep 17 00:00:00 2001 From: Alex Castillo Date: Mon, 26 Oct 2026 20:19:42 -0400 Subject: [PATCH] fix: honor Claude tool_choice when converting requests to Gemini Co-Authored-By: Claude Sonnet 5.5 --- internal/transformer/convert/claude_gemini.go | 34 +++++++++++-- .../convert/claude_gemini_tool_choice_test.go | 49 +++++++++++++++++++ 2 files changed, 80 insertions(+), 3 deletions(-) create mode 100644 internal/transformer/convert/claude_gemini_tool_choice_test.go diff --git a/internal/transformer/convert/claude_gemini.go b/internal/transformer/convert/claude_gemini.go index a7fc3c4..25f2924 100644 --- a/internal/transformer/convert/claude_gemini.go +++ b/internal/transformer/convert/claude_gemini.go @@ -73,15 +73,43 @@ func ClaudeReqToGemini(claudeReq []byte, model string) ([]byte, error) { geminiReq["tools"] = []map[string]interface{}{{"functionDeclarations": funcDecls}} // Add toolConfig to enable function calling geminiReq["toolConfig"] = map[string]interface{}{ - "functionCallingConfig": map[string]interface{}{ - "mode": "AUTO", - }, + "functionCallingConfig": claudeToolChoiceToGeminiConfig(req.ToolChoice), } } return json.Marshal(geminiReq) } +// claudeToolChoiceToGeminiConfig maps a Claude tool_choice to a Gemini +// functionCallingConfig. A missing or unknown choice stays AUTO. +func claudeToolChoiceToGeminiConfig(toolChoice interface{}) map[string]interface{} { + config := map[string]interface{}{"mode": "AUTO"} + + var choiceType string + var name string + switch tc := toolChoice.(type) { + case map[string]interface{}: + choiceType, _ = tc["type"].(string) + name, _ = tc["name"].(string) + case string: + choiceType = tc + } + + switch choiceType { + case "any", "required": + config["mode"] = "ANY" + case "tool": + config["mode"] = "ANY" + if name != "" { + config["allowedFunctionNames"] = []string{name} + } + case "none": + config["mode"] = "NONE" + } + + return config +} + // GeminiReqToClaude converts Gemini request to Claude request func GeminiReqToClaude(geminiReq []byte, model string) ([]byte, error) { var req transformer.GeminiRequest diff --git a/internal/transformer/convert/claude_gemini_tool_choice_test.go b/internal/transformer/convert/claude_gemini_tool_choice_test.go new file mode 100644 index 0000000..d7c367d --- /dev/null +++ b/internal/transformer/convert/claude_gemini_tool_choice_test.go @@ -0,0 +1,49 @@ +package convert + +import ( + "encoding/json" + "reflect" + "testing" +) + +func TestClaudeReqToGeminiToolChoice(t *testing.T) { + tests := []struct { + name string + toolChoice string + wantMode string + wantNames []interface{} + }{ + {"omitted", ``, "AUTO", nil}, + {"auto", `,"tool_choice":{"type":"auto"}`, "AUTO", nil}, + {"any", `,"tool_choice":{"type":"any"}`, "ANY", nil}, + {"tool", `,"tool_choice":{"type":"tool","name":"get_weather"}`, "ANY", []interface{}{"get_weather"}}, + {"none", `,"tool_choice":{"type":"none"}`, "NONE", nil}, + {"string any", `,"tool_choice":"any"`, "ANY", nil}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + claudeReq := `{"model":"claude","max_tokens":100,"messages":[{"role":"user","content":"hi"}], + "tools":[{"name":"get_weather","description":"d","input_schema":{"type":"object","properties":{}}}]` + tt.toolChoice + `}` + + out, err := ClaudeReqToGemini([]byte(claudeReq), "gemini-2.5-pro") + if err != nil { + t.Fatalf("ClaudeReqToGemini failed: %v", err) + } + + var got map[string]interface{} + if err := json.Unmarshal(out, &got); err != nil { + t.Fatalf("invalid output: %v", err) + } + toolConfig, _ := got["toolConfig"].(map[string]interface{}) + fcc, _ := toolConfig["functionCallingConfig"].(map[string]interface{}) + if fcc["mode"] != tt.wantMode { + t.Errorf("mode = %v, want %v", fcc["mode"], tt.wantMode) + } + gotNames, _ := fcc["allowedFunctionNames"].([]interface{}) + if !reflect.DeepEqual(gotNames, tt.wantNames) { + t.Errorf("allowedFunctionNames = %v, want %v", gotNames, tt.wantNames) + } + }) + } +}