diff --git a/internal/util/thinking.go b/internal/util/thinking.go index 37200980..bcf92c5b 100644 --- a/internal/util/thinking.go +++ b/internal/util/thinking.go @@ -113,3 +113,45 @@ func defaultReasoningLevel(levels []string) string { } return "" } + +// standardReasoningEfforts defines the canonical set of reasoning effort levels. +// This serves as the single source of truth for valid effort values. +var standardReasoningEfforts = []string{"none", "auto", "minimal", "low", "medium", "high", "xhigh"} + +// IsValidReasoningEffort checks if the given effort string is a valid reasoning effort level. +// This is a registry-independent check against the standard effort levels. +func IsValidReasoningEffort(effort string) bool { + if effort == "" { + return false + } + lowered := strings.ToLower(strings.TrimSpace(effort)) + for _, e := range standardReasoningEfforts { + if e == lowered { + return true + } + } + return false +} + +// NormalizeReasoningEffort normalizes a reasoning effort string to its canonical form. +// It first tries registry-based normalization if a model is provided, then falls back +// to the standard effort levels. Returns empty string and false if invalid. +func NormalizeReasoningEffort(model, effort string) (string, bool) { + if effort == "" { + return "", false + } + lowered := strings.ToLower(strings.TrimSpace(effort)) + + if model != "" { + if normalized, ok := NormalizeReasoningEffortLevel(model, effort); ok { + return normalized, true + } + } + + for _, e := range standardReasoningEfforts { + if e == lowered { + return e, true + } + } + return "", false +} diff --git a/internal/util/thinking_suffix.go b/internal/util/thinking_suffix.go index e3fd9136..1a1a8715 100644 --- a/internal/util/thinking_suffix.go +++ b/internal/util/thinking_suffix.go @@ -58,8 +58,9 @@ func NormalizeThinkingModel(modelName string) (string, map[string]any) { baseModel = modelName[:idx] budgetOverride = &parsed matched = true - } else if effort, okEffort := normalizeReasoningEffort(value); okEffort { + } else if IsValidReasoningEffort(value) { baseModel = modelName[:idx] + effort := strings.ToLower(strings.TrimSpace(value)) reasoningEffort = &effort matched = true } @@ -185,7 +186,9 @@ func ReasoningEffortFromMetadata(metadata map[string]any) (string, bool) { return "", false } if effort != nil && *effort != "" { - return *effort, true + if IsValidReasoningEffort(*effort) { + return strings.ToLower(strings.TrimSpace(*effort)), true + } } if budget != nil { switch *budget { @@ -207,7 +210,11 @@ func ThinkingEffortToBudget(model, effort string) (int, bool) { if effort == "" { return 0, false } - switch strings.ToLower(effort) { + normalized, ok := NormalizeReasoningEffort(model, effort) + if !ok { + return 0, false + } + switch normalized { case "none": return 0, true case "auto": @@ -312,16 +319,3 @@ func parseNumberToInt(raw any) (int, bool) { } return 0, false } - -func normalizeReasoningEffort(value string) (string, bool) { - if value == "" { - return "", false - } - effort := strings.ToLower(strings.TrimSpace(value)) - switch effort { - case "minimal", "low", "medium", "high", "xhigh", "auto", "none": - return effort, true - default: - return "", false - } -}