1use agent_core::reasoning::ReasoningLevel;
12
13use super::{
14 anthropic_static_capability, capability_cache, codex_static_capability, kimi_code_capability,
15 kimi_static_capability, muse_default_level, muse_routable_id, muse_supported_levels,
16 plan_codex_execution, xai_static_capability, CatalogProviderKind, CodexMultiAgentVersion,
17 CodexRequestRole, KimiReasoningCapability, ReasoningSupport, XaiReasoningCapability,
18};
19
20const CONSERVATIVE_OPTIONS: &[&str] = &["off", "adaptive", "low", "medium", "high", "xhigh"];
23
24fn kimi_route_capability(model: &str) -> Option<Option<KimiReasoningCapability>> {
29 if let Some(model_id) = model.strip_prefix("kimi/") {
30 return Some(kimi_static_capability(model_id));
31 }
32 if let Some(model_id) = model.strip_prefix("kimi-code/") {
33 return Some(kimi_code_capability(model_id));
34 }
35 None
36}
37
38fn anthropic_model_id(model: &str) -> Option<&str> {
39 model
40 .strip_prefix("anthropic/")
41 .or_else(|| model.starts_with("claude-").then_some(model))
42}
43
44fn unsupported_msg(level: ReasoningLevel, model: &str, supported: &[ReasoningLevel]) -> String {
45 format!(
46 "reasoning level '{level}' is not supported by {model}; supported: [{}]",
47 supported
48 .iter()
49 .map(|l| l.as_str())
50 .collect::<Vec<_>>()
51 .join(", ")
52 )
53}
54
55fn codex_reasoning_capability(model: &str, model_id: &str) -> Option<ReasoningSupport> {
56 if let Some(model) = capability_cache::get(model) {
59 if model.provider_kind != CatalogProviderKind::OpenAiCodex {
60 return None;
61 }
62 return matches!(model.reasoning, ReasoningSupport::CodexNamed { .. })
63 .then_some(model.reasoning);
64 }
65 codex_static_capability(model_id)
66}
67
68fn codex_supported_levels(model: &str, model_id: &str) -> Option<Vec<ReasoningLevel>> {
69 match codex_reasoning_capability(model, model_id)? {
70 ReasoningSupport::CodexNamed {
71 mut supported,
72 multi_agent_version,
73 ..
74 } => {
75 if multi_agent_version != Some(CodexMultiAgentVersion::V2) {
76 supported.retain(|level| *level != ReasoningLevel::Ultra);
77 }
78 Some(supported)
79 }
80 _ => None,
81 }
82}
83
84fn anthropic_capability(model_id: &str) -> Option<ReasoningSupport> {
85 capability_cache::get(&format!("anthropic/{model_id}"))
89 .map(|m| m.reasoning)
90 .filter(|r| !matches!(r, ReasoningSupport::Unknown))
91 .or_else(|| anthropic_static_capability(model_id))
92}
93
94pub fn validate_reasoning_mutation(model: &str, level: ReasoningLevel) -> Result<(), String> {
98 if let Some(model_id) = model.strip_prefix("muse/") {
99 if !muse_routable_id(model_id) {
102 return Err(format!(
103 "no capability metadata for {model}; cannot authorize level '{level}'"
104 ));
105 }
106 if level == ReasoningLevel::Adaptive {
107 return Ok(());
108 }
109 return match muse_supported_levels(model_id) {
110 Some(levels) if levels.contains(&level) => Ok(()),
111 Some(levels) => Err(unsupported_msg(level, model, &levels)),
112 None => Err(format!(
113 "{model} has no advertised effort levels (live Muse catalog not loaded, or the \
114 model has none); level '{level}' cannot be sent — use adaptive"
115 )),
116 };
117 }
118 if let Some(model_id) = model.strip_prefix("openai-codex/") {
119 if matches!(level, ReasoningLevel::Off | ReasoningLevel::Adaptive) {
121 return Ok(());
122 }
123 if matches!(level, ReasoningLevel::Max | ReasoningLevel::Ultra) {
124 return plan_codex_execution(model, level, CodexRequestRole::Foreground, None)
125 .map(|_| ())
126 .map_err(|error| error.to_string());
127 }
128 return match codex_supported_levels(model, model_id) {
129 Some(levels) if levels.contains(&level) => Ok(()),
130 Some(levels) => Err(unsupported_msg(level, model, &levels)),
131 None if level.requires_codex_support() => Err(format!(
134 "no capability metadata for {model}; cannot authorize level '{level}'"
135 )),
136 None => Ok(()),
137 };
138 }
139 if let Some(model_id) = model.strip_prefix("xai-auth/") {
140 if level == ReasoningLevel::Adaptive {
142 return Ok(());
143 }
144 return match xai_static_capability(model_id) {
145 Some(XaiReasoningCapability::Effort {
146 supported,
147 can_disable,
148 ..
149 }) => match level {
150 ReasoningLevel::Off if can_disable => Ok(()),
151 ReasoningLevel::Off => Err(format!(
152 "reasoning cannot be disabled on {model}; use adaptive or a supported effort"
153 )),
154 l if supported.contains(&l) => Ok(()),
155 l => Err(unsupported_msg(l, model, supported)),
156 },
157 Some(XaiReasoningCapability::IntrinsicReasoning) => match level {
158 ReasoningLevel::Off => Err(format!(
159 "{model} has no documented way to disable reasoning; use adaptive"
160 )),
161 l => Err(format!(
162 "{model} has no documented effort control; level '{l}' cannot be sent — use adaptive"
163 )),
164 },
165 Some(XaiReasoningCapability::NonReasoning) => match level {
166 ReasoningLevel::Off => Ok(()),
167 l => Err(format!(
168 "{model} is a non-reasoning model; level '{l}' is not supported"
169 )),
170 },
171 None => Err(format!(
172 "no capability metadata for {model}; cannot authorize level '{level}'"
173 )),
174 };
175 }
176 if let Some(capability) = kimi_route_capability(model) {
177 if level == ReasoningLevel::Adaptive {
179 return Ok(());
180 }
181 return match capability {
182 Some(KimiReasoningCapability::Effort {
183 supported,
184 can_disable,
185 ..
186 }) => match level {
187 ReasoningLevel::Off if can_disable => Ok(()),
188 ReasoningLevel::Off => Err(format!(
189 "reasoning cannot be disabled on {model}; use adaptive or a supported effort"
190 )),
191 l if supported.contains(&l) => Ok(()),
192 l => Err(unsupported_msg(l, model, supported)),
193 },
194 Some(KimiReasoningCapability::AlwaysThinking) => match level {
195 ReasoningLevel::Off => Err(format!(
196 "{model} always thinks and the API rejects disabling; use adaptive"
197 )),
198 l => Err(format!(
199 "{model} has no documented effort control; level '{l}' cannot be sent — use adaptive"
200 )),
201 },
202 Some(KimiReasoningCapability::ToggleableThinking) => match level {
203 ReasoningLevel::Off => Ok(()),
204 l => Err(format!(
205 "{model} supports only a thinking on/off toggle; level '{l}' cannot be sent — use off or adaptive"
206 )),
207 },
208 None => Err(format!(
209 "no capability metadata for {model}; cannot authorize level '{level}'"
210 )),
211 };
212 }
213 if let Some(model_id) = anthropic_model_id(model) {
214 if matches!(level, ReasoningLevel::Max | ReasoningLevel::UltraCode) {
215 let capabilities = model
218 .starts_with("anthropic/")
219 .then(|| super::anthropic_mode_capabilities(model))
220 .flatten()
221 .map(|caps| {
222 caps.narrow_with_live_effort(capability_cache::get(model).and_then(|entry| {
223 match entry.reasoning {
224 ReasoningSupport::AnthropicAdaptive { adaptive } => Some(adaptive),
225 ReasoningSupport::None => Some(false),
226 _ => None,
227 }
228 }))
229 });
230 let supported = match level {
231 ReasoningLevel::Max => capabilities.is_some_and(|caps| caps.max_supported),
232 ReasoningLevel::UltraCode => {
233 capabilities.is_some_and(|caps| caps.ultracode_supported())
234 }
235 _ => unreachable!(),
236 };
237 return supported
238 .then_some(())
239 .ok_or_else(|| format!("reasoning level '{level}' is not supported by {model}"));
240 }
241 if level == ReasoningLevel::Ultra {
242 return Err(format!(
243 "reasoning level '{level}' requires authoritative exact-model capability metadata; {model} has none"
244 ));
245 }
246 if matches!(anthropic_capability(model_id), Some(ReasoningSupport::None))
249 && !matches!(level, ReasoningLevel::Off | ReasoningLevel::Adaptive)
250 {
251 return Err(format!(
252 "{model} does not support extended thinking; level '{level}' is not available"
253 ));
254 }
255 return Ok(());
256 }
257 if level.requires_codex_support() {
259 return Err(format!(
260 "reasoning level '{level}' requires authoritative exact-model capability metadata; {model} has none"
261 ));
262 }
263 Ok(())
264}
265
266pub fn default_level_for_model(model: &str) -> Option<ReasoningLevel> {
269 if let Some(model_id) = model.strip_prefix("muse/") {
270 return Some(muse_default_level(model_id).unwrap_or(ReasoningLevel::Adaptive));
273 }
274 if let Some(model_id) = model.strip_prefix("openai-codex/") {
275 return match codex_reasoning_capability(model, model_id)? {
276 ReasoningSupport::CodexNamed { default_level, .. } => default_level,
277 _ => None,
278 };
279 }
280 if let Some(model_id) = model.strip_prefix("xai-auth/") {
281 return Some(match xai_static_capability(model_id) {
282 Some(XaiReasoningCapability::Effort {
283 default_level: Some(level),
284 ..
285 }) => level,
286 _ => ReasoningLevel::Adaptive,
289 });
290 }
291 if let Some(capability) = kimi_route_capability(model) {
292 return Some(match capability {
293 Some(KimiReasoningCapability::Effort {
294 default_level: Some(level),
295 ..
296 }) => level,
297 _ => ReasoningLevel::Adaptive,
299 });
300 }
301 None
302}
303
304pub fn reasoning_type_for_model(model: &str) -> &'static str {
309 if let Some(model_id) = model.strip_prefix("muse/") {
310 return if muse_supported_levels(model_id).is_some() {
311 "effort (named)"
312 } else {
313 "unknown"
314 };
315 }
316 if model.strip_prefix("openai-codex/").is_some() {
317 return "effort (named)";
319 }
320 if let Some(model_id) = model.strip_prefix("xai-auth/") {
321 return match xai_static_capability(model_id) {
322 Some(XaiReasoningCapability::Effort { .. }) => "effort",
323 Some(XaiReasoningCapability::IntrinsicReasoning) => "intrinsic",
324 Some(XaiReasoningCapability::NonReasoning) => "none",
325 None => "unknown",
326 };
327 }
328 if let Some(capability) = kimi_route_capability(model) {
329 return match capability {
330 Some(KimiReasoningCapability::Effort { .. }) => "effort",
331 Some(KimiReasoningCapability::AlwaysThinking) => "intrinsic",
332 Some(KimiReasoningCapability::ToggleableThinking) => "toggle (on/off)",
333 None => "unknown",
334 };
335 }
336 if let Some(model_id) = anthropic_model_id(model) {
337 return match anthropic_capability(model_id) {
338 Some(ReasoningSupport::AnthropicAdaptive { adaptive: true }) => "adaptive",
339 Some(ReasoningSupport::None) => "none",
340 _ => "budget (legacy)",
343 };
344 }
345 "unverified"
346}
347
348pub fn thinking_options_for_model(model: &str) -> Vec<String> {
351 let owned = |slice: &[&str]| slice.iter().map(|s| s.to_string()).collect::<Vec<_>>();
352 if let Some(model_id) = model.strip_prefix("muse/") {
353 let mut opts = vec!["adaptive".to_string()];
356 if let Some(levels) = muse_supported_levels(model_id) {
357 if levels.contains(&ReasoningLevel::Off) {
358 opts.insert(0, "off".to_string());
359 }
360 opts.extend(
361 levels
362 .iter()
363 .filter(|l| **l != ReasoningLevel::Off)
364 .map(|l| l.as_str().to_string()),
365 );
366 }
367 return opts;
368 }
369 if let Some(model_id) = model.strip_prefix("openai-codex/") {
370 if let Some(levels) = codex_supported_levels(model, model_id) {
371 let mut opts = vec!["off".to_string(), "adaptive".to_string()];
372 opts.extend(levels.iter().map(|l| l.as_str().to_string()));
373 return opts;
374 }
375 return owned(CONSERVATIVE_OPTIONS);
376 }
377 if let Some(model_id) = model.strip_prefix("xai-auth/") {
378 return match xai_static_capability(model_id) {
379 Some(XaiReasoningCapability::Effort {
380 supported,
381 can_disable,
382 ..
383 }) => {
384 let mut opts = Vec::new();
385 if can_disable {
386 opts.push("off".to_string());
387 }
388 opts.push("adaptive".to_string());
389 opts.extend(supported.iter().map(|l| l.as_str().to_string()));
390 opts
391 }
392 Some(XaiReasoningCapability::NonReasoning) => owned(&["off", "adaptive"]),
393 _ => owned(&["adaptive"]),
396 };
397 }
398 if let Some(capability) = kimi_route_capability(model) {
399 return match capability {
400 Some(KimiReasoningCapability::Effort {
401 supported,
402 can_disable,
403 ..
404 }) => {
405 let mut opts = Vec::new();
406 if can_disable {
407 opts.push("off".to_string());
408 }
409 opts.push("adaptive".to_string());
410 opts.extend(supported.iter().map(|l| l.as_str().to_string()));
411 opts
412 }
413 Some(KimiReasoningCapability::ToggleableThinking) => owned(&["off", "adaptive"]),
414 _ => owned(&["adaptive"]),
417 };
418 }
419 if let Some(model_id) = anthropic_model_id(model) {
420 let live = model
421 .starts_with("anthropic/")
422 .then(|| capability_cache::get(model))
423 .flatten()
424 .map(|entry| entry.reasoning);
425 let specials_authorized = super::anthropic_mode_capabilities(model).is_some()
426 && !matches!(live, Some(ReasoningSupport::None));
427 return match anthropic_capability(model_id) {
428 Some(ReasoningSupport::AnthropicAdaptive { .. }) if specials_authorized => owned(&[
431 "off",
432 "adaptive",
433 "low",
434 "medium",
435 "high",
436 "xhigh",
437 "max",
438 "ultracode",
439 ]),
440 Some(ReasoningSupport::AnthropicAdaptive { .. }) => owned(CONSERVATIVE_OPTIONS),
441 Some(ReasoningSupport::None) => owned(&["off", "adaptive"]),
443 _ if specials_authorized => owned(&[
444 "off",
445 "adaptive",
446 "low",
447 "medium",
448 "high",
449 "xhigh",
450 "max",
451 "ultracode",
452 ]),
453 _ => owned(CONSERVATIVE_OPTIONS),
454 };
455 }
456 owned(CONSERVATIVE_OPTIONS)
457}
458
459#[cfg(test)]
460mod tests {
461 use super::*;
462 use ReasoningLevel::*;
463
464 #[test]
465 fn muse_levels_follow_exact_live_catalog_and_fail_closed_otherwise() {
466 let _guard = crate::runtime::openai::catalog::muse::test_cache_guard();
467 let seed = "muse/muse-wonder-1.0";
469 assert!(validate_reasoning_mutation(seed, Adaptive).is_ok());
470 for level in [Off, Low, High, Max, Ultra] {
471 assert!(validate_reasoning_mutation(seed, level).is_err(), "{level}");
472 }
473 assert_eq!(thinking_options_for_model(seed), ["adaptive"]);
474 assert_eq!(default_level_for_model(seed), Some(Adaptive));
475 assert!(validate_reasoning_mutation("muse/never-cataloged-zz9", Adaptive).is_ok());
477 assert!(validate_reasoning_mutation("muse/never-cataloged-zz9", High).is_err());
478 assert!(validate_reasoning_mutation("muse/Bad Id", Adaptive).is_err());
480
481 let models = crate::runtime::openai::catalog::parse_muse_catalog_models(
483 r#"{"models":[{"id":"muse-validation-probe-zz1","reasoning_effort_variants":
484 [{"effort":"none"},{"effort":"low"},{"effort":"high"}]}]}"#,
485 )
486 .unwrap();
487 capability_cache::replace_provider("muse", &models);
488 let live = "muse/muse-validation-probe-zz1";
489 for level in [Adaptive, Off, Low, High] {
490 assert!(validate_reasoning_mutation(live, level).is_ok(), "{level}");
491 }
492 for level in [Medium, XHigh, Max, Ultra, UltraCode] {
493 assert!(validate_reasoning_mutation(live, level).is_err(), "{level}");
494 }
495 assert_eq!(
496 thinking_options_for_model(live),
497 ["off", "adaptive", "low", "high"]
498 );
499 assert_eq!(reasoning_type_for_model(live), "effort (named)");
500 capability_cache::replace_provider("muse", &[]);
501 }
502
503 #[test]
504 fn fable_options_expose_distinct_special_modes() {
505 let model = "anthropic/claude-fable-5";
506 assert_eq!(
507 thinking_options_for_model(model),
508 [
509 "off",
510 "adaptive",
511 "low",
512 "medium",
513 "high",
514 "xhigh",
515 "max",
516 "ultracode"
517 ]
518 );
519 }
520
521 #[test]
524 fn xai_45_and_46_accept_exact_efforts_and_reject_off_xhigh_max_ultra() {
525 for model in [
526 "xai-auth/grok-4.5",
527 "xai-auth/grok-4.5-latest",
528 "xai-auth/grok-4.6",
529 ] {
530 for level in [Adaptive, Low, Medium, High] {
531 assert!(
532 validate_reasoning_mutation(model, level).is_ok(),
533 "{model} {level}"
534 );
535 }
536 for level in [Off, XHigh, Max, Ultra] {
537 let err = validate_reasoning_mutation(model, level).unwrap_err();
538 assert!(err.contains(model), "{err}");
539 }
540 }
541 }
542
543 #[test]
544 fn xai_multi_agent_accepts_xhigh_but_not_off() {
545 let model = "xai-auth/grok-4.20-multi-agent-0309";
546 for level in [Adaptive, Low, Medium, High, XHigh] {
547 assert!(validate_reasoning_mutation(model, level).is_ok(), "{level}");
548 }
549 for level in [Off, Max, Ultra] {
550 assert!(
551 validate_reasoning_mutation(model, level).is_err(),
552 "{level}"
553 );
554 }
555 }
556
557 #[test]
558 fn xai_intrinsic_reasoning_rejects_named_and_off_accepts_adaptive() {
559 for id in [
560 "grok-4.3",
561 "grok-4.3-latest",
562 "grok-latest",
563 "grok-4.20-0309-reasoning",
564 ] {
565 let model = format!("xai-auth/{id}");
566 assert!(validate_reasoning_mutation(&model, Adaptive).is_ok());
567 for level in [Off, Low, Medium, High, XHigh, Max, Ultra] {
568 assert!(
569 validate_reasoning_mutation(&model, level).is_err(),
570 "{model} {level}"
571 );
572 }
573 }
574 }
575
576 #[test]
577 fn xai_non_reasoning_rejects_named_accepts_off_adaptive() {
578 let model = "xai-auth/grok-4.20-0309-non-reasoning";
579 assert!(validate_reasoning_mutation(model, Off).is_ok());
580 assert!(validate_reasoning_mutation(model, Adaptive).is_ok());
581 for level in [Low, Medium, High, XHigh, Max, Ultra] {
582 assert!(
583 validate_reasoning_mutation(model, level).is_err(),
584 "{level}"
585 );
586 }
587 }
588
589 #[test]
590 fn xai_unknown_id_fails_closed_except_adaptive() {
591 let model = "xai-auth/grok-9000";
592 assert!(validate_reasoning_mutation(model, Adaptive).is_ok());
593 for level in [Off, Low, Medium, High, XHigh, Max, Ultra] {
594 assert!(
595 validate_reasoning_mutation(model, level).is_err(),
596 "{level}"
597 );
598 }
599 }
600
601 #[test]
604 fn unknown_codex_model_rejects_max_and_ultra_at_mutation_time() {
605 for level in [Max, Ultra] {
606 let err =
607 validate_reasoning_mutation("openai-codex/gpt-unknown-future", level).unwrap_err();
608 assert!(err.contains("no capability metadata"), "{err}");
609 }
610 for level in [Off, Adaptive, Low, Medium, High, XHigh] {
612 assert!(
613 validate_reasoning_mutation("openai-codex/gpt-unknown-future", level).is_ok(),
614 "{level}"
615 );
616 }
617 }
618
619 #[test]
620 fn known_codex_model_membership_still_enforced() {
621 assert!(validate_reasoning_mutation("openai-codex/gpt-5.6-sol", Ultra).is_ok());
622 assert!(validate_reasoning_mutation("openai-codex/gpt-5.5", Ultra).is_err());
623 }
624
625 #[test]
626 fn live_codex_ultra_requires_v2_at_mutation_and_in_options() {
627 use super::super::{
628 CatalogModel, CatalogProviderKind, CatalogSource, CodexMultiAgentVersion,
629 };
630
631 for (suffix, version) in [("missing", None), ("v1", Some(CodexMultiAgentVersion::V1))] {
632 let id = format!("gpt-ultra-validation-{suffix}");
633 let qualified = format!("openai-codex/{id}");
634 let mut live = CatalogModel::new("openai-codex", "OpenAI Codex", &id).unwrap();
635 live.provider_kind = CatalogProviderKind::OpenAiCodex;
636 live.source = CatalogSource::Live;
637 live.reasoning = ReasoningSupport::CodexNamed {
638 multi_agent_reasoning_effort: None,
639 supported: vec![Low, Medium, High, XHigh, Max, Ultra],
640 default_level: Some(High),
641 multi_agent_version: version,
642 };
643 capability_cache::insert(live);
644
645 assert!(
646 validate_reasoning_mutation(&qualified, Max).is_ok(),
647 "Max does not require v2: {qualified}"
648 );
649 let err = validate_reasoning_mutation(&qualified, Ultra)
650 .expect_err("Ultra must fail closed without exact v2 evidence");
651 assert!(err.contains("multi-agent v2"), "{err}");
652
653 let options = thinking_options_for_model(&qualified);
654 assert!(options.contains(&"max".to_string()), "{options:?}");
655 assert!(!options.contains(&"ultra".to_string()), "{options:?}");
656 }
657 }
658
659 #[test]
662 fn anthropic_accepts_budget_expressible_levels_and_rejects_extended() {
663 for model in ["anthropic/claude-opus-4-7", "claude-sonnet-4-6"] {
664 for level in [Off, Adaptive, Low, Medium, High, XHigh] {
665 assert!(
666 validate_reasoning_mutation(model, level).is_ok(),
667 "{model} {level}"
668 );
669 }
670 for level in [Max, Ultra, UltraCode] {
671 assert!(
672 validate_reasoning_mutation(model, level).is_err(),
673 "{model} {level}"
674 );
675 }
676 }
677 }
678
679 #[test]
680 fn anthropic_live_no_thinking_evidence_rejects_named() {
681 let mut m = super::super::CatalogModel::new(
682 "anthropic",
683 "Anthropic",
684 "claude-test-validation-nothink",
685 )
686 .unwrap();
687 m.provider_kind = super::super::CatalogProviderKind::Anthropic;
688 m.reasoning = ReasoningSupport::None;
689 m.source = super::super::CatalogSource::Live;
690 capability_cache::insert(m);
691 let model = "anthropic/claude-test-validation-nothink";
692 for level in [Off, Adaptive] {
693 assert!(validate_reasoning_mutation(model, level).is_ok(), "{level}");
694 }
695 for level in [Low, Medium, High, XHigh] {
696 assert!(
697 validate_reasoning_mutation(model, level).is_err(),
698 "{level}"
699 );
700 }
701 }
702
703 #[test]
706 fn default_levels_come_from_exact_descriptors() {
707 assert_eq!(default_level_for_model("xai-auth/grok-4.5"), Some(High));
708 assert_eq!(
709 default_level_for_model("xai-auth/grok-4.5-latest"),
710 Some(High)
711 );
712 assert_eq!(default_level_for_model("xai-auth/grok-4.6"), Some(High));
713 assert_eq!(
714 default_level_for_model("xai-auth/grok-4.20-multi-agent-0309"),
715 Some(Adaptive)
716 );
717 assert_eq!(default_level_for_model("xai-auth/grok-4.3"), Some(Adaptive));
718 assert_eq!(
719 default_level_for_model("xai-auth/grok-4.20-0309-non-reasoning"),
720 Some(Adaptive)
721 );
722 assert_eq!(default_level_for_model("anthropic/claude-opus-4-7"), None);
723 assert_eq!(
724 default_level_for_model("openai-codex/gpt-5.3-codex-spark"),
725 Some(High)
726 );
727 }
728
729 #[test]
732 fn xai_options_derive_from_exact_capabilities() {
733 assert_eq!(
734 thinking_options_for_model("xai-auth/grok-4.5"),
735 vec!["adaptive", "low", "medium", "high"]
736 );
737 assert_eq!(
738 thinking_options_for_model("xai-auth/grok-4.6"),
739 vec!["adaptive", "low", "medium", "high"]
740 );
741 assert_eq!(
742 thinking_options_for_model("xai-auth/grok-4.20-multi-agent-0309"),
743 vec!["adaptive", "low", "medium", "high", "xhigh"]
744 );
745 assert_eq!(
746 thinking_options_for_model("xai-auth/grok-4.3"),
747 vec!["adaptive"]
748 );
749 assert_eq!(
750 thinking_options_for_model("xai-auth/grok-4.20-0309-non-reasoning"),
751 vec!["off", "adaptive"]
752 );
753 assert_eq!(
754 thinking_options_for_model("xai-auth/grok-9000"),
755 vec!["adaptive"]
756 );
757 }
758
759 #[test]
762 fn reasoning_type_derives_from_exact_capabilities() {
763 assert_eq!(
765 reasoning_type_for_model("openai-codex/gpt-5.6-sol"),
766 "effort (named)"
767 );
768 assert_eq!(
769 reasoning_type_for_model("openai-codex/gpt-unknown-future"),
770 "effort (named)"
771 );
772 assert_eq!(reasoning_type_for_model("xai-auth/grok-4.5"), "effort");
774 assert_eq!(reasoning_type_for_model("xai-auth/grok-4.6"), "effort");
775 assert_eq!(reasoning_type_for_model("xai-auth/grok-4.3"), "intrinsic");
776 assert_eq!(
777 reasoning_type_for_model("xai-auth/grok-4.20-0309-non-reasoning"),
778 "none"
779 );
780 assert_eq!(reasoning_type_for_model("xai-auth/grok-9000"), "unknown");
781 assert_eq!(
783 reasoning_type_for_model("anthropic/claude-opus-4-7"),
784 "adaptive"
785 );
786 assert_eq!(
787 reasoning_type_for_model("anthropic/claude-sonnet-4-6"),
788 "budget (legacy)"
789 );
790 assert_eq!(
791 reasoning_type_for_model("claude-sonnet-4-6"),
792 "budget (legacy)"
793 );
794 assert_eq!(
796 reasoning_type_for_model("anthropic/claude-future-x"),
797 "budget (legacy)"
798 );
799 assert_eq!(
801 reasoning_type_for_model("groq/llama-3.3-70b-versatile"),
802 "unverified"
803 );
804 }
805
806 #[test]
810 fn cached_unknown_reasoning_does_not_shadow_static_capability() {
811 let mut m =
812 super::super::CatalogModel::new("anthropic", "Anthropic", "claude-opus-4-7").unwrap();
813 m.reasoning = crate::runtime::openai::catalog::ReasoningSupport::Unknown;
814 capability_cache::insert(m);
815 assert_eq!(
816 reasoning_type_for_model("anthropic/claude-opus-4-7"),
817 "adaptive",
818 "cached Unknown must fall through to the static adaptive entry"
819 );
820 }
821
822 #[test]
823 fn reasoning_type_none_for_live_no_thinking_anthropic_evidence() {
824 let mut m = super::super::CatalogModel::new(
825 "anthropic",
826 "Anthropic",
827 "claude-test-reasoning-type-nothink",
828 )
829 .unwrap();
830 m.provider_kind = super::super::CatalogProviderKind::Anthropic;
831 m.reasoning = ReasoningSupport::None;
832 m.source = super::super::CatalogSource::Live;
833 capability_cache::insert(m);
834 assert_eq!(
835 reasoning_type_for_model("anthropic/claude-test-reasoning-type-nothink"),
836 "none"
837 );
838 }
839
840 #[test]
841 fn anthropic_options_derive_from_capabilities() {
842 assert_eq!(
843 thinking_options_for_model("anthropic/claude-opus-4-7"),
844 vec!["off", "adaptive", "low", "medium", "high", "xhigh"]
845 );
846 assert_eq!(
848 thinking_options_for_model("claude-sonnet-4-6"),
849 vec!["off", "adaptive", "low", "medium", "high", "xhigh"]
850 );
851 }
852
853 #[test]
854 fn options_never_advertise_max_ultra_off_codex() {
855 for model in [
856 "anthropic/claude-opus-4-7",
857 "xai-auth/grok-4.5",
858 "groq/llama-3.3-70b-versatile",
859 ] {
860 let opts = thinking_options_for_model(model);
861 assert!(!opts.contains(&"max".to_string()), "{model}");
862 assert!(!opts.contains(&"ultra".to_string()), "{model}");
863 }
864 }
865
866 #[test]
867 fn kimi_k3_accepts_exact_efforts_and_rejects_off_medium_xhigh_ultra() {
868 for model in [
869 "kimi/kimi-k3",
870 "kimi/kimi-k3.1-pro",
871 "kimi/kimi-k3.1-pro-highspeed",
872 ] {
873 for level in [Adaptive, Low, High, Max] {
874 assert!(
875 validate_reasoning_mutation(model, level).is_ok(),
876 "{model} {level}"
877 );
878 }
879 for level in [Off, Medium, XHigh, Ultra, UltraCode] {
880 let err = validate_reasoning_mutation(model, level).unwrap_err();
881 assert!(err.contains("kimi"), "{model}: {err}");
882 }
883 }
884 }
885
886 #[test]
887 fn kimi_always_thinking_models_accept_only_adaptive() {
888 for model in ["kimi/kimi-k2.7-code", "kimi/kimi-k2.7-code-highspeed"] {
889 assert!(
890 validate_reasoning_mutation(model, Adaptive).is_ok(),
891 "{model}"
892 );
893 for level in [Off, Low, Medium, High, Max] {
894 assert!(
895 validate_reasoning_mutation(model, level).is_err(),
896 "{model} {level}"
897 );
898 }
899 }
900 }
901
902 #[test]
903 fn kimi_toggleable_models_accept_off_and_adaptive_only() {
904 for model in ["kimi/kimi-k2.6", "kimi/kimi-k2.5"] {
905 for level in [Off, Adaptive] {
906 assert!(
907 validate_reasoning_mutation(model, level).is_ok(),
908 "{model} {level}"
909 );
910 }
911 for level in [Low, Medium, High, Max] {
912 assert!(
913 validate_reasoning_mutation(model, level).is_err(),
914 "{model} {level}"
915 );
916 }
917 }
918 }
919
920 #[test]
921 fn kimi_unknown_ids_fail_closed_except_adaptive() {
922 let model = "kimi/kimi-k9000";
923 assert!(validate_reasoning_mutation(model, Adaptive).is_ok());
924 for level in [Off, Low, Medium, High, Max, Ultra] {
925 assert!(
926 validate_reasoning_mutation(model, level).is_err(),
927 "{level}"
928 );
929 }
930 }
931
932 #[test]
933 fn kimi_options_are_exact_per_model() {
934 for model in [
935 "kimi/kimi-k3",
936 "kimi/kimi-k3.1-pro",
937 "kimi/kimi-k3.1-pro-highspeed",
938 ] {
939 assert_eq!(
940 thinking_options_for_model(model),
941 vec!["adaptive", "low", "high", "max"],
942 "{model}"
943 );
944 }
945 for model in ["kimi/kimi-k2.7-code", "kimi/kimi-k2.7-code-highspeed"] {
946 assert_eq!(
947 thinking_options_for_model(model),
948 vec!["adaptive"],
949 "{model}"
950 );
951 }
952 for model in ["kimi/kimi-k2.6", "kimi/kimi-k2.5"] {
953 assert_eq!(
954 thinking_options_for_model(model),
955 vec!["off", "adaptive"],
956 "{model}"
957 );
958 }
959 assert_eq!(
960 thinking_options_for_model("kimi/kimi-k9000"),
961 vec!["adaptive"]
962 );
963 }
964
965 #[test]
966 fn kimi_reasoning_types_and_default_level() {
967 for model in [
968 "kimi/kimi-k3",
969 "kimi/kimi-k3.1-pro",
970 "kimi/kimi-k3.1-pro-highspeed",
971 ] {
972 assert_eq!(reasoning_type_for_model(model), "effort", "{model}");
973 assert_eq!(
974 default_level_for_model(model),
975 Some(ReasoningLevel::Max),
976 "{model}"
977 );
978 }
979 assert_eq!(reasoning_type_for_model("kimi/kimi-k2.7-code"), "intrinsic");
980 assert_eq!(
981 reasoning_type_for_model("kimi/kimi-k2.6"),
982 "toggle (on/off)"
983 );
984 assert_eq!(reasoning_type_for_model("kimi/kimi-k9000"), "unknown");
985 assert_eq!(
986 default_level_for_model("kimi/kimi-k2.6"),
987 Some(ReasoningLevel::Adaptive)
988 );
989 }
990}