feat(thinking): add HasLevel and MapToClaudeEffort functions for adaptive thinking support

This commit is contained in:
hkfires
2026-03-03 14:16:36 +08:00
parent d2e5857b82
commit 0452b869e8
6 changed files with 48 additions and 99 deletions
+37
View File
@@ -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. // ModelCapability describes the thinking format support of a model.
type ModelCapability int type ModelCapability int
+1 -12
View File
@@ -7,8 +7,6 @@
package codex package codex
import ( import (
"strings"
"github.com/router-for-me/CLIProxyAPI/v6/internal/registry" "github.com/router-for-me/CLIProxyAPI/v6/internal/registry"
"github.com/router-for-me/CLIProxyAPI/v6/internal/thinking" "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking"
"github.com/tidwall/gjson" "github.com/tidwall/gjson"
@@ -68,7 +66,7 @@ func (a *Applier) Apply(body []byte, config thinking.ThinkingConfig, modelInfo *
effort := "" effort := ""
support := modelInfo.Thinking support := modelInfo.Thinking
if config.Budget == 0 { 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) effort = string(thinking.LevelNone)
} }
} }
@@ -120,12 +118,3 @@ func applyCompatibleCodex(body []byte, config thinking.ThinkingConfig) ([]byte,
result, _ := sjson.SetBytes(body, "reasoning.effort", effort) result, _ := sjson.SetBytes(body, "reasoning.effort", effort)
return result, nil 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
}
+1 -12
View File
@@ -6,8 +6,6 @@
package openai package openai
import ( import (
"strings"
"github.com/router-for-me/CLIProxyAPI/v6/internal/registry" "github.com/router-for-me/CLIProxyAPI/v6/internal/registry"
"github.com/router-for-me/CLIProxyAPI/v6/internal/thinking" "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking"
"github.com/tidwall/gjson" "github.com/tidwall/gjson"
@@ -65,7 +63,7 @@ func (a *Applier) Apply(body []byte, config thinking.ThinkingConfig, modelInfo *
effort := "" effort := ""
support := modelInfo.Thinking support := modelInfo.Thinking
if config.Budget == 0 { 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) effort = string(thinking.LevelNone)
} }
} }
@@ -117,12 +115,3 @@ func applyCompatibleOpenAI(body []byte, config thinking.ThinkingConfig) ([]byte,
result, _ := sjson.SetBytes(body, "reasoning_effort", effort) result, _ := sjson.SetBytes(body, "reasoning_effort", effort)
return result, nil 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
}
@@ -116,37 +116,9 @@ func ConvertGeminiRequestToClaude(modelName string, inputRawJSON []byte, stream
// Include thoughts configuration for reasoning process visibility // Include thoughts configuration for reasoning process visibility
// Translator only does format conversion, ApplyThinking handles model capability validation. // Translator only does format conversion, ApplyThinking handles model capability validation.
if thinkingConfig := genConfig.Get("thinkingConfig"); thinkingConfig.Exists() && thinkingConfig.IsObject() { 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") mi := registry.LookupModelInfo(modelName, "claude")
supportsAdaptive := mi != nil && mi.Thinking != nil && len(mi.Thinking.Levels) > 0 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))
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
}
}
thinkingLevel := thinkingConfig.Get("thinkingLevel") thinkingLevel := thinkingConfig.Get("thinkingLevel")
if !thinkingLevel.Exists() { 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, "thinking.budget_tokens")
out, _ = sjson.Delete(out, "output_config.effort") out, _ = sjson.Delete(out, "output_config.effort")
default: default:
effort, ok := mapToEffort(level) effort, ok := thinking.MapToClaudeEffort(level, supportsMax)
if ok { if ok {
out, _ = sjson.Set(out, "thinking.type", "adaptive") out, _ = sjson.Set(out, "thinking.type", "adaptive")
out, _ = sjson.Delete(out, "thinking.budget_tokens") out, _ = sjson.Delete(out, "thinking.budget_tokens")
@@ -201,7 +173,7 @@ func ConvertGeminiRequestToClaude(modelName string, inputRawJSON []byte, stream
default: default:
level, ok := thinking.ConvertBudgetToLevel(budget) level, ok := thinking.ConvertBudgetToLevel(budget)
if ok { if ok {
effort, ok := mapToEffort(level) effort, ok := thinking.MapToClaudeEffort(level, supportsMax)
if ok { if ok {
out, _ = sjson.Set(out, "thinking.type", "adaptive") out, _ = sjson.Set(out, "thinking.type", "adaptive")
out, _ = sjson.Delete(out, "thinking.budget_tokens") out, _ = sjson.Delete(out, "thinking.budget_tokens")
@@ -69,17 +69,9 @@ func ConvertOpenAIRequestToClaude(modelName string, inputRawJSON []byte, stream
if v := root.Get("reasoning_effort"); v.Exists() { if v := root.Get("reasoning_effort"); v.Exists() {
effort := strings.ToLower(strings.TrimSpace(v.String())) effort := strings.ToLower(strings.TrimSpace(v.String()))
if effort != "" { 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") mi := registry.LookupModelInfo(modelName, "claude")
supportsAdaptive := mi != nil && mi.Thinking != nil && len(mi.Thinking.Levels) > 0 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. // Claude 4.6 supports adaptive thinking with output_config.effort.
if supportsAdaptive { if supportsAdaptive {
@@ -94,19 +86,8 @@ func ConvertOpenAIRequestToClaude(modelName string, inputRawJSON []byte, stream
out, _ = sjson.Delete(out, "output_config.effort") out, _ = sjson.Delete(out, "output_config.effort")
default: default:
// Map non-Claude effort levels into Claude 4.6 effort vocabulary. // Map non-Claude effort levels into Claude 4.6 effort vocabulary.
switch effort { if mapped, ok := thinking.MapToClaudeEffort(effort, supportsMax); ok {
case "minimal": effort = mapped
effort = "low"
case "xhigh":
if supportsMax {
effort = "max"
} else {
effort = "high"
}
case "max":
if !supportsMax {
effort = "high"
}
} }
out, _ = sjson.Set(out, "thinking.type", "adaptive") out, _ = sjson.Set(out, "thinking.type", "adaptive")
out, _ = sjson.Delete(out, "thinking.budget_tokens") out, _ = sjson.Delete(out, "thinking.budget_tokens")
@@ -57,17 +57,9 @@ func ConvertOpenAIResponsesRequestToClaude(modelName string, inputRawJSON []byte
if v := root.Get("reasoning.effort"); v.Exists() { if v := root.Get("reasoning.effort"); v.Exists() {
effort := strings.ToLower(strings.TrimSpace(v.String())) effort := strings.ToLower(strings.TrimSpace(v.String()))
if effort != "" { 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") mi := registry.LookupModelInfo(modelName, "claude")
supportsAdaptive := mi != nil && mi.Thinking != nil && len(mi.Thinking.Levels) > 0 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. // Claude 4.6 supports adaptive thinking with output_config.effort.
if supportsAdaptive { if supportsAdaptive {
@@ -82,19 +74,8 @@ func ConvertOpenAIResponsesRequestToClaude(modelName string, inputRawJSON []byte
out, _ = sjson.Delete(out, "output_config.effort") out, _ = sjson.Delete(out, "output_config.effort")
default: default:
// Map non-Claude effort levels into Claude 4.6 effort vocabulary. // Map non-Claude effort levels into Claude 4.6 effort vocabulary.
switch effort { if mapped, ok := thinking.MapToClaudeEffort(effort, supportsMax); ok {
case "minimal": effort = mapped
effort = "low"
case "xhigh":
if supportsMax {
effort = "max"
} else {
effort = "high"
}
case "max":
if !supportsMax {
effort = "high"
}
} }
out, _ = sjson.Set(out, "thinking.type", "adaptive") out, _ = sjson.Set(out, "thinking.type", "adaptive")
out, _ = sjson.Delete(out, "thinking.budget_tokens") out, _ = sjson.Delete(out, "thinking.budget_tokens")