diff --git a/internal/thinking/apply.go b/internal/thinking/apply.go index 8a5a1d7d2..0982bbccf 100644 --- a/internal/thinking/apply.go +++ b/internal/thinking/apply.go @@ -342,12 +342,13 @@ func hasThinkingConfig(config ThinkingConfig) bool { // extractClaudeConfig extracts thinking configuration from Claude format request body. // // Claude API format: -// - thinking.type: "enabled" or "disabled" +// - thinking.type: "enabled", "disabled", or "adaptive" // - thinking.budget_tokens: integer (-1=auto, 0=disabled, >0=budget) // // Priority: thinking.type="disabled" takes precedence over budget_tokens. // When type="enabled" without budget_tokens, returns ModeAuto to indicate // the user wants thinking enabled but didn't specify a budget. +// When type="adaptive" without budget_tokens or effort, defaults to LevelHigh. func extractClaudeConfig(body []byte) ThinkingConfig { thinkingType := gjson.GetBytes(body, "thinking.type").String() if thinkingType == "disabled" { @@ -367,14 +368,44 @@ func extractClaudeConfig(body []byte) ThinkingConfig { } } - // If type="enabled" but no budget_tokens, treat as auto (user wants thinking but no budget specified) - if thinkingType == "enabled" { + // If type="enabled" or "adaptive" but no budget_tokens, check output_config.effort + // "adaptive" is a newer Anthropic API thinking type where the model decides the thinking budget + if thinkingType == "enabled" || thinkingType == "adaptive" { + // Check output_config.effort (Claude Opus 4.6+ adaptive thinking parameter) + if effort := gjson.GetBytes(body, "output_config.effort"); effort.Exists() && effort.String() != "" { + level := MapClaudeEffortToLevel(effort.String()) + if level != "" { + return ThinkingConfig{Mode: ModeLevel, Level: ThinkingLevel(level)} + } + } + if thinkingType == "adaptive" { + // "adaptive" without explicit effort defaults to high + return ThinkingConfig{Mode: ModeLevel, Level: LevelHigh} + } + // "enabled" without budget_tokens: preserve backward-compatible auto behavior return ThinkingConfig{Mode: ModeAuto, Budget: -1} } return ThinkingConfig{} } +// MapClaudeEffortToLevel maps Claude API output_config.effort values to internal ThinkingLevel strings. +// Claude effort levels: "max", "high", "medium", "low" +func MapClaudeEffortToLevel(effort string) string { + switch strings.ToLower(effort) { + case "max": + return string(LevelXHigh) + case "high": + return string(LevelHigh) + case "medium": + return string(LevelMedium) + case "low": + return string(LevelLow) + default: + return "" + } +} + // extractGeminiConfig extracts thinking configuration from Gemini format request body. // // Gemini API format: diff --git a/internal/thinking/strip.go b/internal/thinking/strip.go index eb6917150..c524795a4 100644 --- a/internal/thinking/strip.go +++ b/internal/thinking/strip.go @@ -30,7 +30,7 @@ func StripThinkingConfig(body []byte, provider string) []byte { var paths []string switch provider { case "claude": - paths = []string{"thinking"} + paths = []string{"thinking", "output_config"} case "gemini": paths = []string{"generationConfig.thinkingConfig"} case "gemini-cli", "antigravity": diff --git a/internal/translator/antigravity/claude/antigravity_claude_request.go b/internal/translator/antigravity/claude/antigravity_claude_request.go index 69ed42e12..d86799677 100644 --- a/internal/translator/antigravity/claude/antigravity_claude_request.go +++ b/internal/translator/antigravity/claude/antigravity_claude_request.go @@ -344,7 +344,11 @@ func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _ // Inject interleaved thinking hint when both tools and thinking are active hasTools := toolDeclCount > 0 thinkingResult := gjson.GetBytes(rawJSON, "thinking") - hasThinking := thinkingResult.Exists() && thinkingResult.IsObject() && thinkingResult.Get("type").String() == "enabled" + thinkingType := "" + if thinkingResult.Exists() && thinkingResult.IsObject() { + thinkingType = thinkingResult.Get("type").String() + } + hasThinking := thinkingType == "enabled" || thinkingType == "adaptive" isClaudeThinking := util.IsClaudeThinkingModel(modelName) if hasTools && hasThinking && isClaudeThinking { @@ -375,13 +379,28 @@ func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _ out, _ = sjson.SetRaw(out, "request.tools", toolsJSON) } - // Map Anthropic thinking -> Gemini thinkingBudget/include_thoughts when type==enabled + // Map Anthropic thinking -> Gemini thinkingBudget/include_thoughts when type==enabled or adaptive if t := gjson.GetBytes(rawJSON, "thinking"); enableThoughtTranslate && t.Exists() && t.IsObject() { - if t.Get("type").String() == "enabled" { + tType := t.Get("type").String() + if tType == "enabled" || tType == "adaptive" { if b := t.Get("budget_tokens"); b.Exists() && b.Type == gjson.Number { budget := int(b.Int()) out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.thinkingBudget", budget) out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.includeThoughts", true) + } else { + // No budget_tokens: signal auto so ApplyThinking resolves from model config + out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.thinkingBudget", -1) + out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.includeThoughts", true) + // adaptive without budget_tokens defaults to level "high" + if tType == "adaptive" { + out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.thinkingLevel", "high") + } + } + // output_config.effort overrides the default level — ApplyThinking converts to budget via model config + if effort := gjson.GetBytes(rawJSON, "output_config.effort"); effort.Exists() && effort.String() != "" { + if level := thinking.MapClaudeEffortToLevel(effort.String()); level != "" { + out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.thinkingLevel", level) + } } } } diff --git a/internal/translator/codex/claude/codex_claude_request.go b/internal/translator/codex/claude/codex_claude_request.go index d73207175..8b288109a 100644 --- a/internal/translator/codex/claude/codex_claude_request.go +++ b/internal/translator/codex/claude/codex_claude_request.go @@ -215,19 +215,33 @@ func ConvertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool) reasoningEffort := "medium" if thinkingConfig := rootResult.Get("thinking"); thinkingConfig.Exists() && thinkingConfig.IsObject() { switch thinkingConfig.Get("type").String() { - case "enabled": + case "enabled", "adaptive": if budgetTokens := thinkingConfig.Get("budget_tokens"); budgetTokens.Exists() { budget := int(budgetTokens.Int()) if effort, ok := thinking.ConvertBudgetToLevel(budget); ok && effort != "" { reasoningEffort = effort } + } else if thinkingConfig.Get("type").String() == "adaptive" { + // "adaptive" without budget_tokens: default to "high" + reasoningEffort = "high" } + // "enabled" without budget_tokens: keep default "medium" case "disabled": if effort, ok := thinking.ConvertBudgetToLevel(0); ok && effort != "" { reasoningEffort = effort } } } + // output_config.effort takes priority (Claude Opus 4.6+ adaptive thinking) + if effort := rootResult.Get("output_config.effort"); effort.Exists() && effort.String() != "" { + if mapped := thinking.MapClaudeEffortToLevel(effort.String()); mapped != "" { + // Cap xhigh to high — Codex only supports low/medium/high + if mapped == string(thinking.LevelXHigh) { + mapped = string(thinking.LevelHigh) + } + reasoningEffort = mapped + } + } template, _ = sjson.Set(template, "reasoning.effort", reasoningEffort) template, _ = sjson.Set(template, "reasoning.summary", "auto") template, _ = sjson.Set(template, "stream", true) 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 657d33c86..e67721314 100644 --- a/internal/translator/gemini-cli/claude/gemini-cli_claude_request.go +++ b/internal/translator/gemini-cli/claude/gemini-cli_claude_request.go @@ -9,6 +9,7 @@ import ( "bytes" "strings" + "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking" "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/common" "github.com/tidwall/gjson" "github.com/tidwall/sjson" @@ -171,13 +172,28 @@ func ConvertClaudeRequestToCLI(modelName string, inputRawJSON []byte, _ bool) [] } } - // Map Anthropic thinking -> Gemini thinkingBudget/include_thoughts when type==enabled + // Map Anthropic thinking -> Gemini thinkingBudget/include_thoughts when type==enabled or adaptive if t := gjson.GetBytes(rawJSON, "thinking"); t.Exists() && t.IsObject() { - if t.Get("type").String() == "enabled" { + tType := t.Get("type").String() + if tType == "enabled" || tType == "adaptive" { if b := t.Get("budget_tokens"); b.Exists() && b.Type == gjson.Number { budget := int(b.Int()) out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.thinkingBudget", budget) out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.includeThoughts", true) + } else { + // No budget_tokens: signal auto so ApplyThinking resolves from model config + out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.thinkingBudget", -1) + out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.includeThoughts", true) + // adaptive without budget_tokens defaults to level "high" + if tType == "adaptive" { + out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.thinkingLevel", "high") + } + } + // output_config.effort overrides the default level — ApplyThinking converts to budget via model config + if effort := gjson.GetBytes(rawJSON, "output_config.effort"); effort.Exists() && effort.String() != "" { + if level := thinking.MapClaudeEffortToLevel(effort.String()); level != "" { + out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.thinkingLevel", level) + } } } } diff --git a/internal/translator/gemini/claude/gemini_claude_request.go b/internal/translator/gemini/claude/gemini_claude_request.go index bab42952b..2c93cff06 100644 --- a/internal/translator/gemini/claude/gemini_claude_request.go +++ b/internal/translator/gemini/claude/gemini_claude_request.go @@ -9,6 +9,7 @@ import ( "bytes" "strings" + "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking" "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/common" "github.com/tidwall/gjson" "github.com/tidwall/sjson" @@ -151,14 +152,29 @@ func ConvertClaudeRequestToGemini(modelName string, inputRawJSON []byte, _ bool) } } - // Map Anthropic thinking -> Gemini thinkingBudget/include_thoughts when enabled + // Map Anthropic thinking -> Gemini thinkingBudget/include_thoughts when enabled or adaptive // Translator only does format conversion, ApplyThinking handles model capability validation. if t := gjson.GetBytes(rawJSON, "thinking"); t.Exists() && t.IsObject() { - if t.Get("type").String() == "enabled" { + tType := t.Get("type").String() + if tType == "enabled" || tType == "adaptive" { if b := t.Get("budget_tokens"); b.Exists() && b.Type == gjson.Number { budget := int(b.Int()) out, _ = sjson.Set(out, "generationConfig.thinkingConfig.thinkingBudget", budget) out, _ = sjson.Set(out, "generationConfig.thinkingConfig.includeThoughts", true) + } else { + // No budget_tokens: signal auto so ApplyThinking resolves from model config + out, _ = sjson.Set(out, "generationConfig.thinkingConfig.thinkingBudget", -1) + out, _ = sjson.Set(out, "generationConfig.thinkingConfig.includeThoughts", true) + // adaptive without budget_tokens defaults to level "high" + if tType == "adaptive" { + out, _ = sjson.Set(out, "generationConfig.thinkingConfig.thinkingLevel", "high") + } + } + // output_config.effort overrides the default level — ApplyThinking converts to budget via model config + if effort := gjson.GetBytes(rawJSON, "output_config.effort"); effort.Exists() && effort.String() != "" { + if level := thinking.MapClaudeEffortToLevel(effort.String()); level != "" { + out, _ = sjson.Set(out, "generationConfig.thinkingConfig.thinkingLevel", level) + } } } } diff --git a/internal/translator/openai/claude/openai_claude_request.go b/internal/translator/openai/claude/openai_claude_request.go index 1d9db94bd..619ed8bd9 100644 --- a/internal/translator/openai/claude/openai_claude_request.go +++ b/internal/translator/openai/claude/openai_claude_request.go @@ -63,14 +63,17 @@ func ConvertClaudeRequestToOpenAI(modelName string, inputRawJSON []byte, stream if thinkingConfig := root.Get("thinking"); thinkingConfig.Exists() && thinkingConfig.IsObject() { if thinkingType := thinkingConfig.Get("type"); thinkingType.Exists() { switch thinkingType.String() { - case "enabled": + case "enabled", "adaptive": if budgetTokens := thinkingConfig.Get("budget_tokens"); budgetTokens.Exists() { budget := int(budgetTokens.Int()) if effort, ok := thinking.ConvertBudgetToLevel(budget); ok && effort != "" { out, _ = sjson.Set(out, "reasoning_effort", effort) } + } else if thinkingType.String() == "adaptive" { + // "adaptive" without budget_tokens: default to high + out, _ = sjson.Set(out, "reasoning_effort", "high") } else { - // No budget_tokens specified, default to "auto" for enabled thinking + // "enabled" without budget_tokens: preserve backward-compatible auto behavior if effort, ok := thinking.ConvertBudgetToLevel(-1); ok && effort != "" { out, _ = sjson.Set(out, "reasoning_effort", effort) } @@ -82,6 +85,16 @@ func ConvertClaudeRequestToOpenAI(modelName string, inputRawJSON []byte, stream } } } + // output_config.effort takes priority (Claude Opus 4.6+ adaptive thinking) + if effort := root.Get("output_config.effort"); effort.Exists() && effort.String() != "" { + if mapped := thinking.MapClaudeEffortToLevel(effort.String()); mapped != "" { + // Cap xhigh to high — OpenAI only supports low/medium/high + if mapped == string(thinking.LevelXHigh) { + mapped = string(thinking.LevelHigh) + } + out, _ = sjson.Set(out, "reasoning_effort", mapped) + } + } // Process messages and system var messagesJSON = "[]"