feat(thinking): add HasLevel and MapToClaudeEffort functions for adaptive thinking support
This commit is contained in:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
Reference in New Issue
Block a user