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