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_accepts_exact_efforts_and_rejects_off_xhigh_max_ultra() {
437 for model in ["xai-auth/grok-4.5", "xai-auth/grok-4.5-latest"] {
438 for level in [Adaptive, Low, Medium, High] {
439 assert!(
440 validate_reasoning_mutation(model, level).is_ok(),
441 "{model} {level}"
442 );
443 }
444 for level in [Off, XHigh, Max, Ultra] {
445 let err = validate_reasoning_mutation(model, level).unwrap_err();
446 assert!(err.contains(model), "{err}");
447 }
448 }
449 }
450
451 #[test]
452 fn xai_multi_agent_accepts_xhigh_but_not_off() {
453 let model = "xai-auth/grok-4.20-multi-agent-0309";
454 for level in [Adaptive, Low, Medium, High, XHigh] {
455 assert!(validate_reasoning_mutation(model, level).is_ok(), "{level}");
456 }
457 for level in [Off, Max, Ultra] {
458 assert!(
459 validate_reasoning_mutation(model, level).is_err(),
460 "{level}"
461 );
462 }
463 }
464
465 #[test]
466 fn xai_intrinsic_reasoning_rejects_named_and_off_accepts_adaptive() {
467 for id in [
468 "grok-4.3",
469 "grok-4.3-latest",
470 "grok-latest",
471 "grok-4.20-0309-reasoning",
472 ] {
473 let model = format!("xai-auth/{id}");
474 assert!(validate_reasoning_mutation(&model, Adaptive).is_ok());
475 for level in [Off, Low, Medium, High, XHigh, Max, Ultra] {
476 assert!(
477 validate_reasoning_mutation(&model, level).is_err(),
478 "{model} {level}"
479 );
480 }
481 }
482 }
483
484 #[test]
485 fn xai_non_reasoning_rejects_named_accepts_off_adaptive() {
486 let model = "xai-auth/grok-4.20-0309-non-reasoning";
487 assert!(validate_reasoning_mutation(model, Off).is_ok());
488 assert!(validate_reasoning_mutation(model, Adaptive).is_ok());
489 for level in [Low, Medium, High, XHigh, Max, Ultra] {
490 assert!(
491 validate_reasoning_mutation(model, level).is_err(),
492 "{level}"
493 );
494 }
495 }
496
497 #[test]
498 fn xai_unknown_id_fails_closed_except_adaptive() {
499 let model = "xai-auth/grok-9000";
500 assert!(validate_reasoning_mutation(model, Adaptive).is_ok());
501 for level in [Off, Low, Medium, High, XHigh, Max, Ultra] {
502 assert!(
503 validate_reasoning_mutation(model, level).is_err(),
504 "{level}"
505 );
506 }
507 }
508
509 #[test]
512 fn unknown_codex_model_rejects_max_and_ultra_at_mutation_time() {
513 for level in [Max, Ultra] {
514 let err =
515 validate_reasoning_mutation("openai-codex/gpt-unknown-future", level).unwrap_err();
516 assert!(err.contains("no capability metadata"), "{err}");
517 }
518 for level in [Off, Adaptive, Low, Medium, High, XHigh] {
520 assert!(
521 validate_reasoning_mutation("openai-codex/gpt-unknown-future", level).is_ok(),
522 "{level}"
523 );
524 }
525 }
526
527 #[test]
528 fn known_codex_model_membership_still_enforced() {
529 assert!(validate_reasoning_mutation("openai-codex/gpt-5.6-sol", Ultra).is_ok());
530 assert!(validate_reasoning_mutation("openai-codex/gpt-5.5", Ultra).is_err());
531 }
532
533 #[test]
534 fn live_codex_ultra_requires_v2_at_mutation_and_in_options() {
535 use super::super::{
536 CatalogModel, CatalogProviderKind, CatalogSource, CodexMultiAgentVersion,
537 };
538
539 for (suffix, version) in [("missing", None), ("v1", Some(CodexMultiAgentVersion::V1))] {
540 let id = format!("gpt-ultra-validation-{suffix}");
541 let qualified = format!("openai-codex/{id}");
542 let mut live = CatalogModel::new("openai-codex", "OpenAI Codex", &id).unwrap();
543 live.provider_kind = CatalogProviderKind::OpenAiCodex;
544 live.source = CatalogSource::Live;
545 live.reasoning = ReasoningSupport::CodexNamed {
546 supported: vec![Low, Medium, High, XHigh, Max, Ultra],
547 default_level: Some(High),
548 multi_agent_version: version,
549 };
550 capability_cache::insert(live);
551
552 assert!(
553 validate_reasoning_mutation(&qualified, Max).is_ok(),
554 "Max does not require v2: {qualified}"
555 );
556 let err = validate_reasoning_mutation(&qualified, Ultra)
557 .expect_err("Ultra must fail closed without exact v2 evidence");
558 assert!(err.contains("multi-agent v2"), "{err}");
559
560 let options = thinking_options_for_model(&qualified);
561 assert!(options.contains(&"max".to_string()), "{options:?}");
562 assert!(!options.contains(&"ultra".to_string()), "{options:?}");
563 }
564 }
565
566 #[test]
569 fn anthropic_accepts_budget_expressible_levels_and_rejects_extended() {
570 for model in ["anthropic/claude-opus-4-7", "claude-sonnet-4-6"] {
571 for level in [Off, Adaptive, Low, Medium, High, XHigh] {
572 assert!(
573 validate_reasoning_mutation(model, level).is_ok(),
574 "{model} {level}"
575 );
576 }
577 for level in [Max, Ultra, UltraCode] {
578 assert!(
579 validate_reasoning_mutation(model, level).is_err(),
580 "{model} {level}"
581 );
582 }
583 }
584 }
585
586 #[test]
587 fn anthropic_live_no_thinking_evidence_rejects_named() {
588 let mut m = super::super::CatalogModel::new(
589 "anthropic",
590 "Anthropic",
591 "claude-test-validation-nothink",
592 )
593 .unwrap();
594 m.provider_kind = super::super::CatalogProviderKind::Anthropic;
595 m.reasoning = ReasoningSupport::None;
596 m.source = super::super::CatalogSource::Live;
597 capability_cache::insert(m);
598 let model = "anthropic/claude-test-validation-nothink";
599 for level in [Off, Adaptive] {
600 assert!(validate_reasoning_mutation(model, level).is_ok(), "{level}");
601 }
602 for level in [Low, Medium, High, XHigh] {
603 assert!(
604 validate_reasoning_mutation(model, level).is_err(),
605 "{level}"
606 );
607 }
608 }
609
610 #[test]
613 fn default_levels_come_from_exact_descriptors() {
614 assert_eq!(default_level_for_model("xai-auth/grok-4.5"), Some(High));
615 assert_eq!(
616 default_level_for_model("xai-auth/grok-4.5-latest"),
617 Some(High)
618 );
619 assert_eq!(
620 default_level_for_model("xai-auth/grok-4.20-multi-agent-0309"),
621 Some(Adaptive)
622 );
623 assert_eq!(default_level_for_model("xai-auth/grok-4.3"), Some(Adaptive));
624 assert_eq!(
625 default_level_for_model("xai-auth/grok-4.20-0309-non-reasoning"),
626 Some(Adaptive)
627 );
628 assert_eq!(default_level_for_model("anthropic/claude-opus-4-7"), None);
629 assert_eq!(
630 default_level_for_model("openai-codex/gpt-5.3-codex-spark"),
631 Some(High)
632 );
633 }
634
635 #[test]
638 fn xai_options_derive_from_exact_capabilities() {
639 assert_eq!(
640 thinking_options_for_model("xai-auth/grok-4.5"),
641 vec!["adaptive", "low", "medium", "high"]
642 );
643 assert_eq!(
644 thinking_options_for_model("xai-auth/grok-4.20-multi-agent-0309"),
645 vec!["adaptive", "low", "medium", "high", "xhigh"]
646 );
647 assert_eq!(
648 thinking_options_for_model("xai-auth/grok-4.3"),
649 vec!["adaptive"]
650 );
651 assert_eq!(
652 thinking_options_for_model("xai-auth/grok-4.20-0309-non-reasoning"),
653 vec!["off", "adaptive"]
654 );
655 assert_eq!(
656 thinking_options_for_model("xai-auth/grok-9000"),
657 vec!["adaptive"]
658 );
659 }
660
661 #[test]
664 fn reasoning_type_derives_from_exact_capabilities() {
665 assert_eq!(
667 reasoning_type_for_model("openai-codex/gpt-5.6-sol"),
668 "effort (named)"
669 );
670 assert_eq!(
671 reasoning_type_for_model("openai-codex/gpt-unknown-future"),
672 "effort (named)"
673 );
674 assert_eq!(reasoning_type_for_model("xai-auth/grok-4.5"), "effort");
676 assert_eq!(reasoning_type_for_model("xai-auth/grok-4.3"), "intrinsic");
677 assert_eq!(
678 reasoning_type_for_model("xai-auth/grok-4.20-0309-non-reasoning"),
679 "none"
680 );
681 assert_eq!(reasoning_type_for_model("xai-auth/grok-9000"), "unknown");
682 assert_eq!(
684 reasoning_type_for_model("anthropic/claude-opus-4-7"),
685 "adaptive"
686 );
687 assert_eq!(
688 reasoning_type_for_model("anthropic/claude-sonnet-4-6"),
689 "budget (legacy)"
690 );
691 assert_eq!(
692 reasoning_type_for_model("claude-sonnet-4-6"),
693 "budget (legacy)"
694 );
695 assert_eq!(
697 reasoning_type_for_model("anthropic/claude-future-x"),
698 "budget (legacy)"
699 );
700 assert_eq!(
702 reasoning_type_for_model("groq/llama-3.3-70b-versatile"),
703 "unverified"
704 );
705 }
706
707 #[test]
711 fn cached_unknown_reasoning_does_not_shadow_static_capability() {
712 let mut m =
713 super::super::CatalogModel::new("anthropic", "Anthropic", "claude-opus-4-7").unwrap();
714 m.reasoning = crate::runtime::openai::catalog::ReasoningSupport::Unknown;
715 capability_cache::insert(m);
716 assert_eq!(
717 reasoning_type_for_model("anthropic/claude-opus-4-7"),
718 "adaptive",
719 "cached Unknown must fall through to the static adaptive entry"
720 );
721 }
722
723 #[test]
724 fn reasoning_type_none_for_live_no_thinking_anthropic_evidence() {
725 let mut m = super::super::CatalogModel::new(
726 "anthropic",
727 "Anthropic",
728 "claude-test-reasoning-type-nothink",
729 )
730 .unwrap();
731 m.provider_kind = super::super::CatalogProviderKind::Anthropic;
732 m.reasoning = ReasoningSupport::None;
733 m.source = super::super::CatalogSource::Live;
734 capability_cache::insert(m);
735 assert_eq!(
736 reasoning_type_for_model("anthropic/claude-test-reasoning-type-nothink"),
737 "none"
738 );
739 }
740
741 #[test]
742 fn anthropic_options_derive_from_capabilities() {
743 assert_eq!(
744 thinking_options_for_model("anthropic/claude-opus-4-7"),
745 vec!["off", "adaptive", "low", "medium", "high", "xhigh"]
746 );
747 assert_eq!(
749 thinking_options_for_model("claude-sonnet-4-6"),
750 vec!["off", "adaptive", "low", "medium", "high", "xhigh"]
751 );
752 }
753
754 #[test]
755 fn options_never_advertise_max_ultra_off_codex() {
756 for model in [
757 "anthropic/claude-opus-4-7",
758 "xai-auth/grok-4.5",
759 "groq/llama-3.3-70b-versatile",
760 ] {
761 let opts = thinking_options_for_model(model);
762 assert!(!opts.contains(&"max".to_string()), "{model}");
763 assert!(!opts.contains(&"ultra".to_string()), "{model}");
764 }
765 }
766
767 #[test]
768 fn kimi_k3_accepts_exact_efforts_and_rejects_off_medium_xhigh_ultra() {
769 let model = "kimi/kimi-k3";
770 for level in [Adaptive, Low, High, Max] {
771 assert!(
772 validate_reasoning_mutation(model, level).is_ok(),
773 "{model} {level}"
774 );
775 }
776 for level in [Off, Medium, XHigh, Ultra, UltraCode] {
777 let err = validate_reasoning_mutation(model, level).unwrap_err();
778 assert!(err.contains("kimi"), "{err}");
779 }
780 }
781
782 #[test]
783 fn kimi_always_thinking_models_accept_only_adaptive() {
784 for model in ["kimi/kimi-k2.7-code", "kimi/kimi-k2.7-code-highspeed"] {
785 assert!(
786 validate_reasoning_mutation(model, Adaptive).is_ok(),
787 "{model}"
788 );
789 for level in [Off, Low, Medium, High, Max] {
790 assert!(
791 validate_reasoning_mutation(model, level).is_err(),
792 "{model} {level}"
793 );
794 }
795 }
796 }
797
798 #[test]
799 fn kimi_toggleable_models_accept_off_and_adaptive_only() {
800 for model in ["kimi/kimi-k2.6", "kimi/kimi-k2.5"] {
801 for level in [Off, Adaptive] {
802 assert!(
803 validate_reasoning_mutation(model, level).is_ok(),
804 "{model} {level}"
805 );
806 }
807 for level in [Low, Medium, High, Max] {
808 assert!(
809 validate_reasoning_mutation(model, level).is_err(),
810 "{model} {level}"
811 );
812 }
813 }
814 }
815
816 #[test]
817 fn kimi_unknown_ids_fail_closed_except_adaptive() {
818 let model = "kimi/kimi-k9000";
819 assert!(validate_reasoning_mutation(model, Adaptive).is_ok());
820 for level in [Off, Low, Medium, High, Max, Ultra] {
821 assert!(
822 validate_reasoning_mutation(model, level).is_err(),
823 "{level}"
824 );
825 }
826 }
827
828 #[test]
829 fn kimi_options_are_exact_per_model() {
830 assert_eq!(
831 thinking_options_for_model("kimi/kimi-k3"),
832 vec!["adaptive", "low", "high", "max"]
833 );
834 for model in ["kimi/kimi-k2.7-code", "kimi/kimi-k2.7-code-highspeed"] {
835 assert_eq!(
836 thinking_options_for_model(model),
837 vec!["adaptive"],
838 "{model}"
839 );
840 }
841 for model in ["kimi/kimi-k2.6", "kimi/kimi-k2.5"] {
842 assert_eq!(
843 thinking_options_for_model(model),
844 vec!["off", "adaptive"],
845 "{model}"
846 );
847 }
848 assert_eq!(
849 thinking_options_for_model("kimi/kimi-k9000"),
850 vec!["adaptive"]
851 );
852 }
853
854 #[test]
855 fn kimi_reasoning_types_and_default_level() {
856 assert_eq!(reasoning_type_for_model("kimi/kimi-k3"), "effort");
857 assert_eq!(reasoning_type_for_model("kimi/kimi-k2.7-code"), "intrinsic");
858 assert_eq!(
859 reasoning_type_for_model("kimi/kimi-k2.6"),
860 "toggle (on/off)"
861 );
862 assert_eq!(reasoning_type_for_model("kimi/kimi-k9000"), "unknown");
863 assert_eq!(
864 default_level_for_model("kimi/kimi-k3"),
865 Some(ReasoningLevel::Max)
866 );
867 assert_eq!(
868 default_level_for_model("kimi/kimi-k2.6"),
869 Some(ReasoningLevel::Adaptive)
870 );
871 }
872}