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 {
67 match self {
68 Self::Off => "off",
69 Self::Adaptive => "adaptive",
70 Self::Low => "low",
71 Self::Medium => "medium",
72 Self::High => "high",
73 Self::XHigh => "xhigh",
74 Self::Max => "max",
75 Self::Ultra => "ultra",
76 Self::UltraCode => "ultracode",
77 }
78 }
79
80 pub fn to_legacy_budget(self) -> Option<u32> {
84 match self {
85 Self::Off | Self::Adaptive => Some(0),
86 Self::Low => Some(2048),
87 Self::Medium => Some(4096),
88 Self::High => Some(16384),
89 Self::XHigh => Some(32768),
90 Self::Max | Self::Ultra | Self::UltraCode => None,
91 }
92 }
93
94 pub fn from_legacy_budget(budget: u32) -> Self {
99 match budget {
100 0 => Self::Adaptive,
101 1..=2048 => Self::Low,
102 2049..=4096 => Self::Medium,
103 4097..=16384 => Self::High,
104 _ => Self::XHigh,
105 }
106 }
107
108 pub fn requires_codex_support(self) -> bool {
110 matches!(self, Self::Ultra)
111 }
112
113 pub fn requires_anthropic_support(self) -> bool {
115 matches!(self, Self::Max | Self::UltraCode)
116 }
117}
118
119impl fmt::Display for ReasoningLevel {
120 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
121 f.write_str(self.as_str())
122 }
123}
124
125#[derive(Debug, Clone, PartialEq, Eq)]
131pub enum ThinkingSpec {
132 Named(ReasoningLevel),
134 Budget(u32),
136}
137
138impl ThinkingSpec {
139 pub fn parse(s: &str) -> Option<Self> {
143 if let Some(level) = ReasoningLevel::parse(s) {
145 return Some(Self::Named(level));
146 }
147 if let Ok(n) = s.trim().parse::<u32>() {
149 return Some(Self::Budget(n));
150 }
151 None
152 }
153
154 pub fn to_level(&self) -> ReasoningLevel {
157 match self {
158 Self::Named(l) => *l,
159 Self::Budget(n) => ReasoningLevel::from_legacy_budget(*n),
160 }
161 }
162
163 pub fn to_budget(&self) -> Option<u32> {
166 match self {
167 Self::Named(l) => l.to_legacy_budget(),
168 Self::Budget(n) => Some(*n),
169 }
170 }
171}
172
173pub fn parse_subagent_thinking(value: &str) -> Option<ReasoningLevel> {
177 match ReasoningLevel::parse(value)? {
178 level @ (ReasoningLevel::Low | ReasoningLevel::Medium | ReasoningLevel::High
179 | ReasoningLevel::XHigh | ReasoningLevel::Max) => Some(level),
180 _ => None,
181 }
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187
188 #[test]
189 fn ultracode_is_a_distinct_canonical_non_numeric_level() {
190 let level = ReasoningLevel::parse("ultracode").expect("canonical ultracode");
191 assert_eq!(level, ReasoningLevel::UltraCode);
192 assert_eq!(level.to_string(), "ultracode");
193 assert_ne!(level, ReasoningLevel::Ultra);
194 assert_ne!(level, ReasoningLevel::Max);
195 assert_ne!(level, ReasoningLevel::XHigh);
196 assert_eq!(level.to_legacy_budget(), None);
197 assert!(!level.requires_codex_support());
198 assert!(level.requires_anthropic_support());
199 }
200
201 #[test]
204 fn all_canonical_strings_parse() {
205 for (s, expected) in [
206 ("off", ReasoningLevel::Off),
207 ("none", ReasoningLevel::Off),
208 ("adaptive", ReasoningLevel::Adaptive),
209 ("low", ReasoningLevel::Low),
210 ("medium", ReasoningLevel::Medium),
211 ("med", ReasoningLevel::Medium),
212 ("high", ReasoningLevel::High),
213 ("xhigh", ReasoningLevel::XHigh),
214 ("max", ReasoningLevel::Max),
215 ("ultra", ReasoningLevel::Ultra),
216 ("ultracode", ReasoningLevel::UltraCode),
217 ] {
218 assert_eq!(
219 ReasoningLevel::parse(s),
220 Some(expected),
221 "failed to parse {s:?}"
222 );
223 }
224 }
225
226 #[test]
227 fn case_insensitive_parse() {
228 assert_eq!(ReasoningLevel::parse("MAX"), Some(ReasoningLevel::Max));
229 assert_eq!(ReasoningLevel::parse("Ultra"), Some(ReasoningLevel::Ultra));
230 assert_eq!(ReasoningLevel::parse("XHIGH"), Some(ReasoningLevel::XHigh));
231 assert_eq!(ReasoningLevel::parse(" Low "), Some(ReasoningLevel::Low));
232 }
233
234 #[test]
235 fn unknown_strings_return_none() {
236 assert_eq!(ReasoningLevel::parse(""), None);
237 assert_eq!(ReasoningLevel::parse("bogus"), None);
238 assert_eq!(ReasoningLevel::parse("maximum"), None);
239 assert_eq!(ReasoningLevel::parse("hyper"), None);
240 assert_eq!(ReasoningLevel::parse("xhigh+"), None);
241 }
242
243 #[test]
244 fn as_str_round_trips() {
245 for level in [
246 ReasoningLevel::Off,
247 ReasoningLevel::Adaptive,
248 ReasoningLevel::Low,
249 ReasoningLevel::Medium,
250 ReasoningLevel::High,
251 ReasoningLevel::XHigh,
252 ReasoningLevel::Max,
253 ReasoningLevel::Ultra,
254 ReasoningLevel::UltraCode,
255 ] {
256 let s = level.as_str();
257 assert_eq!(
258 ReasoningLevel::parse(s),
259 Some(level),
260 "as_str() → parse() failed for {:?}",
261 level
262 );
263 }
264 }
265
266 #[test]
267 fn display_matches_as_str() {
268 assert_eq!(ReasoningLevel::Max.to_string(), "max");
269 assert_eq!(ReasoningLevel::Ultra.to_string(), "ultra");
270 assert_eq!(ReasoningLevel::XHigh.to_string(), "xhigh");
271 }
272
273 #[test]
276 fn max_is_distinct_from_xhigh() {
277 assert_ne!(ReasoningLevel::Max, ReasoningLevel::XHigh);
278 assert_ne!(ReasoningLevel::Max.as_str(), ReasoningLevel::XHigh.as_str());
279 }
280
281 #[test]
284 fn ultra_has_no_legacy_budget() {
285 assert_eq!(ReasoningLevel::Ultra.to_legacy_budget(), None);
286 }
287
288 #[test]
289 fn max_has_no_legacy_budget() {
290 assert_eq!(ReasoningLevel::Max.to_legacy_budget(), None);
291 }
292
293 #[test]
296 fn known_legacy_budgets_convert_correctly() {
297 assert_eq!(ReasoningLevel::Off.to_legacy_budget(), Some(0));
298 assert_eq!(ReasoningLevel::Adaptive.to_legacy_budget(), Some(0));
299 assert_eq!(ReasoningLevel::Low.to_legacy_budget(), Some(2048));
300 assert_eq!(ReasoningLevel::Medium.to_legacy_budget(), Some(4096));
301 assert_eq!(ReasoningLevel::High.to_legacy_budget(), Some(16384));
302 assert_eq!(ReasoningLevel::XHigh.to_legacy_budget(), Some(32768));
303 }
304
305 #[test]
306 fn from_legacy_budget_never_returns_max_or_ultra() {
307 for budget in [0u32, 2048, 4096, 16384, 32768, 65536, u32::MAX] {
308 let level = ReasoningLevel::from_legacy_budget(budget);
309 assert!(
310 !matches!(level, ReasoningLevel::Max | ReasoningLevel::Ultra),
311 "from_legacy_budget({budget}) returned {level:?} which must not be Max/Ultra"
312 );
313 }
314 }
315
316 #[test]
317 fn from_legacy_budget_round_trips_for_known_values() {
318 for (budget, expected) in [
319 (0u32, ReasoningLevel::Adaptive),
320 (2048, ReasoningLevel::Low),
321 (4096, ReasoningLevel::Medium),
322 (16384, ReasoningLevel::High),
323 (32768, ReasoningLevel::XHigh),
324 ] {
325 assert_eq!(
326 ReasoningLevel::from_legacy_budget(budget),
327 expected,
328 "budget {budget}"
329 );
330 }
331 }
332
333 #[test]
336 fn ordering_is_intensity() {
337 let levels = [
338 ReasoningLevel::Off,
339 ReasoningLevel::Adaptive,
340 ReasoningLevel::Low,
341 ReasoningLevel::Medium,
342 ReasoningLevel::High,
343 ReasoningLevel::XHigh,
344 ReasoningLevel::Max,
345 ReasoningLevel::Ultra,
346 ];
347 for w in levels.windows(2) {
348 assert!(w[0] < w[1], "{:?} should be less than {:?}", w[0], w[1]);
349 }
350 }
351
352 #[test]
355 fn only_ultra_requires_categorically_codex_support() {
356 assert!(!ReasoningLevel::Max.requires_codex_support());
357 assert!(ReasoningLevel::Ultra.requires_codex_support());
358 assert!(!ReasoningLevel::UltraCode.requires_codex_support());
359 assert!(!ReasoningLevel::XHigh.requires_codex_support());
360 assert!(!ReasoningLevel::High.requires_codex_support());
361 assert!(!ReasoningLevel::Adaptive.requires_codex_support());
362 }
363
364 #[test]
367 fn thinking_spec_named_levels_parse() {
368 assert_eq!(
369 ThinkingSpec::parse("max"),
370 Some(ThinkingSpec::Named(ReasoningLevel::Max))
371 );
372 assert_eq!(
373 ThinkingSpec::parse("ultra"),
374 Some(ThinkingSpec::Named(ReasoningLevel::Ultra))
375 );
376 assert_eq!(
377 ThinkingSpec::parse("xhigh"),
378 Some(ThinkingSpec::Named(ReasoningLevel::XHigh))
379 );
380 }
381
382 #[test]
383 fn thinking_spec_numeric_budget_parse() {
384 assert_eq!(
385 ThinkingSpec::parse("8192"),
386 Some(ThinkingSpec::Budget(8192))
387 );
388 assert_eq!(ThinkingSpec::parse("0"), Some(ThinkingSpec::Budget(0)));
389 }
390
391 #[test]
392 fn thinking_spec_parse_invalid() {
393 assert_eq!(ThinkingSpec::parse("bogus"), None);
394 assert_eq!(ThinkingSpec::parse(""), None);
395 }
396
397 #[test]
398 fn thinking_spec_max_has_no_budget() {
399 let spec = ThinkingSpec::Named(ReasoningLevel::Max);
400 assert_eq!(spec.to_budget(), None);
401 assert_eq!(spec.to_level(), ReasoningLevel::Max);
402 }
403
404 #[test]
405 fn thinking_spec_budget_round_trips() {
406 let spec = ThinkingSpec::Budget(8192);
407 assert_eq!(spec.to_budget(), Some(8192));
408 assert_eq!(spec.to_level(), ReasoningLevel::High);
410 }
411}