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