1package provider
2
3import "strings"
4
5var reasoningLevelOrder = []string{"", "minimum", "low", "medium", "high", "xhigh", "max"}
6
7// AvailableReasoningLevels returns the distinct reasoning levels supported by
8// a model. Optional per-model overrides can remove, remap, or extend protocol
9// defaults. The empty string represents off.
10func AvailableReasoningLevels(model Model) []string {
11 defaults := defaultReasoningLevels(model)
12 if !model.Reasoning || len(model.ReasoningLevelMap) == 0 {
13 return defaults
14 }
15
16 available := map[string]bool{"": true}
17 for _, level := range reasoningLevelOrder[1:] {
18 if !containsReasoningLevel(defaults, level) {
19 if _, overridden := model.ReasoningLevelMap[level]; !overridden {
20 continue
21 }
22 }
23 effective := level
24 if mapped, overridden := model.ReasoningLevelMap[level]; overridden {
25 effective = NormalizeReasoning(mapped)
26 }
27 if reasoningLevelRank(effective) > 0 {
28 available[effective] = true
29 }
30 }
31
32 levels := []string{""}
33 for _, level := range reasoningLevelOrder[1:] {
34 if available[level] {
35 levels = append(levels, level)
36 }
37 }
38 return levels
39}
40
41func defaultReasoningLevels(model Model) []string {
42 if !model.Reasoning {
43 return []string{""}
44 }
45
46 id := strings.ToLower(model.ID)
47 if (model.Provider == "google" || model.Provider == "google-vertex") && strings.Contains(id, "gemini-3") {
48 if strings.Contains(id, "-pro") {
49 return []string{"", "low", "high"}
50 }
51 return []string{"", "minimum", "low", "medium", "high"}
52 }
53 if model.AdaptiveThinkingCompat {
54 return []string{"", "high"}
55 }
56 if model.AdaptiveThinking {
57 return []string{"", "low", "medium", "high", "xhigh", "max"}
58 }
59 if model.API == APIResponses || model.Provider == "openai-codex" || model.Provider == "openai-responses" || model.Provider == "azure-openai-responses" {
60 levels := []string{"", "low", "medium", "high", "xhigh"}
61 if strings.HasPrefix(id, "gpt-5.6-") {
62 levels = append(levels, "max")
63 }
64 return levels
65 }
66 if model.Provider == "google" || model.Provider == "google-vertex" {
67 if strings.Contains(id, "gemini-2.5") {
68 return []string{"", "minimum", "low", "medium", "high", "xhigh"}
69 }
70 return []string{""}
71 }
72 if usesReasoningBudget(model) {
73 return []string{"", "minimum", "low", "medium", "high", "xhigh"}
74 }
75 if model.Provider == "amazon-bedrock" {
76 return []string{""}
77 }
78 return []string{"", "low", "medium", "high"}
79}
80
81func containsReasoningLevel(levels []string, target string) bool {
82 for _, level := range levels {
83 if level == target {
84 return true
85 }
86 }
87 return false
88}
89
90func hasReasoningLevelOverride(model Model, level string) bool {
91 _, ok := model.ReasoningLevelMap[NormalizeReasoning(level)]
92 return ok
93}
94
95// ClampReasoningForModel maps a configured level to the nearest level exposed
96// for the active model. Ties prefer the higher level.
97func ClampReasoningForModel(model Model, level string) string {
98 normalized := NormalizeReasoning(level)
99 available := AvailableReasoningLevels(model)
100 if mapped, overridden := model.ReasoningLevelMap[normalized]; overridden {
101 target := NormalizeReasoning(mapped)
102 if target != "" && containsReasoningLevel(available, target) {
103 return target
104 }
105 }
106 for _, candidate := range available {
107 if candidate == normalized {
108 return candidate
109 }
110 }
111 return nearestReasoningLevel(available, normalized)
112}
113
114func nearestReasoningLevel(available []string, requested string) string {
115 if requested == "" || len(available) == 1 {
116 return ""
117 }
118 requestedRank := reasoningLevelRank(requested)
119 if requestedRank == 0 {
120 return ""
121 }
122 best, bestDistance := available[1], len(reasoningLevelOrder)
123 for _, candidate := range available[1:] {
124 distance := reasoningLevelRank(candidate) - requestedRank
125 if distance < 0 {
126 distance = -distance
127 }
128 if distance <= bestDistance {
129 best, bestDistance = candidate, distance
130 }
131 }
132 return best
133}
134
135func reasoningLevelRank(level string) int {
136 for rank, candidate := range reasoningLevelOrder {
137 if candidate == level {
138 return rank
139 }
140 }
141 return 0
142}
143
144func usesReasoningBudget(model Model) bool {
145 switch model.Provider {
146 case "anthropic", "fireworks", "kimi", "minimax", "minimax-cn", "vercel-ai-gateway":
147 return true
148 }
149 return model.API == "anthropic"
150}
151
152// NormalizeReasoning canonicalizes zut's user-facing reasoning levels.
153// Empty string means reasoning is disabled. "maximum" remains
154// an alias for xhigh; "max" is the separate opt-in tier above it.
155func NormalizeReasoning(level string) string {
156 switch strings.ToLower(strings.TrimSpace(level)) {
157 case "", "off", "none", "no", "false", "disabled":
158 return ""
159 case "min", "minimal", "minimum":
160 return "minimum"
161 case "low":
162 return "low"
163 case "med", "medium":
164 return "medium"
165 case "hi", "high":
166 return "high"
167 case "xhigh", "maximum":
168 return "xhigh"
169 case "max":
170 return "max"
171 default:
172 return strings.ToLower(strings.TrimSpace(level))
173 }
174}
175
176// ReasoningBudget returns zut's approximate token budget for reasoning-capable
177// providers that accept explicit budgets.
178func ReasoningBudget(level string) int {
179 switch NormalizeReasoning(level) {
180 case "minimum":
181 return 1024
182 case "low":
183 return 2048
184 case "medium":
185 return 8192
186 case "high":
187 return 16384
188 case "xhigh", "max":
189 return 32768
190 default:
191 return 0
192 }
193}
194
195// AnthropicAdaptiveEffort maps zut's user-facing reasoning levels onto the
196// effort enum used by adaptive-thinking models. These models reject explicit
197// thinking budgets; reasoning depth is controlled by output_config.effort.
198func AnthropicAdaptiveEffort(level string) string {
199 switch NormalizeReasoning(level) {
200 case "minimum", "low":
201 return "low"
202 case "medium":
203 return "medium"
204 case "high":
205 return "high"
206 case "xhigh":
207 return "xhigh"
208 case "max":
209 return "max"
210 default:
211 return ""
212 }
213}
214
215// OpenAIReasoningEffort maps zut's thinking setting onto the effort enum
216// accepted by generic OpenAI-compatible chat-completions endpoints.
217func OpenAIReasoningEffort(level string) string {
218 switch NormalizeReasoning(level) {
219 case "minimum", "low":
220 // Many compatible endpoints only accept low/medium/high.
221 return "low"
222 case "medium":
223 return "medium"
224 case "high", "xhigh", "max":
225 return "high"
226 default:
227 return ""
228 }
229}
230
231// OpenAICompatAnthropicEffort maps zut's thinking setting when an adaptive
232// Anthropic model is served over an OpenAI-compatible chat-completions wire.
233// Adaptive models accept native xhigh and max effort values.
234func OpenAICompatAnthropicEffort(level string) string {
235 switch NormalizeReasoning(level) {
236 case "minimum", "low":
237 return "low"
238 case "medium":
239 return "medium"
240 case "high":
241 return "high"
242 case "xhigh":
243 return "xhigh"
244 case "max":
245 return "max"
246 default:
247 return ""
248 }
249}
250
251// OpenAICodexReasoningEffort maps zut levels onto the Responses API effort
252// enum. GPT-5.6 supports native max; other models clamp max to xhigh.
253func OpenAICodexReasoningEffort(level, model string) string {
254 switch NormalizeReasoning(level) {
255 case "minimum", "low":
256 return "low"
257 case "medium":
258 return "medium"
259 case "high":
260 return "high"
261 case "xhigh":
262 return "xhigh"
263 case "max":
264 if strings.HasPrefix(strings.ToLower(model), "gpt-5.6-") {
265 return "max"
266 }
267 return "xhigh"
268 default:
269 return ""
270 }
271}