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