diff --git a/internal/thinking/provider/openai/apply.go b/internal/thinking/provider/openai/apply.go index eaad30ee..e8a2562f 100644 --- a/internal/thinking/provider/openai/apply.go +++ b/internal/thinking/provider/openai/apply.go @@ -10,10 +10,53 @@ import ( "github.com/router-for-me/CLIProxyAPI/v6/internal/registry" "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking" + log "github.com/sirupsen/logrus" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) +// validReasoningEffortLevels contains the standard values accepted by the +// OpenAI reasoning_effort field. Provider-specific extensions (xhigh, minimal, +// auto) are NOT in this set and must be clamped before use. +var validReasoningEffortLevels = map[string]struct{}{ + "none": {}, + "low": {}, + "medium": {}, + "high": {}, +} + +// clampReasoningEffort maps any thinking level string to a value that is safe +// to send as OpenAI reasoning_effort. Non-standard CPA-internal values are +// mapped to the nearest standard equivalent. +// +// Mapping rules: +// - none / low / medium / high → returned as-is (already valid) +// - xhigh → "high" (nearest lower standard level) +// - minimal → "low" (nearest higher standard level) +// - auto → "medium" (reasonable default) +// - anything else → "medium" (safe default) +func clampReasoningEffort(level string) string { + if _, ok := validReasoningEffortLevels[level]; ok { + return level + } + var clamped string + switch level { + case string(thinking.LevelXHigh): + clamped = string(thinking.LevelHigh) + case string(thinking.LevelMinimal): + clamped = string(thinking.LevelLow) + case string(thinking.LevelAuto): + clamped = string(thinking.LevelMedium) + default: + clamped = string(thinking.LevelMedium) + } + log.WithFields(log.Fields{ + "original": level, + "clamped": clamped, + }).Debug("openai: reasoning_effort clamped to nearest valid standard value") + return clamped +} + // Applier implements thinking.ProviderApplier for OpenAI models. // // OpenAI-specific behavior: @@ -58,7 +101,7 @@ func (a *Applier) Apply(body []byte, config thinking.ThinkingConfig, modelInfo * } if config.Mode == thinking.ModeLevel { - result, _ := sjson.SetBytes(body, "reasoning_effort", string(config.Level)) + result, _ := sjson.SetBytes(body, "reasoning_effort", clampReasoningEffort(string(config.Level))) return result, nil } @@ -79,7 +122,7 @@ func (a *Applier) Apply(body []byte, config thinking.ThinkingConfig, modelInfo * return body, nil } - result, _ := sjson.SetBytes(body, "reasoning_effort", effort) + result, _ := sjson.SetBytes(body, "reasoning_effort", clampReasoningEffort(effort)) return result, nil } @@ -114,7 +157,7 @@ func applyCompatibleOpenAI(body []byte, config thinking.ThinkingConfig) ([]byte, return body, nil } - result, _ := sjson.SetBytes(body, "reasoning_effort", effort) + result, _ := sjson.SetBytes(body, "reasoning_effort", clampReasoningEffort(effort)) return result, nil }