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 isGPT56OrGPT6Family(id) {
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// reasoningEffortForModel applies a model-specific wire effort mapping after
96// the requested level has been clamped to one the model exposes. The fallback
97// is the conservative protocol mapping for providers without exact metadata.
98func reasoningEffortForModel(model Model, requested, clamped, fallback string) string {
99 if wire, ok := model.ReasoningEffortMap[NormalizeReasoning(clamped)]; ok {
100 return wire
101 }
102 if hasReasoningLevelOverride(model, requested) {
103 return clamped
104 }
105 return fallback
106}
107
108// ClampReasoningForModel maps a configured level to the nearest level exposed
109// for the active model. Ties prefer the higher level.
110func ClampReasoningForModel(model Model, level string) string {
111 normalized := NormalizeReasoning(level)
112 available := AvailableReasoningLevels(model)
113 if mapped, overridden := model.ReasoningLevelMap[normalized]; overridden {
114 target := NormalizeReasoning(mapped)
115 if target != "" && containsReasoningLevel(available, target) {
116 return target
117 }
118 }
119 for _, candidate := range available {
120 if candidate == normalized {
121 return candidate
122 }
123 }
124 return nearestReasoningLevel(available, normalized)
125}
126
127func nearestReasoningLevel(available []string, requested string) string {
128 if requested == "" || len(available) == 1 {
129 return ""
130 }
131 requestedRank := reasoningLevelRank(requested)
132 if requestedRank == 0 {
133 return ""
134 }
135 best, bestDistance := available[1], len(reasoningLevelOrder)
136 for _, candidate := range available[1:] {
137 distance := reasoningLevelRank(candidate) - requestedRank
138 if distance < 0 {
139 distance = -distance
140 }
141 if distance <= bestDistance {
142 best, bestDistance = candidate, distance
143 }
144 }
145 return best
146}
147
148func reasoningLevelRank(level string) int {
149 for rank, candidate := range reasoningLevelOrder {
150 if candidate == level {
151 return rank
152 }
153 }
154 return 0
155}
156
157func usesReasoningBudget(model Model) bool {
158 switch model.Provider {
159 case "anthropic", "fireworks", "kimi", "minimax", "minimax-cn", "vercel-ai-gateway":
160 return true
161 }
162 return model.API == APIAnthropicMessages || model.API == "anthropic" // "anthropic" is the legacy user-model API value
163}
164
165// NormalizeReasoning canonicalizes zut's user-facing reasoning levels.
166// Empty string means reasoning is disabled. "maximum" remains
167// an alias for xhigh; "max" is the separate opt-in tier above it.
168func NormalizeReasoning(level string) string {
169 switch strings.ToLower(strings.TrimSpace(level)) {
170 case "", "off", "none", "no", "false", "disabled":
171 return ""
172 case "min", "minimal", "minimum":
173 return "minimum"
174 case "low":
175 return "low"
176 case "med", "medium":
177 return "medium"
178 case "hi", "high":
179 return "high"
180 case "xhigh", "maximum":
181 return "xhigh"
182 case "max":
183 return "max"
184 default:
185 return strings.ToLower(strings.TrimSpace(level))
186 }
187}
188
189// ReasoningBudget returns zut's approximate token budget for reasoning-capable
190// providers that accept explicit budgets.
191func ReasoningBudget(level string) int {
192 switch NormalizeReasoning(level) {
193 case "minimum":
194 return 1024
195 case "low":
196 return 2048
197 case "medium":
198 return 8192
199 case "high":
200 return 16384
201 case "xhigh", "max":
202 return 32768
203 default:
204 return 0
205 }
206}
207
208// AnthropicAdaptiveEffort maps zut's user-facing reasoning levels onto the
209// effort enum used by adaptive-thinking models. These models reject explicit
210// thinking budgets; reasoning depth is controlled by output_config.effort.
211func AnthropicAdaptiveEffort(level string) string {
212 switch NormalizeReasoning(level) {
213 case "minimum", "low":
214 return "low"
215 case "medium":
216 return "medium"
217 case "high":
218 return "high"
219 case "xhigh":
220 return "xhigh"
221 case "max":
222 return "max"
223 default:
224 return ""
225 }
226}
227
228// OpenAIReasoningEffort maps zut's thinking setting onto the effort enum
229// accepted by generic OpenAI-compatible chat-completions endpoints.
230func OpenAIReasoningEffort(level string) string {
231 switch NormalizeReasoning(level) {
232 case "minimum", "low":
233 // Many compatible endpoints only accept low/medium/high.
234 return "low"
235 case "medium":
236 return "medium"
237 case "high", "xhigh", "max":
238 return "high"
239 default:
240 return ""
241 }
242}
243
244// OpenAICompatAnthropicEffort maps zut's thinking setting when an adaptive
245// Anthropic model is served over an OpenAI-compatible chat-completions wire.
246// Adaptive models accept native xhigh and max effort values.
247func OpenAICompatAnthropicEffort(level string) string {
248 switch NormalizeReasoning(level) {
249 case "minimum", "low":
250 return "low"
251 case "medium":
252 return "medium"
253 case "high":
254 return "high"
255 case "xhigh":
256 return "xhigh"
257 case "max":
258 return "max"
259 default:
260 return ""
261 }
262}
263
264// isGPT56OrGPT6Family reports whether id belongs to the GPT-5.6 or GPT-6.x
265// families (for example gpt-6-astra or gpt-6.1-sol), which share native max
266// reasoning and explicit prompt-cache support.
267func isGPT56OrGPT6Family(id string) bool {
268 id = strings.ToLower(strings.TrimSpace(id))
269 return strings.HasPrefix(id, "gpt-5.6-") || strings.HasPrefix(id, "gpt-6-") || strings.HasPrefix(id, "gpt-6.")
270}
271
272// OpenAICodexReasoningEffort maps zut levels onto the Responses API effort
273// enum. GPT-5.6 and GPT-6 support native max; other models clamp to xhigh.
274func OpenAICodexReasoningEffort(level, model string) string {
275 switch NormalizeReasoning(level) {
276 case "minimum", "low":
277 return "low"
278 case "medium":
279 return "medium"
280 case "high":
281 return "high"
282 case "xhigh":
283 return "xhigh"
284 case "max":
285 if isGPT56OrGPT6Family(model) {
286 return "max"
287 }
288 return "xhigh"
289 default:
290 return ""
291 }
292}