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