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