Publish Docker image (Multi-arch) / Build & push (amd64) (push) Failing after 1m40s
Release (Linux, macOS, Windows) / Linux Release (push) Failing after 42m16s
Publish Docker image (Multi-arch) / Build & push (arm64) (push) Canceled after 0s
Release (Linux, macOS, Windows) / macOS Release (push) Canceled after 0s
Release (Linux, macOS, Windows) / Windows Release (push) Canceled after 0s
Publish Docker image (Multi-arch) / Create multi-arch manifests (push) Canceled after 0s
Sync production source including routing, billing, Astra detection and public uptime. Update custom build instructions and license packaging.
387 lines
12 KiB
Go
387 lines
12 KiB
Go
package reasoning
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"strings"
|
|
|
|
"github.com/QuantumNous/new-api/relaykit/dto"
|
|
"github.com/QuantumNous/new-api/relaykit/types"
|
|
)
|
|
|
|
type ClaudeRender struct {
|
|
Thinking *dto.Thinking
|
|
OutputEffort Effort
|
|
EffectiveEffort Effort
|
|
ClearSampling bool
|
|
ConstrainThinkingSampling bool
|
|
// MaxTokens is set when manual thinking needed a larger max_tokens than
|
|
// the request carried; callers apply it to the outgoing request.
|
|
MaxTokens *uint
|
|
Diagnostics []types.ConversionDiagnostic
|
|
}
|
|
|
|
type claudeCapabilities struct {
|
|
adaptive bool
|
|
supportsManual bool
|
|
defaultThinking bool
|
|
supportsDisable bool
|
|
supportsEffort bool
|
|
supportsXHigh bool
|
|
supportsMax bool
|
|
strictSampling bool
|
|
}
|
|
|
|
func claudeCapabilitiesFor(model string) claudeCapabilities {
|
|
model = strings.ToLower(model)
|
|
capabilities := claudeCapabilities{supportsManual: true, supportsDisable: true}
|
|
|
|
switch {
|
|
case strings.HasPrefix(model, "claude-fable-5"),
|
|
strings.HasPrefix(model, "claude-mythos-5"):
|
|
capabilities.adaptive = true
|
|
capabilities.supportsManual = false
|
|
capabilities.defaultThinking = true
|
|
capabilities.supportsDisable = false
|
|
capabilities.supportsXHigh = true
|
|
capabilities.supportsMax = true
|
|
capabilities.strictSampling = true
|
|
case strings.HasPrefix(model, "claude-mythos-preview"):
|
|
capabilities.adaptive = true
|
|
capabilities.defaultThinking = true
|
|
capabilities.supportsDisable = false
|
|
capabilities.supportsMax = true
|
|
capabilities.strictSampling = true
|
|
case strings.HasPrefix(model, "claude-opus-5"),
|
|
strings.HasPrefix(model, "claude-sonnet-5"),
|
|
strings.HasPrefix(model, "claude-opus-4-8"),
|
|
strings.HasPrefix(model, "claude-opus-4-7"):
|
|
capabilities.adaptive = true
|
|
capabilities.supportsManual = false
|
|
if strings.HasPrefix(model, "claude-opus-5") || strings.HasPrefix(model, "claude-sonnet-5") {
|
|
capabilities.defaultThinking = true
|
|
}
|
|
capabilities.supportsEffort = true
|
|
capabilities.supportsXHigh = true
|
|
capabilities.supportsMax = true
|
|
capabilities.strictSampling = true
|
|
case strings.HasPrefix(model, "claude-opus-4-6"),
|
|
strings.HasPrefix(model, "claude-sonnet-4-6"):
|
|
capabilities.adaptive = true
|
|
capabilities.supportsEffort = true
|
|
capabilities.supportsMax = true
|
|
case strings.HasPrefix(model, "claude-opus-4-5"):
|
|
capabilities.supportsEffort = true
|
|
}
|
|
|
|
return capabilities
|
|
}
|
|
|
|
func RenderClaude(model string, intent Intent, maxTokens *uint, adapterBudgetPercentage float64) (ClaudeRender, error) {
|
|
disabledWithEffort := intent.Mode == ModeDisabled && intent.Effort != "" && intent.Effort != EffortNone
|
|
if disabledWithEffort {
|
|
effort, err := ParseEffort(string(intent.Effort))
|
|
if err != nil {
|
|
return ClaudeRender{}, err
|
|
}
|
|
intent.Effort = effort
|
|
}
|
|
diagnostics := make([]types.ConversionDiagnostic, 0, 1)
|
|
if !disabledWithEffort {
|
|
var normalizeDiagnostics []types.ConversionDiagnostic
|
|
var err error
|
|
intent, normalizeDiagnostics, err = normalizeIntent(intent)
|
|
if err != nil {
|
|
return ClaudeRender{}, err
|
|
}
|
|
diagnostics = append(diagnostics, normalizeDiagnostics...)
|
|
}
|
|
capabilities := claudeCapabilitiesFor(model)
|
|
if disabledWithEffort {
|
|
diagnostics = append(diagnostics, claudeReasoningDiagnostic(
|
|
"claude_disabled_effort_ignored",
|
|
fmt.Sprintf("model %q cannot apply effort %q while thinking is disabled; the effort was ignored", model, intent.Effort),
|
|
))
|
|
}
|
|
if !intent.HasStrength() {
|
|
if intent.IncludeThoughts != nil && capabilities.adaptive && capabilities.defaultThinking {
|
|
thinking := &dto.Thinking{Type: "adaptive"}
|
|
if *intent.IncludeThoughts {
|
|
thinking.Display = "summarized"
|
|
} else {
|
|
thinking.Display = "omitted"
|
|
}
|
|
return ClaudeRender{
|
|
Thinking: thinking,
|
|
EffectiveEffort: EffortHigh,
|
|
ClearSampling: capabilities.strictSampling,
|
|
}, nil
|
|
}
|
|
if capabilities.defaultThinking {
|
|
return ClaudeRender{EffectiveEffort: EffortHigh, ClearSampling: capabilities.strictSampling}, nil
|
|
}
|
|
return ClaudeRender{ClearSampling: capabilities.strictSampling}, nil
|
|
}
|
|
|
|
if intent.Mode == ModeDisabled || intent.Effort == EffortNone {
|
|
if !capabilities.supportsDisable {
|
|
diagnostics = append(diagnostics, claudeReasoningDiagnostic(
|
|
"claude_thinking_disable_unsupported",
|
|
fmt.Sprintf("model %q cannot disable thinking; using the lowest representable thinking mode", model),
|
|
))
|
|
if capabilities.adaptive {
|
|
thinking := &dto.Thinking{Type: "adaptive"}
|
|
if intent.IncludeThoughts != nil {
|
|
if *intent.IncludeThoughts {
|
|
thinking.Display = "summarized"
|
|
} else {
|
|
thinking.Display = "omitted"
|
|
}
|
|
}
|
|
outputEffort := Effort("")
|
|
effectiveEffort := EffortHigh
|
|
if capabilities.supportsEffort {
|
|
outputEffort = EffortLow
|
|
effectiveEffort = EffortLow
|
|
}
|
|
return ClaudeRender{
|
|
Thinking: thinking,
|
|
OutputEffort: outputEffort,
|
|
EffectiveEffort: effectiveEffort,
|
|
ClearSampling: capabilities.strictSampling,
|
|
Diagnostics: diagnostics,
|
|
}, nil
|
|
}
|
|
return ClaudeRender{Diagnostics: diagnostics}, nil
|
|
}
|
|
return ClaudeRender{
|
|
Thinking: &dto.Thinking{Type: "disabled"},
|
|
EffectiveEffort: EffortNone,
|
|
ClearSampling: capabilities.strictSampling,
|
|
Diagnostics: diagnostics,
|
|
}, nil
|
|
}
|
|
|
|
preferManual := capabilities.supportsManual && intent.BudgetTokens != nil && intent.Mode != ModeAdaptive
|
|
if !capabilities.supportsManual && intent.BudgetTokens != nil && intent.Mode == ModeEnabled {
|
|
diagnostics = append(diagnostics, claudeReasoningDiagnostic(
|
|
"claude_budget_to_adaptive",
|
|
fmt.Sprintf("model %q uses adaptive thinking; budget_tokens was converted to an effort level", model),
|
|
))
|
|
}
|
|
if capabilities.adaptive && !preferManual {
|
|
effort := intent.Effort
|
|
if effort == "" && intent.BudgetTokens != nil {
|
|
effort = EffortFromBudget(*intent.BudgetTokens)
|
|
}
|
|
if effort == "" && intent.Mode == ModeEnabled {
|
|
effort = EffortHigh
|
|
}
|
|
normalizedEffort := normalizeClaudeEffort(effort, capabilities)
|
|
if effort != "" && normalizedEffort != effort {
|
|
diagnostics = append(diagnostics, claudeReasoningDiagnostic(
|
|
"claude_effort_adjusted",
|
|
fmt.Sprintf("model %q does not support effort %q; using %q", model, effort, normalizedEffort),
|
|
))
|
|
}
|
|
effort = normalizedEffort
|
|
effectiveEffort := effort
|
|
if effectiveEffort == "" && intent.Mode == ModeAdaptive {
|
|
effectiveEffort = EffortHigh
|
|
}
|
|
|
|
// Claude effort can be used without enabling thinking. Preserve that
|
|
// distinction for native Claude requests; OpenAI extractors explicitly
|
|
// mark reasoning efforts as ModeEnabled.
|
|
if intent.Mode == ModeUnset {
|
|
return ClaudeRender{
|
|
OutputEffort: effort,
|
|
EffectiveEffort: effectiveEffort,
|
|
ClearSampling: capabilities.strictSampling,
|
|
Diagnostics: diagnostics,
|
|
}, nil
|
|
}
|
|
|
|
thinking := &dto.Thinking{Type: "adaptive"}
|
|
if intent.IncludeThoughts != nil {
|
|
if *intent.IncludeThoughts {
|
|
thinking.Display = "summarized"
|
|
} else {
|
|
thinking.Display = "omitted"
|
|
}
|
|
}
|
|
return ClaudeRender{
|
|
Thinking: thinking,
|
|
OutputEffort: effort,
|
|
EffectiveEffort: effectiveEffort,
|
|
ClearSampling: capabilities.strictSampling,
|
|
ConstrainThinkingSampling: !capabilities.strictSampling,
|
|
Diagnostics: diagnostics,
|
|
}, nil
|
|
}
|
|
|
|
if intent.Mode == ModeAdaptive {
|
|
diagnostics = append(diagnostics, claudeReasoningDiagnostic(
|
|
"claude_adaptive_to_manual",
|
|
fmt.Sprintf("model %q does not support adaptive thinking; using manual thinking", model),
|
|
))
|
|
intent.Mode = ModeEnabled
|
|
if intent.Effort == "" {
|
|
intent.Effort = EffortHigh
|
|
}
|
|
}
|
|
if intent.Mode == ModeUnset {
|
|
return ClaudeRender{OutputEffort: intent.Effort, EffectiveEffort: intent.Effort, Diagnostics: diagnostics}, nil
|
|
}
|
|
var raisedMaxTokens *uint
|
|
if maxTokens == nil || *maxTokens <= 1024 {
|
|
// Manual thinking needs room above the 1024-token minimum budget.
|
|
// Raising max_tokens keeps the request usable instead of rejecting it.
|
|
raised := uint(1280)
|
|
if intent.BudgetTokens != nil && *intent.BudgetTokens >= 0 && *intent.BudgetTokens < math.MaxInt32/2 {
|
|
raised = max(raised, uint(*intent.BudgetTokens)+1)
|
|
}
|
|
current := "a missing max_tokens"
|
|
if maxTokens != nil {
|
|
current = fmt.Sprintf("max_tokens %d", *maxTokens)
|
|
}
|
|
diagnostics = append(diagnostics, claudeReasoningDiagnostic(
|
|
"claude_max_tokens_raised",
|
|
fmt.Sprintf("model %q requires max_tokens greater than 1024 for manual thinking; raised %s to %d", model, current, raised),
|
|
))
|
|
raisedMaxTokens = &raised
|
|
maxTokens = &raised
|
|
}
|
|
if uint64(*maxTokens) > uint64(math.MaxInt) {
|
|
return ClaudeRender{}, fmt.Errorf("max_tokens is too large for a thinking budget")
|
|
}
|
|
|
|
budget := 0
|
|
if intent.BudgetTokens != nil && *intent.BudgetTokens >= 0 {
|
|
requestedBudget := *intent.BudgetTokens
|
|
budget = max(requestedBudget, 1024)
|
|
if uint(budget) >= *maxTokens {
|
|
budget = int(*maxTokens) - 1
|
|
}
|
|
if budget != requestedBudget {
|
|
diagnostics = append(diagnostics, claudeReasoningDiagnostic(
|
|
"claude_budget_adjusted",
|
|
fmt.Sprintf("model %q requires 1024 <= budget_tokens < max_tokens; adjusted %d to %d", model, requestedBudget, budget),
|
|
))
|
|
}
|
|
} else {
|
|
if intent.BudgetTokens != nil {
|
|
diagnostics = append(diagnostics, claudeReasoningDiagnostic(
|
|
"claude_dynamic_budget_converted",
|
|
fmt.Sprintf("model %q does not support a dynamic budget; derived a manual budget from reasoning effort", model),
|
|
))
|
|
}
|
|
percentage := effortPercentage(intent.Effort, adapterBudgetPercentage)
|
|
budget = max(int(*maxTokens)*percentage/100, 1024)
|
|
if uint(budget) >= *maxTokens {
|
|
budget = int(*maxTokens) - 1
|
|
}
|
|
}
|
|
|
|
effectiveEffort := intent.Effort
|
|
if intent.BudgetTokens != nil && !capabilities.supportsEffort {
|
|
effectiveEffort = EffortFromBudget(budget)
|
|
} else if effectiveEffort == "" {
|
|
effectiveEffort = EffortFromBudget(budget)
|
|
}
|
|
outputEffort := Effort("")
|
|
if capabilities.supportsEffort && intent.Effort != "" {
|
|
outputEffort = normalizeClaudeEffort(intent.Effort, capabilities)
|
|
effectiveEffort = outputEffort
|
|
}
|
|
thinking := &dto.Thinking{Type: "enabled", BudgetTokens: &budget}
|
|
if intent.IncludeThoughts != nil {
|
|
if *intent.IncludeThoughts {
|
|
thinking.Display = "summarized"
|
|
} else {
|
|
thinking.Display = "omitted"
|
|
}
|
|
}
|
|
return ClaudeRender{
|
|
Thinking: thinking,
|
|
OutputEffort: outputEffort,
|
|
EffectiveEffort: effectiveEffort,
|
|
ConstrainThinkingSampling: true,
|
|
MaxTokens: raisedMaxTokens,
|
|
Diagnostics: diagnostics,
|
|
}, nil
|
|
}
|
|
|
|
func claudeReasoningDiagnostic(code string, message string) types.ConversionDiagnostic {
|
|
return types.ConversionDiagnostic{
|
|
Code: code,
|
|
Path: "thinking",
|
|
Message: message,
|
|
Severity: types.ConversionDiagnosticWarning,
|
|
To: types.RelayFormatClaude,
|
|
}
|
|
}
|
|
|
|
// ClaudeUsesManualThinking reports whether an exact numeric budget is rendered
|
|
// as legacy extended thinking rather than being reduced to adaptive effort.
|
|
func ClaudeUsesManualThinking(model string, intent Intent) bool {
|
|
capabilities := claudeCapabilitiesFor(model)
|
|
return capabilities.supportsManual && intent.BudgetTokens != nil && intent.Mode != ModeAdaptive
|
|
}
|
|
|
|
func IsKnownClaudeModel(model string) bool {
|
|
return isKnownClaudeModel(model)
|
|
}
|
|
|
|
func ResolveClaudeDefault(model string, intent Intent) Intent {
|
|
if intent.HasStrength() || !claudeCapabilitiesFor(model).defaultThinking {
|
|
return intent
|
|
}
|
|
intent.Mode = ModeAdaptive
|
|
intent.Effort = EffortHigh
|
|
return intent
|
|
}
|
|
|
|
func normalizeClaudeEffort(effort Effort, capabilities claudeCapabilities) Effort {
|
|
switch effort {
|
|
case EffortMinimal:
|
|
return EffortLow
|
|
case EffortXHigh:
|
|
if capabilities.supportsXHigh {
|
|
return effort
|
|
}
|
|
if capabilities.supportsMax {
|
|
return EffortMax
|
|
}
|
|
return EffortHigh
|
|
case EffortMax:
|
|
if !capabilities.supportsMax {
|
|
return EffortHigh
|
|
}
|
|
}
|
|
return effort
|
|
}
|
|
|
|
func effortPercentage(effort Effort, adapterBudgetPercentage float64) int {
|
|
switch effort {
|
|
case EffortMinimal:
|
|
return 5
|
|
case EffortLow:
|
|
return 20
|
|
case EffortMedium:
|
|
return 50
|
|
case EffortHigh:
|
|
return 80
|
|
case EffortXHigh, EffortMax:
|
|
return 95
|
|
}
|
|
percentage := int(math.Round(adapterBudgetPercentage * 100))
|
|
if percentage <= 0 {
|
|
return 80
|
|
}
|
|
if percentage >= 100 {
|
|
return 99
|
|
}
|
|
return percentage
|
|
}
|