From 0452b869e81198eee18fb90d8e74a09703edd634 Mon Sep 17 00:00:00 2001 From: hkfires <10558748+hkfires@users.noreply.github.com> Date: Tue, 3 Mar 2026 14:16:36 +0800 Subject: [PATCH] feat(thinking): add HasLevel and MapToClaudeEffort functions for adaptive thinking support --- internal/thinking/convert.go | 37 +++++++++++++++++++ internal/thinking/provider/codex/apply.go | 13 +------ internal/thinking/provider/openai/apply.go | 13 +------ .../claude/gemini/claude_gemini_request.go | 34 ++--------------- .../chat-completions/claude_openai_request.go | 25 ++----------- .../claude_openai-responses_request.go | 25 ++----------- 6 files changed, 48 insertions(+), 99 deletions(-) diff --git a/internal/thinking/convert.go b/internal/thinking/convert.go index 8374ddbb..89db7745 100644 --- a/internal/thinking/convert.go +++ b/internal/thinking/convert.go @@ -96,6 +96,43 @@ func ConvertBudgetToLevel(budget int) (string, bool) { } } +// HasLevel reports whether the given target level exists in the levels slice. +// Matching is case-insensitive with leading/trailing whitespace trimmed. +func HasLevel(levels []string, target string) bool { + for _, level := range levels { + if strings.EqualFold(strings.TrimSpace(level), target) { + return true + } + } + return false +} + +// MapToClaudeEffort maps a generic thinking level string to a Claude adaptive +// thinking effort value (low/medium/high/max). +// +// supportsMax indicates whether the target model supports "max" effort. +// Returns the mapped effort and true if the level is valid, or ("", false) otherwise. +func MapToClaudeEffort(level string, supportsMax bool) (string, bool) { + level = strings.ToLower(strings.TrimSpace(level)) + switch level { + case "": + return "", false + case "minimal": + return "low", true + case "low", "medium", "high": + return level, true + case "xhigh", "max": + if supportsMax { + return "max", true + } + return "high", true + case "auto": + return "high", true + default: + return "", false + } +} + // ModelCapability describes the thinking format support of a model. type ModelCapability int diff --git a/internal/thinking/provider/codex/apply.go b/internal/thinking/provider/codex/apply.go index 3bed318b..0f336359 100644 --- a/internal/thinking/provider/codex/apply.go +++ b/internal/thinking/provider/codex/apply.go @@ -7,8 +7,6 @@ package codex import ( - "strings" - "github.com/router-for-me/CLIProxyAPI/v6/internal/registry" "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking" "github.com/tidwall/gjson" @@ -68,7 +66,7 @@ func (a *Applier) Apply(body []byte, config thinking.ThinkingConfig, modelInfo * effort := "" support := modelInfo.Thinking if config.Budget == 0 { - if support.ZeroAllowed || hasLevel(support.Levels, string(thinking.LevelNone)) { + if support.ZeroAllowed || thinking.HasLevel(support.Levels, string(thinking.LevelNone)) { effort = string(thinking.LevelNone) } } @@ -120,12 +118,3 @@ func applyCompatibleCodex(body []byte, config thinking.ThinkingConfig) ([]byte, result, _ := sjson.SetBytes(body, "reasoning.effort", effort) return result, nil } - -func hasLevel(levels []string, target string) bool { - for _, level := range levels { - if strings.EqualFold(strings.TrimSpace(level), target) { - return true - } - } - return false -} diff --git a/internal/thinking/provider/openai/apply.go b/internal/thinking/provider/openai/apply.go index eaad30ee..c77c1ab8 100644 --- a/internal/thinking/provider/openai/apply.go +++ b/internal/thinking/provider/openai/apply.go @@ -6,8 +6,6 @@ package openai import ( - "strings" - "github.com/router-for-me/CLIProxyAPI/v6/internal/registry" "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking" "github.com/tidwall/gjson" @@ -65,7 +63,7 @@ func (a *Applier) Apply(body []byte, config thinking.ThinkingConfig, modelInfo * effort := "" support := modelInfo.Thinking if config.Budget == 0 { - if support.ZeroAllowed || hasLevel(support.Levels, string(thinking.LevelNone)) { + if support.ZeroAllowed || thinking.HasLevel(support.Levels, string(thinking.LevelNone)) { effort = string(thinking.LevelNone) } } @@ -117,12 +115,3 @@ func applyCompatibleOpenAI(body []byte, config thinking.ThinkingConfig) ([]byte, result, _ := sjson.SetBytes(body, "reasoning_effort", effort) return result, nil } - -func hasLevel(levels []string, target string) bool { - for _, level := range levels { - if strings.EqualFold(strings.TrimSpace(level), target) { - return true - } - } - return false -} diff --git a/internal/translator/claude/gemini/claude_gemini_request.go b/internal/translator/claude/gemini/claude_gemini_request.go index 2d2fee50..66914462 100644 --- a/internal/translator/claude/gemini/claude_gemini_request.go +++ b/internal/translator/claude/gemini/claude_gemini_request.go @@ -116,37 +116,9 @@ func ConvertGeminiRequestToClaude(modelName string, inputRawJSON []byte, stream // Include thoughts configuration for reasoning process visibility // Translator only does format conversion, ApplyThinking handles model capability validation. if thinkingConfig := genConfig.Get("thinkingConfig"); thinkingConfig.Exists() && thinkingConfig.IsObject() { - hasLevel := func(levels []string, target string) bool { - for _, level := range levels { - if strings.EqualFold(strings.TrimSpace(level), target) { - return true - } - } - return false - } mi := registry.LookupModelInfo(modelName, "claude") supportsAdaptive := mi != nil && mi.Thinking != nil && len(mi.Thinking.Levels) > 0 - supportsMax := supportsAdaptive && hasLevel(mi.Thinking.Levels, "max") - mapToEffort := func(level string) (string, bool) { - level = strings.ToLower(strings.TrimSpace(level)) - switch level { - case "": - return "", false - case "minimal": - return "low", true - case "low", "medium", "high": - return level, true - case "xhigh", "max": - if supportsMax { - return "max", true - } - return "high", true - case "auto": - return "high", true - default: - return "", false - } - } + supportsMax := supportsAdaptive && thinking.HasLevel(mi.Thinking.Levels, string(thinking.LevelMax)) thinkingLevel := thinkingConfig.Get("thinkingLevel") if !thinkingLevel.Exists() { @@ -162,7 +134,7 @@ func ConvertGeminiRequestToClaude(modelName string, inputRawJSON []byte, stream out, _ = sjson.Delete(out, "thinking.budget_tokens") out, _ = sjson.Delete(out, "output_config.effort") default: - effort, ok := mapToEffort(level) + effort, ok := thinking.MapToClaudeEffort(level, supportsMax) if ok { out, _ = sjson.Set(out, "thinking.type", "adaptive") out, _ = sjson.Delete(out, "thinking.budget_tokens") @@ -201,7 +173,7 @@ func ConvertGeminiRequestToClaude(modelName string, inputRawJSON []byte, stream default: level, ok := thinking.ConvertBudgetToLevel(budget) if ok { - effort, ok := mapToEffort(level) + effort, ok := thinking.MapToClaudeEffort(level, supportsMax) if ok { out, _ = sjson.Set(out, "thinking.type", "adaptive") out, _ = sjson.Delete(out, "thinking.budget_tokens") diff --git a/internal/translator/claude/openai/chat-completions/claude_openai_request.go b/internal/translator/claude/openai/chat-completions/claude_openai_request.go index 7155d1e0..2706a73e 100644 --- a/internal/translator/claude/openai/chat-completions/claude_openai_request.go +++ b/internal/translator/claude/openai/chat-completions/claude_openai_request.go @@ -69,17 +69,9 @@ func ConvertOpenAIRequestToClaude(modelName string, inputRawJSON []byte, stream if v := root.Get("reasoning_effort"); v.Exists() { effort := strings.ToLower(strings.TrimSpace(v.String())) if effort != "" { - hasLevel := func(levels []string, target string) bool { - for _, level := range levels { - if strings.EqualFold(strings.TrimSpace(level), target) { - return true - } - } - return false - } mi := registry.LookupModelInfo(modelName, "claude") supportsAdaptive := mi != nil && mi.Thinking != nil && len(mi.Thinking.Levels) > 0 - supportsMax := supportsAdaptive && hasLevel(mi.Thinking.Levels, "max") + supportsMax := supportsAdaptive && thinking.HasLevel(mi.Thinking.Levels, string(thinking.LevelMax)) // Claude 4.6 supports adaptive thinking with output_config.effort. if supportsAdaptive { @@ -94,19 +86,8 @@ func ConvertOpenAIRequestToClaude(modelName string, inputRawJSON []byte, stream out, _ = sjson.Delete(out, "output_config.effort") default: // Map non-Claude effort levels into Claude 4.6 effort vocabulary. - switch effort { - case "minimal": - effort = "low" - case "xhigh": - if supportsMax { - effort = "max" - } else { - effort = "high" - } - case "max": - if !supportsMax { - effort = "high" - } + if mapped, ok := thinking.MapToClaudeEffort(effort, supportsMax); ok { + effort = mapped } out, _ = sjson.Set(out, "thinking.type", "adaptive") out, _ = sjson.Delete(out, "thinking.budget_tokens") diff --git a/internal/translator/claude/openai/responses/claude_openai-responses_request.go b/internal/translator/claude/openai/responses/claude_openai-responses_request.go index cd1b8885..9e8f28da 100644 --- a/internal/translator/claude/openai/responses/claude_openai-responses_request.go +++ b/internal/translator/claude/openai/responses/claude_openai-responses_request.go @@ -57,17 +57,9 @@ func ConvertOpenAIResponsesRequestToClaude(modelName string, inputRawJSON []byte if v := root.Get("reasoning.effort"); v.Exists() { effort := strings.ToLower(strings.TrimSpace(v.String())) if effort != "" { - hasLevel := func(levels []string, target string) bool { - for _, level := range levels { - if strings.EqualFold(strings.TrimSpace(level), target) { - return true - } - } - return false - } mi := registry.LookupModelInfo(modelName, "claude") supportsAdaptive := mi != nil && mi.Thinking != nil && len(mi.Thinking.Levels) > 0 - supportsMax := supportsAdaptive && hasLevel(mi.Thinking.Levels, "max") + supportsMax := supportsAdaptive && thinking.HasLevel(mi.Thinking.Levels, string(thinking.LevelMax)) // Claude 4.6 supports adaptive thinking with output_config.effort. if supportsAdaptive { @@ -82,19 +74,8 @@ func ConvertOpenAIResponsesRequestToClaude(modelName string, inputRawJSON []byte out, _ = sjson.Delete(out, "output_config.effort") default: // Map non-Claude effort levels into Claude 4.6 effort vocabulary. - switch effort { - case "minimal": - effort = "low" - case "xhigh": - if supportsMax { - effort = "max" - } else { - effort = "high" - } - case "max": - if !supportsMax { - effort = "high" - } + if mapped, ok := thinking.MapToClaudeEffort(effort, supportsMax); ok { + effort = mapped } out, _ = sjson.Set(out, "thinking.type", "adaptive") out, _ = sjson.Delete(out, "thinking.budget_tokens")