Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 34 additions & 3 deletions internal/thinking/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -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" {
Expand All @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion internal/thinking/strip.go
Original file line number Diff line number Diff line change
Expand Up @@ -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":
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
}
}
}
}
Comment on lines +382 to 406
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

There's significant code duplication for handling Claude's thinking parameters across the Gemini-family translators (antigravity, gemini-cli, and gemini). This logic is nearly identical in all three files, with the only major difference being the JSON path prefix for setting the configuration.

To improve maintainability and reduce redundancy, consider extracting this logic into a shared helper function within the internal/translator/gemini/common package. This function could take the raw JSON, the output buffer, the path prefix, and the enableThoughtTranslate flag as arguments.

This would centralize the logic, making future updates easier and less error-prone.

Expand Down
16 changes: 15 additions & 1 deletion internal/translator/codex/claude/codex_claude_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Comment on lines 216 to +244
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

There's a bug in how the reasoningEffort is determined and capped. The current logic only caps the xhigh effort level to high when it's derived from output_config.effort. However, thinking.ConvertBudgetToLevel can also return xhigh for a large budget_tokens value, which is not supported by the Codex provider. This would result in an invalid xhigh value being sent.

I suggest refactoring this block to determine the reasoningEffort from both thinking and output_config.effort first, and then apply the capping logic once at the end before setting the value. This will fix the bug and also make the code cleaner.

if thinkingConfig := rootResult.Get("thinking"); thinkingConfig.Exists() && thinkingConfig.IsObject() {
		thinkingType := thinkingConfig.Get("type").String()
		switch thinkingType {
		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 thinkingType == "adaptive" {
				reasoningEffort = "high"
			}
			// "enabled" without budget_tokens keeps 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 != "" {
			reasoningEffort = mapped
		}
	}

	// Cap xhigh to high — Codex only supports low/medium/high
	if reasoningEffort == string(thinking.LevelXHigh) {
		reasoningEffort = string(thinking.LevelHigh)
	}

template, _ = sjson.Set(template, "reasoning.effort", reasoningEffort)
template, _ = sjson.Set(template, "reasoning.summary", "auto")
template, _ = sjson.Set(template, "stream", true)
Expand Down
20 changes: 18 additions & 2 deletions internal/translator/gemini-cli/claude/gemini-cli_claude_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
}
}
}
}
Expand Down
20 changes: 18 additions & 2 deletions internal/translator/gemini/claude/gemini_claude_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
}
}
}
}
Expand Down
17 changes: 15 additions & 2 deletions internal/translator/openai/claude/openai_claude_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand All @@ -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 = "[]"
Expand Down
Loading