From 41577bce0789d962fa8276d88ccfd8f21369f6c3 Mon Sep 17 00:00:00 2001 From: hkfires <10558748+hkfires@users.noreply.github.com> Date: Wed, 29 Oct 2025 13:11:28 +0800 Subject: [PATCH] feat(claude): map Anthropic 'thinking' to Gemini thinkingBudget --- .../claude/gemini-cli_claude_request.go | 27 +++----- .../gemini/claude/gemini_claude_request.go | 27 +++----- internal/util/thinking.go | 69 +++++++++++++++++++ 3 files changed, 90 insertions(+), 33 deletions(-) create mode 100644 internal/util/thinking.go 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 f4ba7d37..e4801d08 100644 --- a/internal/translator/gemini-cli/claude/gemini-cli_claude_request.go +++ b/internal/translator/gemini-cli/claude/gemini-cli_claude_request.go @@ -11,6 +11,7 @@ import ( "strings" client "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" + "github.com/router-for-me/CLIProxyAPI/v6/internal/util" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) @@ -135,7 +136,7 @@ func ConvertClaudeRequestToCLI(modelName string, inputRawJSON []byte, _ bool) [] tools = make([]client.ToolDeclaration, 0) } - // Build output Gemini CLI request JSON (no default thinkingConfig) + // Build output Gemini CLI request JSON out := `{"model":"","request":{"contents":[]}}` out, _ = sjson.Set(out, "model", modelName) if systemInstruction != nil { @@ -151,22 +152,14 @@ func ConvertClaudeRequestToCLI(modelName string, inputRawJSON []byte, _ bool) [] out, _ = sjson.SetRaw(out, "request.tools", string(b)) } - // Map reasoning and sampling configs: only set thinkingConfig when explicitly requested - reasoningEffortResult := gjson.GetBytes(rawJSON, "reasoning_effort") - if reasoningEffortResult.Exists() { - if reasoningEffortResult.String() == "none" { - out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.include_thoughts", false) - out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.thinkingBudget", 0) - } else if reasoningEffortResult.String() == "auto" { - out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.thinkingBudget", -1) - } else if reasoningEffortResult.String() == "low" { - out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.thinkingBudget", 1024) - } else if reasoningEffortResult.String() == "medium" { - out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.thinkingBudget", 8192) - } else if reasoningEffortResult.String() == "high" { - out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.thinkingBudget", 24576) - } else { - out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.thinkingBudget", -1) + // Map Anthropic thinking -> Gemini thinkingBudget when type==enabled + if t := gjson.GetBytes(rawJSON, "thinking"); t.Exists() && t.IsObject() && util.ModelSupportsThinking(modelName) { + if t.Get("type").String() == "enabled" { + if b := t.Get("budget_tokens"); b.Exists() && b.Type == gjson.Number { + budget := int(b.Int()) + budget = util.NormalizeThinkingBudget(modelName, budget) + out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.thinkingBudget", budget) + } } } if v := gjson.GetBytes(rawJSON, "temperature"); v.Exists() && v.Type == gjson.Number { diff --git a/internal/translator/gemini/claude/gemini_claude_request.go b/internal/translator/gemini/claude/gemini_claude_request.go index 6a477dbd..e9d79276 100644 --- a/internal/translator/gemini/claude/gemini_claude_request.go +++ b/internal/translator/gemini/claude/gemini_claude_request.go @@ -11,6 +11,7 @@ import ( "strings" client "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" + "github.com/router-for-me/CLIProxyAPI/v6/internal/util" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) @@ -129,7 +130,7 @@ func ConvertClaudeRequestToGemini(modelName string, inputRawJSON []byte, _ bool) } // Build output Gemini CLI request JSON - out := `{"contents":[],"generationConfig":{"thinkingConfig":{"include_thoughts":true}}}` + out := `{"contents":[]}` out, _ = sjson.Set(out, "model", modelName) if systemInstruction != nil { b, _ := json.Marshal(systemInstruction) @@ -144,21 +145,15 @@ func ConvertClaudeRequestToGemini(modelName string, inputRawJSON []byte, _ bool) out, _ = sjson.SetRaw(out, "tools", string(b)) } - // Map reasoning and sampling configs - reasoningEffortResult := gjson.GetBytes(rawJSON, "reasoning_effort") - if reasoningEffortResult.String() == "none" { - out, _ = sjson.Set(out, "generationConfig.thinkingConfig.include_thoughts", false) - out, _ = sjson.Set(out, "generationConfig.thinkingConfig.thinkingBudget", 0) - } else if reasoningEffortResult.String() == "auto" { - out, _ = sjson.Set(out, "generationConfig.thinkingConfig.thinkingBudget", -1) - } else if reasoningEffortResult.String() == "low" { - out, _ = sjson.Set(out, "generationConfig.thinkingConfig.thinkingBudget", 1024) - } else if reasoningEffortResult.String() == "medium" { - out, _ = sjson.Set(out, "generationConfig.thinkingConfig.thinkingBudget", 8192) - } else if reasoningEffortResult.String() == "high" { - out, _ = sjson.Set(out, "generationConfig.thinkingConfig.thinkingBudget", 24576) - } else { - out, _ = sjson.Set(out, "generationConfig.thinkingConfig.thinkingBudget", -1) + // Map Anthropic thinking -> Gemini thinkingBudget when enabled + if t := gjson.GetBytes(rawJSON, "thinking"); t.Exists() && t.IsObject() && util.ModelSupportsThinking(modelName) { + if t.Get("type").String() == "enabled" { + if b := t.Get("budget_tokens"); b.Exists() && b.Type == gjson.Number { + budget := int(b.Int()) + budget = util.NormalizeThinkingBudget(modelName, budget) + out, _ = sjson.Set(out, "generationConfig.thinkingConfig.thinkingBudget", budget) + } + } } if v := gjson.GetBytes(rawJSON, "temperature"); v.Exists() && v.Type == gjson.Number { out, _ = sjson.Set(out, "generationConfig.temperature", v.Num) diff --git a/internal/util/thinking.go b/internal/util/thinking.go new file mode 100644 index 00000000..c16b91cd --- /dev/null +++ b/internal/util/thinking.go @@ -0,0 +1,69 @@ +package util + +import ( + "github.com/router-for-me/CLIProxyAPI/v6/internal/registry" +) + +// ModelSupportsThinking reports whether the given model has Thinking capability +// according to the model registry metadata (provider-agnostic). +func ModelSupportsThinking(model string) bool { + if model == "" { + return false + } + if info := registry.GetGlobalRegistry().GetModelInfo(model); info != nil { + return info.Thinking != nil + } + return false +} + +// NormalizeThinkingBudget clamps the requested thinking budget to the +// supported range for the specified model using registry metadata only. +// If the model is unknown or has no Thinking metadata, returns the original budget. +// For dynamic (-1), returns -1 if DynamicAllowed; otherwise approximates mid-range +// or min (0 if zero is allowed and mid <= 0). +func NormalizeThinkingBudget(model string, budget int) int { + if budget == -1 { // dynamic + if found, min, max, zeroAllowed, dynamicAllowed := thinkingRangeFromRegistry(model); found { + if dynamicAllowed { + return -1 + } + mid := (min + max) / 2 + if mid <= 0 && zeroAllowed { + return 0 + } + if mid <= 0 { + return min + } + return mid + } + return -1 + } + if found, min, max, zeroAllowed, _ := thinkingRangeFromRegistry(model); found { + if budget == 0 { + if zeroAllowed { + return 0 + } + return min + } + if budget < min { + return min + } + if budget > max { + return max + } + return budget + } + return budget +} + +// thinkingRangeFromRegistry attempts to read thinking ranges from the model registry. +func thinkingRangeFromRegistry(model string) (found bool, min int, max int, zeroAllowed bool, dynamicAllowed bool) { + if model == "" { + return false, 0, 0, false, false + } + info := registry.GetGlobalRegistry().GetModelInfo(model) + if info == nil || info.Thinking == nil { + return false, 0, 0, false, false + } + return true, info.Thinking.Min, info.Thinking.Max, info.Thinking.ZeroAllowed, info.Thinking.DynamicAllowed +}