1use agent_core::reasoning::ReasoningLevel;
12
13use super::{
14 anthropic_static_capability, capability_cache, codex_static_capability, kimi_code_capability,
15 kimi_static_capability, plan_codex_execution, xai_static_capability, CatalogProviderKind,
16 CodexMultiAgentVersion, CodexRequestRole, KimiReasoningCapability, ReasoningSupport,
17 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("openai-codex/") {
99 if matches!(level, ReasoningLevel::Off | ReasoningLevel::Adaptive) {
101 return Ok(());
102 }
103 if matches!(level, ReasoningLevel::Max | ReasoningLevel::Ultra) {
104 return plan_codex_execution(model, level, CodexRequestRole::Foreground, None)
105 .map(|_| ())
106 .map_err(|error| error.to_string());
107 }
108 return match codex_supported_levels(model, model_id) {
109 Some(levels) if levels.contains(&level) => Ok(()),
110 Some(levels) => Err(unsupported_msg(level, model, &levels)),
111 None if level.requires_codex_support() => Err(format!(
114 "no capability metadata for {model}; cannot authorize level '{level}'"
115 )),
116 None => Ok(()),
117 };
118 }
119 if let Some(model_id) = model.strip_prefix("xai-auth/") {
120 if level == ReasoningLevel::Adaptive {
122 return Ok(());
123 }
124 return match xai_static_capability(model_id) {
125 Some(XaiReasoningCapability::Effort {
126 supported,
127 can_disable,
128 ..
129 }) => match level {
130 ReasoningLevel::Off if can_disable => Ok(()),
131 ReasoningLevel::Off => Err(format!(
132 "reasoning cannot be disabled on {model}; use adaptive or a supported effort"
133 )),
134 l if supported.contains(&l) => Ok(()),
135 l => Err(unsupported_msg(l, model, supported)),
136 },
137 Some(XaiReasoningCapability::IntrinsicReasoning) => match level {
138 ReasoningLevel::Off => Err(format!(
139 "{model} has no documented way to disable reasoning; use adaptive"
140 )),
141 l => Err(format!(
142 "{model} has no documented effort control; level '{l}' cannot be sent — use adaptive"
143 )),
144 },
145 Some(XaiReasoningCapability::NonReasoning) => match level {
146 ReasoningLevel::Off => Ok(()),
147 l => Err(format!(
148 "{model} is a non-reasoning model; level '{l}' is not supported"
149 )),
150 },
151 None => Err(format!(
152 "no capability metadata for {model}; cannot authorize level '{level}'"
153 )),
154 };
155 }
156 if let Some(capability) = kimi_route_capability(model) {
157 if level == ReasoningLevel::Adaptive {
159 return Ok(());
160 }
161 return match capability {
162 Some(KimiReasoningCapability::Effort {
163 supported,
164 can_disable,
165 ..
166 }) => match level {
167 ReasoningLevel::Off if can_disable => Ok(()),
168 ReasoningLevel::Off => Err(format!(
169 "reasoning cannot be disabled on {model}; use adaptive or a supported effort"
170 )),
171 l if supported.contains(&l) => Ok(()),
172 l => Err(unsupported_msg(l, model, supported)),
173 },
174 Some(KimiReasoningCapability::AlwaysThinking) => match level {
175 ReasoningLevel::Off => Err(format!(
176 "{model} always thinks and the API rejects disabling; use adaptive"
177 )),
178 l => Err(format!(
179 "{model} has no documented effort control; level '{l}' cannot be sent — use adaptive"
180 )),
181 },
182 Some(KimiReasoningCapability::ToggleableThinking) => match level {
183 ReasoningLevel::Off => Ok(()),
184 l => Err(format!(
185 "{model} supports only a thinking on/off toggle; level '{l}' cannot be sent — use off or adaptive"
186 )),
187 },
188 None => Err(format!(
189 "no capability metadata for {model}; cannot authorize level '{level}'"
190 )),
191 };
192 }
193 if let Some(model_id) = anthropic_model_id(model) {
194 if matches!(level, ReasoningLevel::Max | ReasoningLevel::UltraCode) {
195 let capabilities = model
198 .starts_with("anthropic/")
199 .then(|| super::anthropic_mode_capabilities(model))
200 .flatten()
201 .map(|caps| {
202 caps.narrow_with_live_effort(capability_cache::get(model).and_then(|entry| {
203 match entry.reasoning {
204 ReasoningSupport::AnthropicAdaptive { adaptive } => Some(adaptive),
205 ReasoningSupport::None => Some(false),
206 _ => None,
207 }
208 }))
209 });
210 let supported = match level {
211 ReasoningLevel::Max => capabilities.is_some_and(|caps| caps.max_supported),
212 ReasoningLevel::UltraCode => {
213 capabilities.is_some_and(|caps| caps.ultracode_supported())
214 }
215 _ => unreachable!(),
216 };
217 return supported
218 .then_some(())
219 .ok_or_else(|| format!("reasoning level '{level}' is not supported by {model}"));
220 }
221 if level == ReasoningLevel::Ultra {
222 return Err(format!(
223 "reasoning level '{level}' requires authoritative exact-model capability metadata; {model} has none"
224 ));
225 }
226 if matches!(anthropic_capability(model_id), Some(ReasoningSupport::None))
229 && !matches!(level, ReasoningLevel::Off | ReasoningLevel::Adaptive)
230 {
231 return Err(format!(
232 "{model} does not support extended thinking; level '{level}' is not available"
233 ));
234 }
235 return Ok(());
236 }
237 if level.requires_codex_support() {
239 return Err(format!(
240 "reasoning level '{level}' requires authoritative exact-model capability metadata; {model} has none"
241 ));
242 }
243 Ok(())
244}
245
246pub fn default_level_for_model(model: &str) -> Option<ReasoningLevel> {
249 if let Some(model_id) = model.strip_prefix("openai-codex/") {
250 return match codex_reasoning_capability(model, model_id)? {
251 ReasoningSupport::CodexNamed { default_level, .. } => default_level,
252 _ => None,
253 };
254 }
255 if let Some(model_id) = model.strip_prefix("xai-auth/") {
256 return Some(match xai_static_capability(model_id) {
257 Some(XaiReasoningCapability::Effort {
258 default_level: Some(level),
259 ..
260 }) => level,
261 _ => ReasoningLevel::Adaptive,
264 });
265 }
266 if let Some(capability) = kimi_route_capability(model) {
267 return Some(match capability {
268 Some(KimiReasoningCapability::Effort {
269 default_level: Some(level),
270 ..
271 }) => level,
272 _ => ReasoningLevel::Adaptive,
274 });
275 }
276 None
277}
278
279pub fn reasoning_type_for_model(model: &str) -> &'static str {
284 if model.strip_prefix("openai-codex/").is_some() {
285 return "effort (named)";
287 }
288 if let Some(model_id) = model.strip_prefix("xai-auth/") {
289 return match xai_static_capability(model_id) {
290 Some(XaiReasoningCapability::Effort { .. }) => "effort",
291 Some(XaiReasoningCapability::IntrinsicReasoning) => "intrinsic",
292 Some(XaiReasoningCapability::NonReasoning) => "none",
293 None => "unknown",
294 };
295 }
296 if let Some(capability) = kimi_route_capability(model) {
297 return match capability {
298 Some(KimiReasoningCapability::Effort { .. }) => "effort",
299 Some(KimiReasoningCapability::AlwaysThinking) => "intrinsic",
300 Some(KimiReasoningCapability::ToggleableThinking) => "toggle (on/off)",
301 None => "unknown",
302 };
303 }
304 if let Some(model_id) = anthropic_model_id(model) {
305 return match anthropic_capability(model_id) {
306 Some(ReasoningSupport::AnthropicAdaptive { adaptive: true }) => "adaptive",
307 Some(ReasoningSupport::None) => "none",
308 _ => "budget (legacy)",
311 };
312 }
313 "unverified"
314}
315
316pub fn thinking_options_for_model(model: &str) -> Vec<String> {
319 let owned = |slice: &[&str]| slice.iter().map(|s| s.to_string()).collect::<Vec<_>>();
320 if let Some(model_id) = model.strip_prefix("openai-codex/") {
321 if let Some(levels) = codex_supported_levels(model, model_id) {
322 let mut opts = vec!["off".to_string(), "adaptive".to_string()];
323 opts.extend(levels.iter().map(|l| l.as_str().to_string()));
324 return opts;
325 }
326 return owned(CONSERVATIVE_OPTIONS);
327 }
328 if let Some(model_id) = model.strip_prefix("xai-auth/") {
329 return match xai_static_capability(model_id) {
330 Some(XaiReasoningCapability::Effort {
331 supported,
332 can_disable,
333 ..
334 }) => {
335 let mut opts = Vec::new();
336 if can_disable {
337 opts.push("off".to_string());
338 }
339 opts.push("adaptive".to_string());
340 opts.extend(supported.iter().map(|l| l.as_str().to_string()));
341 opts
342 }
343 Some(XaiReasoningCapability::NonReasoning) => owned(&["off", "adaptive"]),
344 _ => owned(&["adaptive"]),
347 };
348 }
349 if let Some(capability) = kimi_route_capability(model) {
350 return match capability {
351 Some(KimiReasoningCapability::Effort {
352 supported,
353 can_disable,
354 ..
355 }) => {
356 let mut opts = Vec::new();
357 if can_disable {
358 opts.push("off".to_string());
359 }
360 opts.push("adaptive".to_string());
361 opts.extend(supported.iter().map(|l| l.as_str().to_string()));
362 opts
363 }
364 Some(KimiReasoningCapability::ToggleableThinking) => owned(&["off", "adaptive"]),
365 _ => owned(&["adaptive"]),
368 };
369 }
370 if let Some(model_id) = anthropic_model_id(model) {
371 let live = model
372 .starts_with("anthropic/")
373 .then(|| capability_cache::get(model))
374 .flatten()
375 .map(|entry| entry.reasoning);
376 let specials_authorized = super::anthropic_mode_capabilities(model).is_some()
377 && !matches!(live, Some(ReasoningSupport::None));
378 return match anthropic_capability(model_id) {
379 Some(ReasoningSupport::AnthropicAdaptive { .. }) if specials_authorized => owned(&[
382 "off",
383 "adaptive",
384 "low",
385 "medium",
386 "high",
387 "xhigh",
388 "max",
389 "ultracode",
390 ]),
391 Some(ReasoningSupport::AnthropicAdaptive { .. }) => owned(CONSERVATIVE_OPTIONS),
392 Some(ReasoningSupport::None) => owned(&["off", "adaptive"]),
394 _ if specials_authorized => owned(&[
395 "off",
396 "adaptive",
397 "low",
398 "medium",
399 "high",
400 "xhigh",
401 "max",
402 "ultracode",
403 ]),
404 _ => owned(CONSERVATIVE_OPTIONS),
405 };
406 }
407 owned(CONSERVATIVE_OPTIONS)
408}
409
410#[cfg(test)]
411mod tests {
412 use super::*;
413 use ReasoningLevel::*;
414
415 #[test]
416 fn fable_options_expose_distinct_special_modes() {
417 let model = "anthropic/claude-fable-5";
418 assert_eq!(
419 thinking_options_for_model(model),
420 [
421 "off",
422 "adaptive",
423 "low",
424 "medium",
425 "high",
426 "xhigh",
427 "max",
428 "ultracode"
429 ]
430 );
431 }
432
433 #[test]
436 fn xai_45_and_46_accept_exact_efforts_and_reject_off_xhigh_max_ultra() {
437 for model in [
438 "xai-auth/grok-4.5",
439 "xai-auth/grok-4.5-latest",
440 "xai-auth/grok-4.6",
441 ] {
442 for level in [Adaptive, Low, Medium, High] {
443 assert!(
444 validate_reasoning_mutation(model, level).is_ok(),
445 "{model} {level}"
446 );
447 }
448 for level in [Off, XHigh, Max, Ultra] {
449 let err = validate_reasoning_mutation(model, level).unwrap_err();
450 assert!(err.contains(model), "{err}");
451 }
452 }
453 }
454
455 #[test]
456 fn xai_multi_agent_accepts_xhigh_but_not_off() {
457 let model = "xai-auth/grok-4.20-multi-agent-0309";
458 for level in [Adaptive, Low, Medium, High, XHigh] {
459 assert!(validate_reasoning_mutation(model, level).is_ok(), "{level}");
460 }
461 for level in [Off, Max, Ultra] {
462 assert!(
463 validate_reasoning_mutation(model, level).is_err(),
464 "{level}"
465 );
466 }
467 }
468
469 #[test]
470 fn xai_intrinsic_reasoning_rejects_named_and_off_accepts_adaptive() {
471 for id in [
472 "grok-4.3",
473 "grok-4.3-latest",
474 "grok-latest",
475 "grok-4.20-0309-reasoning",
476 ] {
477 let model = format!("xai-auth/{id}");
478 assert!(validate_reasoning_mutation(&model, Adaptive).is_ok());
479 for level in [Off, Low, Medium, High, XHigh, Max, Ultra] {
480 assert!(
481 validate_reasoning_mutation(&model, level).is_err(),
482 "{model} {level}"
483 );
484 }
485 }
486 }
487
488 #[test]
489 fn xai_non_reasoning_rejects_named_accepts_off_adaptive() {
490 let model = "xai-auth/grok-4.20-0309-non-reasoning";
491 assert!(validate_reasoning_mutation(model, Off).is_ok());
492 assert!(validate_reasoning_mutation(model, Adaptive).is_ok());
493 for level in [Low, Medium, High, XHigh, Max, Ultra] {
494 assert!(
495 validate_reasoning_mutation(model, level).is_err(),
496 "{level}"
497 );
498 }
499 }
500
501 #[test]
502 fn xai_unknown_id_fails_closed_except_adaptive() {
503 let model = "xai-auth/grok-9000";
504 assert!(validate_reasoning_mutation(model, Adaptive).is_ok());
505 for level in [Off, Low, Medium, High, XHigh, Max, Ultra] {
506 assert!(
507 validate_reasoning_mutation(model, level).is_err(),
508 "{level}"
509 );
510 }
511 }
512
513 #[test]
516 fn unknown_codex_model_rejects_max_and_ultra_at_mutation_time() {
517 for level in [Max, Ultra] {
518 let err =
519 validate_reasoning_mutation("openai-codex/gpt-unknown-future", level).unwrap_err();
520 assert!(err.contains("no capability metadata"), "{err}");
521 }
522 for level in [Off, Adaptive, Low, Medium, High, XHigh] {
524 assert!(
525 validate_reasoning_mutation("openai-codex/gpt-unknown-future", level).is_ok(),
526 "{level}"
527 );
528 }
529 }
530
531 #[test]
532 fn known_codex_model_membership_still_enforced() {
533 assert!(validate_reasoning_mutation("openai-codex/gpt-5.6-sol", Ultra).is_ok());
534 assert!(validate_reasoning_mutation("openai-codex/gpt-5.5", Ultra).is_err());
535 }
536
537 #[test]
538 fn live_codex_ultra_requires_v2_at_mutation_and_in_options() {
539 use super::super::{
540 CatalogModel, CatalogProviderKind, CatalogSource, CodexMultiAgentVersion,
541 };
542
543 for (suffix, version) in [("missing", None), ("v1", Some(CodexMultiAgentVersion::V1))] {
544 let id = format!("gpt-ultra-validation-{suffix}");
545 let qualified = format!("openai-codex/{id}");
546 let mut live = CatalogModel::new("openai-codex", "OpenAI Codex", &id).unwrap();
547 live.provider_kind = CatalogProviderKind::OpenAiCodex;
548 live.source = CatalogSource::Live;
549 live.reasoning = ReasoningSupport::CodexNamed {
550 multi_agent_reasoning_effort: None,
551 supported: vec![Low, Medium, High, XHigh, Max, Ultra],
552 default_level: Some(High),
553 multi_agent_version: version,
554 };
555 capability_cache::insert(live);
556
557 assert!(
558 validate_reasoning_mutation(&qualified, Max).is_ok(),
559 "Max does not require v2: {qualified}"
560 );
561 let err = validate_reasoning_mutation(&qualified, Ultra)
562 .expect_err("Ultra must fail closed without exact v2 evidence");
563 assert!(err.contains("multi-agent v2"), "{err}");
564
565 let options = thinking_options_for_model(&qualified);
566 assert!(options.contains(&"max".to_string()), "{options:?}");
567 assert!(!options.contains(&"ultra".to_string()), "{options:?}");
568 }
569 }
570
571 #[test]
574 fn anthropic_accepts_budget_expressible_levels_and_rejects_extended() {
575 for model in ["anthropic/claude-opus-4-7", "claude-sonnet-4-6"] {
576 for level in [Off, Adaptive, Low, Medium, High, XHigh] {
577 assert!(
578 validate_reasoning_mutation(model, level).is_ok(),
579 "{model} {level}"
580 );
581 }
582 for level in [Max, Ultra, UltraCode] {
583 assert!(
584 validate_reasoning_mutation(model, level).is_err(),
585 "{model} {level}"
586 );
587 }
588 }
589 }
590
591 #[test]
592 fn anthropic_live_no_thinking_evidence_rejects_named() {
593 let mut m = super::super::CatalogModel::new(
594 "anthropic",
595 "Anthropic",
596 "claude-test-validation-nothink",
597 )
598 .unwrap();
599 m.provider_kind = super::super::CatalogProviderKind::Anthropic;
600 m.reasoning = ReasoningSupport::None;
601 m.source = super::super::CatalogSource::Live;
602 capability_cache::insert(m);
603 let model = "anthropic/claude-test-validation-nothink";
604 for level in [Off, Adaptive] {
605 assert!(validate_reasoning_mutation(model, level).is_ok(), "{level}");
606 }
607 for level in [Low, Medium, High, XHigh] {
608 assert!(
609 validate_reasoning_mutation(model, level).is_err(),
610 "{level}"
611 );
612 }
613 }
614
615 #[test]
618 fn default_levels_come_from_exact_descriptors() {
619 assert_eq!(default_level_for_model("xai-auth/grok-4.5"), Some(High));
620 assert_eq!(
621 default_level_for_model("xai-auth/grok-4.5-latest"),
622 Some(High)
623 );
624 assert_eq!(default_level_for_model("xai-auth/grok-4.6"), Some(High));
625 assert_eq!(
626 default_level_for_model("xai-auth/grok-4.20-multi-agent-0309"),
627 Some(Adaptive)
628 );
629 assert_eq!(default_level_for_model("xai-auth/grok-4.3"), Some(Adaptive));
630 assert_eq!(
631 default_level_for_model("xai-auth/grok-4.20-0309-non-reasoning"),
632 Some(Adaptive)
633 );
634 assert_eq!(default_level_for_model("anthropic/claude-opus-4-7"), None);
635 assert_eq!(
636 default_level_for_model("openai-codex/gpt-5.3-codex-spark"),
637 Some(High)
638 );
639 }
640
641 #[test]
644 fn xai_options_derive_from_exact_capabilities() {
645 assert_eq!(
646 thinking_options_for_model("xai-auth/grok-4.5"),
647 vec!["adaptive", "low", "medium", "high"]
648 );
649 assert_eq!(
650 thinking_options_for_model("xai-auth/grok-4.6"),
651 vec!["adaptive", "low", "medium", "high"]
652 );
653 assert_eq!(
654 thinking_options_for_model("xai-auth/grok-4.20-multi-agent-0309"),
655 vec!["adaptive", "low", "medium", "high", "xhigh"]
656 );
657 assert_eq!(
658 thinking_options_for_model("xai-auth/grok-4.3"),
659 vec!["adaptive"]
660 );
661 assert_eq!(
662 thinking_options_for_model("xai-auth/grok-4.20-0309-non-reasoning"),
663 vec!["off", "adaptive"]
664 );
665 assert_eq!(
666 thinking_options_for_model("xai-auth/grok-9000"),
667 vec!["adaptive"]
668 );
669 }
670
671 #[test]
674 fn reasoning_type_derives_from_exact_capabilities() {
675 assert_eq!(
677 reasoning_type_for_model("openai-codex/gpt-5.6-sol"),
678 "effort (named)"
679 );
680 assert_eq!(
681 reasoning_type_for_model("openai-codex/gpt-unknown-future"),
682 "effort (named)"
683 );
684 assert_eq!(reasoning_type_for_model("xai-auth/grok-4.5"), "effort");
686 assert_eq!(reasoning_type_for_model("xai-auth/grok-4.6"), "effort");
687 assert_eq!(reasoning_type_for_model("xai-auth/grok-4.3"), "intrinsic");
688 assert_eq!(
689 reasoning_type_for_model("xai-auth/grok-4.20-0309-non-reasoning"),
690 "none"
691 );
692 assert_eq!(reasoning_type_for_model("xai-auth/grok-9000"), "unknown");
693 assert_eq!(
695 reasoning_type_for_model("anthropic/claude-opus-4-7"),
696 "adaptive"
697 );
698 assert_eq!(
699 reasoning_type_for_model("anthropic/claude-sonnet-4-6"),
700 "budget (legacy)"
701 );
702 assert_eq!(
703 reasoning_type_for_model("claude-sonnet-4-6"),
704 "budget (legacy)"
705 );
706 assert_eq!(
708 reasoning_type_for_model("anthropic/claude-future-x"),
709 "budget (legacy)"
710 );
711 assert_eq!(
713 reasoning_type_for_model("groq/llama-3.3-70b-versatile"),
714 "unverified"
715 );
716 }
717
718 #[test]
722 fn cached_unknown_reasoning_does_not_shadow_static_capability() {
723 let mut m =
724 super::super::CatalogModel::new("anthropic", "Anthropic", "claude-opus-4-7").unwrap();
725 m.reasoning = crate::runtime::openai::catalog::ReasoningSupport::Unknown;
726 capability_cache::insert(m);
727 assert_eq!(
728 reasoning_type_for_model("anthropic/claude-opus-4-7"),
729 "adaptive",
730 "cached Unknown must fall through to the static adaptive entry"
731 );
732 }
733
734 #[test]
735 fn reasoning_type_none_for_live_no_thinking_anthropic_evidence() {
736 let mut m = super::super::CatalogModel::new(
737 "anthropic",
738 "Anthropic",
739 "claude-test-reasoning-type-nothink",
740 )
741 .unwrap();
742 m.provider_kind = super::super::CatalogProviderKind::Anthropic;
743 m.reasoning = ReasoningSupport::None;
744 m.source = super::super::CatalogSource::Live;
745 capability_cache::insert(m);
746 assert_eq!(
747 reasoning_type_for_model("anthropic/claude-test-reasoning-type-nothink"),
748 "none"
749 );
750 }
751
752 #[test]
753 fn anthropic_options_derive_from_capabilities() {
754 assert_eq!(
755 thinking_options_for_model("anthropic/claude-opus-4-7"),
756 vec!["off", "adaptive", "low", "medium", "high", "xhigh"]
757 );
758 assert_eq!(
760 thinking_options_for_model("claude-sonnet-4-6"),
761 vec!["off", "adaptive", "low", "medium", "high", "xhigh"]
762 );
763 }
764
765 #[test]
766 fn options_never_advertise_max_ultra_off_codex() {
767 for model in [
768 "anthropic/claude-opus-4-7",
769 "xai-auth/grok-4.5",
770 "groq/llama-3.3-70b-versatile",
771 ] {
772 let opts = thinking_options_for_model(model);
773 assert!(!opts.contains(&"max".to_string()), "{model}");
774 assert!(!opts.contains(&"ultra".to_string()), "{model}");
775 }
776 }
777
778 #[test]
779 fn kimi_k3_accepts_exact_efforts_and_rejects_off_medium_xhigh_ultra() {
780 let model = "kimi/kimi-k3";
781 for level in [Adaptive, Low, High, Max] {
782 assert!(
783 validate_reasoning_mutation(model, level).is_ok(),
784 "{model} {level}"
785 );
786 }
787 for level in [Off, Medium, XHigh, Ultra, UltraCode] {
788 let err = validate_reasoning_mutation(model, level).unwrap_err();
789 assert!(err.contains("kimi"), "{err}");
790 }
791 }
792
793 #[test]
794 fn kimi_always_thinking_models_accept_only_adaptive() {
795 for model in ["kimi/kimi-k2.7-code", "kimi/kimi-k2.7-code-highspeed"] {
796 assert!(
797 validate_reasoning_mutation(model, Adaptive).is_ok(),
798 "{model}"
799 );
800 for level in [Off, Low, Medium, High, Max] {
801 assert!(
802 validate_reasoning_mutation(model, level).is_err(),
803 "{model} {level}"
804 );
805 }
806 }
807 }
808
809 #[test]
810 fn kimi_toggleable_models_accept_off_and_adaptive_only() {
811 for model in ["kimi/kimi-k2.6", "kimi/kimi-k2.5"] {
812 for level in [Off, Adaptive] {
813 assert!(
814 validate_reasoning_mutation(model, level).is_ok(),
815 "{model} {level}"
816 );
817 }
818 for level in [Low, Medium, High, Max] {
819 assert!(
820 validate_reasoning_mutation(model, level).is_err(),
821 "{model} {level}"
822 );
823 }
824 }
825 }
826
827 #[test]
828 fn kimi_unknown_ids_fail_closed_except_adaptive() {
829 let model = "kimi/kimi-k9000";
830 assert!(validate_reasoning_mutation(model, Adaptive).is_ok());
831 for level in [Off, Low, Medium, High, Max, Ultra] {
832 assert!(
833 validate_reasoning_mutation(model, level).is_err(),
834 "{level}"
835 );
836 }
837 }
838
839 #[test]
840 fn kimi_options_are_exact_per_model() {
841 assert_eq!(
842 thinking_options_for_model("kimi/kimi-k3"),
843 vec!["adaptive", "low", "high", "max"]
844 );
845 for model in ["kimi/kimi-k2.7-code", "kimi/kimi-k2.7-code-highspeed"] {
846 assert_eq!(
847 thinking_options_for_model(model),
848 vec!["adaptive"],
849 "{model}"
850 );
851 }
852 for model in ["kimi/kimi-k2.6", "kimi/kimi-k2.5"] {
853 assert_eq!(
854 thinking_options_for_model(model),
855 vec!["off", "adaptive"],
856 "{model}"
857 );
858 }
859 assert_eq!(
860 thinking_options_for_model("kimi/kimi-k9000"),
861 vec!["adaptive"]
862 );
863 }
864
865 #[test]
866 fn kimi_reasoning_types_and_default_level() {
867 assert_eq!(reasoning_type_for_model("kimi/kimi-k3"), "effort");
868 assert_eq!(reasoning_type_for_model("kimi/kimi-k2.7-code"), "intrinsic");
869 assert_eq!(
870 reasoning_type_for_model("kimi/kimi-k2.6"),
871 "toggle (on/off)"
872 );
873 assert_eq!(reasoning_type_for_model("kimi/kimi-k9000"), "unknown");
874 assert_eq!(
875 default_level_for_model("kimi/kimi-k3"),
876 Some(ReasoningLevel::Max)
877 );
878 assert_eq!(
879 default_level_for_model("kimi/kimi-k2.6"),
880 Some(ReasoningLevel::Adaptive)
881 );
882 }
883}