1use std::fmt;
15
16#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
20pub enum ReasoningLevel {
21 Off,
23 Adaptive,
25 Low,
27 #[default]
29 Medium,
30 High,
32 XHigh,
34 Max,
36 Ultra,
38 UltraCode,
40}
41
42impl ReasoningLevel {
43 pub fn parse(s: &str) -> Option<Self> {
47 match s.trim().to_ascii_lowercase().as_str() {
48 "off" | "none" => Some(Self::Off),
49 "adaptive" => Some(Self::Adaptive),
50 "low" => Some(Self::Low),
51 "medium" | "med" => Some(Self::Medium),
52 "high" => Some(Self::High),
53 "xhigh" | "x-high" | "x_high" => Some(Self::XHigh),
54 "max" => Some(Self::Max),
55 "ultra" => Some(Self::Ultra),
56 "ultracode" => Some(Self::UltraCode),
57 _ => None,
58 }
59 }
60
61 pub fn as_str(self) -> &'static str {
66 match self {
67 Self::Off => "off",
68 Self::Adaptive => "adaptive",
69 Self::Low => "low",
70 Self::Medium => "medium",
71 Self::High => "high",
72 Self::XHigh => "xhigh",
73 Self::Max => "max",
74 Self::Ultra => "ultra",
75 Self::UltraCode => "ultracode",
76 }
77 }
78
79 pub fn to_legacy_budget(self) -> Option<u32> {
83 match self {
84 Self::Off | Self::Adaptive => Some(0),
85 Self::Low => Some(2048),
86 Self::Medium => Some(4096),
87 Self::High => Some(16384),
88 Self::XHigh => Some(32768),
89 Self::Max | Self::Ultra | Self::UltraCode => None,
90 }
91 }
92
93 pub fn from_legacy_budget(budget: u32) -> Self {
98 match budget {
99 0 => Self::Adaptive,
100 1..=2048 => Self::Low,
101 2049..=4096 => Self::Medium,
102 4097..=16384 => Self::High,
103 _ => Self::XHigh,
104 }
105 }
106
107 pub fn requires_codex_support(self) -> bool {
109 matches!(self, Self::Ultra)
110 }
111
112 pub fn requires_anthropic_support(self) -> bool {
114 matches!(self, Self::Max | Self::UltraCode)
115 }
116}
117
118impl fmt::Display for ReasoningLevel {
119 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
120 f.write_str(self.as_str())
121 }
122}
123
124#[derive(Debug, Clone, PartialEq, Eq)]
130pub enum ThinkingSpec {
131 Named(ReasoningLevel),
133 Budget(u32),
135}
136
137impl ThinkingSpec {
138 pub fn parse(s: &str) -> Option<Self> {
142 if let Some(level) = ReasoningLevel::parse(s) {
144 return Some(Self::Named(level));
145 }
146 if let Ok(n) = s.trim().parse::<u32>() {
148 return Some(Self::Budget(n));
149 }
150 None
151 }
152
153 pub fn to_level(&self) -> ReasoningLevel {
156 match self {
157 Self::Named(l) => *l,
158 Self::Budget(n) => ReasoningLevel::from_legacy_budget(*n),
159 }
160 }
161
162 pub fn to_budget(&self) -> Option<u32> {
165 match self {
166 Self::Named(l) => l.to_legacy_budget(),
167 Self::Budget(n) => Some(*n),
168 }
169 }
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175
176 #[test]
177 fn ultracode_is_a_distinct_canonical_non_numeric_level() {
178 let level = ReasoningLevel::parse("ultracode").expect("canonical ultracode");
179 assert_eq!(level, ReasoningLevel::UltraCode);
180 assert_eq!(level.to_string(), "ultracode");
181 assert_ne!(level, ReasoningLevel::Ultra);
182 assert_ne!(level, ReasoningLevel::Max);
183 assert_ne!(level, ReasoningLevel::XHigh);
184 assert_eq!(level.to_legacy_budget(), None);
185 assert!(!level.requires_codex_support());
186 assert!(level.requires_anthropic_support());
187 }
188
189 #[test]
192 fn all_canonical_strings_parse() {
193 for (s, expected) in [
194 ("off", ReasoningLevel::Off),
195 ("none", ReasoningLevel::Off),
196 ("adaptive", ReasoningLevel::Adaptive),
197 ("low", ReasoningLevel::Low),
198 ("medium", ReasoningLevel::Medium),
199 ("med", ReasoningLevel::Medium),
200 ("high", ReasoningLevel::High),
201 ("xhigh", ReasoningLevel::XHigh),
202 ("max", ReasoningLevel::Max),
203 ("ultra", ReasoningLevel::Ultra),
204 ("ultracode", ReasoningLevel::UltraCode),
205 ] {
206 assert_eq!(
207 ReasoningLevel::parse(s),
208 Some(expected),
209 "failed to parse {s:?}"
210 );
211 }
212 }
213
214 #[test]
215 fn case_insensitive_parse() {
216 assert_eq!(ReasoningLevel::parse("MAX"), Some(ReasoningLevel::Max));
217 assert_eq!(ReasoningLevel::parse("Ultra"), Some(ReasoningLevel::Ultra));
218 assert_eq!(ReasoningLevel::parse("XHIGH"), Some(ReasoningLevel::XHigh));
219 assert_eq!(ReasoningLevel::parse(" Low "), Some(ReasoningLevel::Low));
220 }
221
222 #[test]
223 fn unknown_strings_return_none() {
224 assert_eq!(ReasoningLevel::parse(""), None);
225 assert_eq!(ReasoningLevel::parse("bogus"), None);
226 assert_eq!(ReasoningLevel::parse("maximum"), None);
227 assert_eq!(ReasoningLevel::parse("hyper"), None);
228 assert_eq!(ReasoningLevel::parse("xhigh+"), None);
229 }
230
231 #[test]
232 fn as_str_round_trips() {
233 for level in [
234 ReasoningLevel::Off,
235 ReasoningLevel::Adaptive,
236 ReasoningLevel::Low,
237 ReasoningLevel::Medium,
238 ReasoningLevel::High,
239 ReasoningLevel::XHigh,
240 ReasoningLevel::Max,
241 ReasoningLevel::Ultra,
242 ReasoningLevel::UltraCode,
243 ] {
244 let s = level.as_str();
245 assert_eq!(
246 ReasoningLevel::parse(s),
247 Some(level),
248 "as_str() → parse() failed for {:?}",
249 level
250 );
251 }
252 }
253
254 #[test]
255 fn display_matches_as_str() {
256 assert_eq!(ReasoningLevel::Max.to_string(), "max");
257 assert_eq!(ReasoningLevel::Ultra.to_string(), "ultra");
258 assert_eq!(ReasoningLevel::XHigh.to_string(), "xhigh");
259 }
260
261 #[test]
264 fn max_is_distinct_from_xhigh() {
265 assert_ne!(ReasoningLevel::Max, ReasoningLevel::XHigh);
266 assert_ne!(ReasoningLevel::Max.as_str(), ReasoningLevel::XHigh.as_str());
267 }
268
269 #[test]
272 fn ultra_has_no_legacy_budget() {
273 assert_eq!(ReasoningLevel::Ultra.to_legacy_budget(), None);
274 }
275
276 #[test]
277 fn max_has_no_legacy_budget() {
278 assert_eq!(ReasoningLevel::Max.to_legacy_budget(), None);
279 }
280
281 #[test]
284 fn known_legacy_budgets_convert_correctly() {
285 assert_eq!(ReasoningLevel::Off.to_legacy_budget(), Some(0));
286 assert_eq!(ReasoningLevel::Adaptive.to_legacy_budget(), Some(0));
287 assert_eq!(ReasoningLevel::Low.to_legacy_budget(), Some(2048));
288 assert_eq!(ReasoningLevel::Medium.to_legacy_budget(), Some(4096));
289 assert_eq!(ReasoningLevel::High.to_legacy_budget(), Some(16384));
290 assert_eq!(ReasoningLevel::XHigh.to_legacy_budget(), Some(32768));
291 }
292
293 #[test]
294 fn from_legacy_budget_never_returns_max_or_ultra() {
295 for budget in [0u32, 2048, 4096, 16384, 32768, 65536, u32::MAX] {
296 let level = ReasoningLevel::from_legacy_budget(budget);
297 assert!(
298 !matches!(level, ReasoningLevel::Max | ReasoningLevel::Ultra),
299 "from_legacy_budget({budget}) returned {level:?} which must not be Max/Ultra"
300 );
301 }
302 }
303
304 #[test]
305 fn from_legacy_budget_round_trips_for_known_values() {
306 for (budget, expected) in [
307 (0u32, ReasoningLevel::Adaptive),
308 (2048, ReasoningLevel::Low),
309 (4096, ReasoningLevel::Medium),
310 (16384, ReasoningLevel::High),
311 (32768, ReasoningLevel::XHigh),
312 ] {
313 assert_eq!(
314 ReasoningLevel::from_legacy_budget(budget),
315 expected,
316 "budget {budget}"
317 );
318 }
319 }
320
321 #[test]
324 fn ordering_is_intensity() {
325 let levels = [
326 ReasoningLevel::Off,
327 ReasoningLevel::Adaptive,
328 ReasoningLevel::Low,
329 ReasoningLevel::Medium,
330 ReasoningLevel::High,
331 ReasoningLevel::XHigh,
332 ReasoningLevel::Max,
333 ReasoningLevel::Ultra,
334 ];
335 for w in levels.windows(2) {
336 assert!(w[0] < w[1], "{:?} should be less than {:?}", w[0], w[1]);
337 }
338 }
339
340 #[test]
343 fn only_ultra_requires_categorically_codex_support() {
344 assert!(!ReasoningLevel::Max.requires_codex_support());
345 assert!(ReasoningLevel::Ultra.requires_codex_support());
346 assert!(!ReasoningLevel::UltraCode.requires_codex_support());
347 assert!(!ReasoningLevel::XHigh.requires_codex_support());
348 assert!(!ReasoningLevel::High.requires_codex_support());
349 assert!(!ReasoningLevel::Adaptive.requires_codex_support());
350 }
351
352 #[test]
355 fn thinking_spec_named_levels_parse() {
356 assert_eq!(
357 ThinkingSpec::parse("max"),
358 Some(ThinkingSpec::Named(ReasoningLevel::Max))
359 );
360 assert_eq!(
361 ThinkingSpec::parse("ultra"),
362 Some(ThinkingSpec::Named(ReasoningLevel::Ultra))
363 );
364 assert_eq!(
365 ThinkingSpec::parse("xhigh"),
366 Some(ThinkingSpec::Named(ReasoningLevel::XHigh))
367 );
368 }
369
370 #[test]
371 fn thinking_spec_numeric_budget_parse() {
372 assert_eq!(
373 ThinkingSpec::parse("8192"),
374 Some(ThinkingSpec::Budget(8192))
375 );
376 assert_eq!(ThinkingSpec::parse("0"), Some(ThinkingSpec::Budget(0)));
377 }
378
379 #[test]
380 fn thinking_spec_parse_invalid() {
381 assert_eq!(ThinkingSpec::parse("bogus"), None);
382 assert_eq!(ThinkingSpec::parse(""), None);
383 }
384
385 #[test]
386 fn thinking_spec_max_has_no_budget() {
387 let spec = ThinkingSpec::Named(ReasoningLevel::Max);
388 assert_eq!(spec.to_budget(), None);
389 assert_eq!(spec.to_level(), ReasoningLevel::Max);
390 }
391
392 #[test]
393 fn thinking_spec_budget_round_trips() {
394 let spec = ThinkingSpec::Budget(8192);
395 assert_eq!(spec.to_budget(), Some(8192));
396 assert_eq!(spec.to_level(), ReasoningLevel::High);
398 }
399}