diff --git a/internal/translator/antigravity/openai/chat-completions/antigravity_openai_request.go b/internal/translator/antigravity/openai/chat-completions/antigravity_openai_request.go index 717f88f7..251357bb 100644 --- a/internal/translator/antigravity/openai/chat-completions/antigravity_openai_request.go +++ b/internal/translator/antigravity/openai/chat-completions/antigravity_openai_request.go @@ -39,7 +39,7 @@ func ConvertOpenAIRequestToAntigravity(modelName string, inputRawJSON []byte, _ // Note: OpenAI official fields take precedence over extra_body.google.thinking_config re := gjson.GetBytes(rawJSON, "reasoning_effort") hasOfficialThinking := re.Exists() - if hasOfficialThinking && util.ModelSupportsThinking(modelName) { + if hasOfficialThinking && util.ModelSupportsThinking(modelName) && !util.ModelUsesThinkingLevels(modelName) { switch re.String() { case "none": out, _ = sjson.DeleteBytes(out, "request.generationConfig.thinkingConfig.include_thoughts") @@ -63,7 +63,8 @@ func ConvertOpenAIRequestToAntigravity(modelName string, inputRawJSON []byte, _ } // Cherry Studio extension extra_body.google.thinking_config (effective only when official fields are absent) - if !hasOfficialThinking && util.ModelSupportsThinking(modelName) { + // Only apply for models that use numeric budgets, not discrete levels. + if !hasOfficialThinking && util.ModelSupportsThinking(modelName) && !util.ModelUsesThinkingLevels(modelName) { if tc := gjson.GetBytes(rawJSON, "extra_body.google.thinking_config"); tc.Exists() && tc.IsObject() { var setBudget bool var budget int diff --git a/internal/translator/claude/gemini/claude_gemini_request.go b/internal/translator/claude/gemini/claude_gemini_request.go index 302c7d66..780dd5f4 100644 --- a/internal/translator/claude/gemini/claude_gemini_request.go +++ b/internal/translator/claude/gemini/claude_gemini_request.go @@ -114,7 +114,8 @@ func ConvertGeminiRequestToClaude(modelName string, inputRawJSON []byte, stream } } // Include thoughts configuration for reasoning process visibility - if thinkingConfig := genConfig.Get("thinkingConfig"); thinkingConfig.Exists() && thinkingConfig.IsObject() { + // Only apply for models that use numeric budgets, not discrete levels. + if thinkingConfig := genConfig.Get("thinkingConfig"); thinkingConfig.Exists() && thinkingConfig.IsObject() && !util.ModelUsesThinkingLevels(modelName) { if includeThoughts := thinkingConfig.Get("include_thoughts"); includeThoughts.Exists() { if includeThoughts.Type == gjson.True { out, _ = sjson.Set(out, "thinking.type", "enabled") diff --git a/internal/translator/codex/claude/codex_claude_request.go b/internal/translator/codex/claude/codex_claude_request.go index 3c86e3cf..414efa89 100644 --- a/internal/translator/codex/claude/codex_claude_request.go +++ b/internal/translator/codex/claude/codex_claude_request.go @@ -12,6 +12,7 @@ import ( "strings" "github.com/router-for-me/CLIProxyAPI/v6/internal/misc" + "github.com/router-for-me/CLIProxyAPI/v6/internal/util" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) @@ -214,7 +215,22 @@ func ConvertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool) // Add additional configuration parameters for the Codex API. template, _ = sjson.Set(template, "parallel_tool_calls", true) - template, _ = sjson.Set(template, "reasoning.effort", "medium") + + // Convert thinking.budget_tokens to reasoning.effort for level-based models + reasoningEffort := "medium" // default + if thinking := rootResult.Get("thinking"); thinking.Exists() && thinking.IsObject() { + if thinking.Get("type").String() == "enabled" { + if util.ModelUsesThinkingLevels(modelName) { + if budgetTokens := thinking.Get("budget_tokens"); budgetTokens.Exists() { + budget := int(budgetTokens.Int()) + if effort, ok := util.OpenAIThinkingBudgetToEffort(modelName, budget); ok && effort != "" { + reasoningEffort = effort + } + } + } + } + } + template, _ = sjson.Set(template, "reasoning.effort", reasoningEffort) template, _ = sjson.Set(template, "reasoning.summary", "auto") template, _ = sjson.Set(template, "stream", true) template, _ = sjson.Set(template, "store", false) diff --git a/internal/translator/codex/gemini/codex_gemini_request.go b/internal/translator/codex/gemini/codex_gemini_request.go index 427fd9ad..c2dacd3e 100644 --- a/internal/translator/codex/gemini/codex_gemini_request.go +++ b/internal/translator/codex/gemini/codex_gemini_request.go @@ -245,7 +245,22 @@ func ConvertGeminiRequestToCodex(modelName string, inputRawJSON []byte, _ bool) // Fixed flags aligning with Codex expectations out, _ = sjson.Set(out, "parallel_tool_calls", true) - out, _ = sjson.Set(out, "reasoning.effort", "medium") + + // Convert thinkingBudget to reasoning.effort for level-based models + reasoningEffort := "medium" // default + if genConfig := root.Get("generationConfig"); genConfig.Exists() { + if thinkingConfig := genConfig.Get("thinkingConfig"); thinkingConfig.Exists() && thinkingConfig.IsObject() { + if util.ModelUsesThinkingLevels(modelName) { + if thinkingBudget := thinkingConfig.Get("thinkingBudget"); thinkingBudget.Exists() { + budget := int(thinkingBudget.Int()) + if effort, ok := util.OpenAIThinkingBudgetToEffort(modelName, budget); ok && effort != "" { + reasoningEffort = effort + } + } + } + } + } + out, _ = sjson.Set(out, "reasoning.effort", reasoningEffort) out, _ = sjson.Set(out, "reasoning.summary", "auto") out, _ = sjson.Set(out, "stream", true) out, _ = sjson.Set(out, "store", false) diff --git a/internal/translator/gemini-cli/openai/chat-completions/gemini-cli_openai_request.go b/internal/translator/gemini-cli/openai/chat-completions/gemini-cli_openai_request.go index b52bf224..c7560d2f 100644 --- a/internal/translator/gemini-cli/openai/chat-completions/gemini-cli_openai_request.go +++ b/internal/translator/gemini-cli/openai/chat-completions/gemini-cli_openai_request.go @@ -39,7 +39,7 @@ func ConvertOpenAIRequestToGeminiCLI(modelName string, inputRawJSON []byte, _ bo // Note: OpenAI official fields take precedence over extra_body.google.thinking_config re := gjson.GetBytes(rawJSON, "reasoning_effort") hasOfficialThinking := re.Exists() - if hasOfficialThinking && util.ModelSupportsThinking(modelName) { + if hasOfficialThinking && util.ModelSupportsThinking(modelName) && !util.ModelUsesThinkingLevels(modelName) { switch re.String() { case "none": out, _ = sjson.DeleteBytes(out, "request.generationConfig.thinkingConfig.include_thoughts") @@ -63,7 +63,8 @@ func ConvertOpenAIRequestToGeminiCLI(modelName string, inputRawJSON []byte, _ bo } // Cherry Studio extension extra_body.google.thinking_config (effective only when official fields are absent) - if !hasOfficialThinking && util.ModelSupportsThinking(modelName) { + // Only apply for models that use numeric budgets, not discrete levels. + if !hasOfficialThinking && util.ModelSupportsThinking(modelName) && !util.ModelUsesThinkingLevels(modelName) { if tc := gjson.GetBytes(rawJSON, "extra_body.google.thinking_config"); tc.Exists() && tc.IsObject() { var setBudget bool var budget int diff --git a/internal/translator/gemini/claude/gemini_claude_request.go b/internal/translator/gemini/claude/gemini_claude_request.go index 45a5a88f..f626a581 100644 --- a/internal/translator/gemini/claude/gemini_claude_request.go +++ b/internal/translator/gemini/claude/gemini_claude_request.go @@ -154,7 +154,8 @@ func ConvertClaudeRequestToGemini(modelName string, inputRawJSON []byte, _ bool) } // Map Anthropic thinking -> Gemini thinkingBudget/include_thoughts when enabled - if t := gjson.GetBytes(rawJSON, "thinking"); t.Exists() && t.IsObject() && util.ModelSupportsThinking(modelName) { + // Only apply for models that use numeric budgets, not discrete levels. + if t := gjson.GetBytes(rawJSON, "thinking"); t.Exists() && t.IsObject() && util.ModelSupportsThinking(modelName) && !util.ModelUsesThinkingLevels(modelName) { if t.Get("type").String() == "enabled" { if b := t.Get("budget_tokens"); b.Exists() && b.Type == gjson.Number { budget := int(b.Int()) diff --git a/internal/translator/gemini/openai/chat-completions/gemini_openai_request.go b/internal/translator/gemini/openai/chat-completions/gemini_openai_request.go index 8c48a5b3..e754d0f1 100644 --- a/internal/translator/gemini/openai/chat-completions/gemini_openai_request.go +++ b/internal/translator/gemini/openai/chat-completions/gemini_openai_request.go @@ -37,9 +37,11 @@ func ConvertOpenAIRequestToGemini(modelName string, inputRawJSON []byte, _ bool) // Reasoning effort -> thinkingBudget/include_thoughts // Note: OpenAI official fields take precedence over extra_body.google.thinking_config + // Only convert for models that use numeric budgets (not discrete levels) to avoid + // incorrectly applying thinkingBudget for level-based models like gpt-5. re := gjson.GetBytes(rawJSON, "reasoning_effort") hasOfficialThinking := re.Exists() - if hasOfficialThinking && util.ModelSupportsThinking(modelName) { + if hasOfficialThinking && util.ModelSupportsThinking(modelName) && !util.ModelUsesThinkingLevels(modelName) { switch re.String() { case "none": out, _ = sjson.DeleteBytes(out, "generationConfig.thinkingConfig.include_thoughts") @@ -63,7 +65,8 @@ func ConvertOpenAIRequestToGemini(modelName string, inputRawJSON []byte, _ bool) } // Cherry Studio extension extra_body.google.thinking_config (effective only when official fields are absent) - if !hasOfficialThinking && util.ModelSupportsThinking(modelName) { + // Only apply for models that use numeric budgets, not discrete levels. + if !hasOfficialThinking && util.ModelSupportsThinking(modelName) && !util.ModelUsesThinkingLevels(modelName) { if tc := gjson.GetBytes(rawJSON, "extra_body.google.thinking_config"); tc.Exists() && tc.IsObject() { var setBudget bool var budget int diff --git a/internal/translator/gemini/openai/responses/gemini_openai-responses_request.go b/internal/translator/gemini/openai/responses/gemini_openai-responses_request.go index bdf59785..b6f471d9 100644 --- a/internal/translator/gemini/openai/responses/gemini_openai-responses_request.go +++ b/internal/translator/gemini/openai/responses/gemini_openai-responses_request.go @@ -389,8 +389,9 @@ func ConvertOpenAIResponsesRequestToGemini(modelName string, inputRawJSON []byte } // OpenAI official reasoning fields take precedence + // Only convert for models that use numeric budgets (not discrete levels). hasOfficialThinking := root.Get("reasoning.effort").Exists() - if hasOfficialThinking && util.ModelSupportsThinking(modelName) { + if hasOfficialThinking && util.ModelSupportsThinking(modelName) && !util.ModelUsesThinkingLevels(modelName) { reasoningEffort := root.Get("reasoning.effort") switch reasoningEffort.String() { case "none": @@ -418,7 +419,8 @@ func ConvertOpenAIResponsesRequestToGemini(modelName string, inputRawJSON []byte } // Cherry Studio extension (applies only when official fields are missing) - if !hasOfficialThinking && util.ModelSupportsThinking(modelName) { + // Only apply for models that use numeric budgets, not discrete levels. + if !hasOfficialThinking && util.ModelSupportsThinking(modelName) && !util.ModelUsesThinkingLevels(modelName) { if tc := root.Get("extra_body.google.thinking_config"); tc.Exists() && tc.IsObject() { var setBudget bool var budget int diff --git a/internal/translator/openai/claude/openai_claude_request.go b/internal/translator/openai/claude/openai_claude_request.go index 3521b2e5..0ee8c225 100644 --- a/internal/translator/openai/claude/openai_claude_request.go +++ b/internal/translator/openai/claude/openai_claude_request.go @@ -10,6 +10,7 @@ import ( "encoding/json" "strings" + "github.com/router-for-me/CLIProxyAPI/v6/internal/util" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) @@ -60,6 +61,18 @@ func ConvertClaudeRequestToOpenAI(modelName string, inputRawJSON []byte, stream // Stream out, _ = sjson.Set(out, "stream", stream) + // Thinking: Convert Claude thinking.budget_tokens to OpenAI reasoning_effort + if thinking := root.Get("thinking"); thinking.Exists() && thinking.IsObject() { + if thinkingType := thinking.Get("type"); thinkingType.Exists() && thinkingType.String() == "enabled" { + if budgetTokens := thinking.Get("budget_tokens"); budgetTokens.Exists() { + budget := int(budgetTokens.Int()) + if effort, ok := util.OpenAIThinkingBudgetToEffort(modelName, budget); ok && effort != "" { + out, _ = sjson.Set(out, "reasoning_effort", effort) + } + } + } + } + // Process messages and system var messagesJSON = "[]" diff --git a/internal/translator/openai/gemini/openai_gemini_request.go b/internal/translator/openai/gemini/openai_gemini_request.go index deedf96a..1fd20f82 100644 --- a/internal/translator/openai/gemini/openai_gemini_request.go +++ b/internal/translator/openai/gemini/openai_gemini_request.go @@ -13,6 +13,7 @@ import ( "math/big" "strings" + "github.com/router-for-me/CLIProxyAPI/v6/internal/util" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) @@ -76,6 +77,18 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream out, _ = sjson.Set(out, "stop", stops) } } + + // Convert thinkingBudget to reasoning_effort for level-based models + if thinkingConfig := genConfig.Get("thinkingConfig"); thinkingConfig.Exists() && thinkingConfig.IsObject() { + if util.ModelUsesThinkingLevels(modelName) { + if thinkingBudget := thinkingConfig.Get("thinkingBudget"); thinkingBudget.Exists() { + budget := int(thinkingBudget.Int()) + if effort, ok := util.OpenAIThinkingBudgetToEffort(modelName, budget); ok && effort != "" { + out, _ = sjson.Set(out, "reasoning_effort", effort) + } + } + } + } } // Stream parameter