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 supported: vec![Low, Medium, High, XHigh, Max, Ultra],
551 default_level: Some(High),
552 multi_agent_version: version,
553 };
554 capability_cache::insert(live);
555
556 assert!(
557 validate_reasoning_mutation(&qualified, Max).is_ok(),
558 "Max does not require v2: {qualified}"
559 );
560 let err = validate_reasoning_mutation(&qualified, Ultra)
561 .expect_err("Ultra must fail closed without exact v2 evidence");
562 assert!(err.contains("multi-agent v2"), "{err}");
563
564 let options = thinking_options_for_model(&qualified);
565 assert!(options.contains(&"max".to_string()), "{options:?}");
566 assert!(!options.contains(&"ultra".to_string()), "{options:?}");
567 }
568 }
569
570 #[test]
573 fn anthropic_accepts_budget_expressible_levels_and_rejects_extended() {
574 for model in ["anthropic/claude-opus-4-7", "claude-sonnet-4-6"] {
575 for level in [Off, Adaptive, Low, Medium, High, XHigh] {
576 assert!(
577 validate_reasoning_mutation(model, level).is_ok(),
578 "{model} {level}"
579 );
580 }
581 for level in [Max, Ultra, UltraCode] {
582 assert!(
583 validate_reasoning_mutation(model, level).is_err(),
584 "{model} {level}"
585 );
586 }
587 }
588 }
589
590 #[test]
591 fn anthropic_live_no_thinking_evidence_rejects_named() {
592 let mut m = super::super::CatalogModel::new(
593 "anthropic",
594 "Anthropic",
595 "claude-test-validation-nothink",
596 )
597 .unwrap();
598 m.provider_kind = super::super::CatalogProviderKind::Anthropic;
599 m.reasoning = ReasoningSupport::None;
600 m.source = super::super::CatalogSource::Live;
601 capability_cache::insert(m);
602 let model = "anthropic/claude-test-validation-nothink";
603 for level in [Off, Adaptive] {
604 assert!(validate_reasoning_mutation(model, level).is_ok(), "{level}");
605 }
606 for level in [Low, Medium, High, XHigh] {
607 assert!(
608 validate_reasoning_mutation(model, level).is_err(),
609 "{level}"
610 );
611 }
612 }
613
614 #[test]
617 fn default_levels_come_from_exact_descriptors() {
618 assert_eq!(default_level_for_model("xai-auth/grok-4.5"), Some(High));
619 assert_eq!(
620 default_level_for_model("xai-auth/grok-4.5-latest"),
621 Some(High)
622 );
623 assert_eq!(default_level_for_model("xai-auth/grok-4.6"), Some(High));
624 assert_eq!(
625 default_level_for_model("xai-auth/grok-4.20-multi-agent-0309"),
626 Some(Adaptive)
627 );
628 assert_eq!(default_level_for_model("xai-auth/grok-4.3"), Some(Adaptive));
629 assert_eq!(
630 default_level_for_model("xai-auth/grok-4.20-0309-non-reasoning"),
631 Some(Adaptive)
632 );
633 assert_eq!(default_level_for_model("anthropic/claude-opus-4-7"), None);
634 assert_eq!(
635 default_level_for_model("openai-codex/gpt-5.3-codex-spark"),
636 Some(High)
637 );
638 }
639
640 #[test]
643 fn xai_options_derive_from_exact_capabilities() {
644 assert_eq!(
645 thinking_options_for_model("xai-auth/grok-4.5"),
646 vec!["adaptive", "low", "medium", "high"]
647 );
648 assert_eq!(
649 thinking_options_for_model("xai-auth/grok-4.6"),
650 vec!["adaptive", "low", "medium", "high"]
651 );
652 assert_eq!(
653 thinking_options_for_model("xai-auth/grok-4.20-multi-agent-0309"),
654 vec!["adaptive", "low", "medium", "high", "xhigh"]
655 );
656 assert_eq!(
657 thinking_options_for_model("xai-auth/grok-4.3"),
658 vec!["adaptive"]
659 );
660 assert_eq!(
661 thinking_options_for_model("xai-auth/grok-4.20-0309-non-reasoning"),
662 vec!["off", "adaptive"]
663 );
664 assert_eq!(
665 thinking_options_for_model("xai-auth/grok-9000"),
666 vec!["adaptive"]
667 );
668 }
669
670 #[test]
673 fn reasoning_type_derives_from_exact_capabilities() {
674 assert_eq!(
676 reasoning_type_for_model("openai-codex/gpt-5.6-sol"),
677 "effort (named)"
678 );
679 assert_eq!(
680 reasoning_type_for_model("openai-codex/gpt-unknown-future"),
681 "effort (named)"
682 );
683 assert_eq!(reasoning_type_for_model("xai-auth/grok-4.5"), "effort");
685 assert_eq!(reasoning_type_for_model("xai-auth/grok-4.6"), "effort");
686 assert_eq!(reasoning_type_for_model("xai-auth/grok-4.3"), "intrinsic");
687 assert_eq!(
688 reasoning_type_for_model("xai-auth/grok-4.20-0309-non-reasoning"),
689 "none"
690 );
691 assert_eq!(reasoning_type_for_model("xai-auth/grok-9000"), "unknown");
692 assert_eq!(
694 reasoning_type_for_model("anthropic/claude-opus-4-7"),
695 "adaptive"
696 );
697 assert_eq!(
698 reasoning_type_for_model("anthropic/claude-sonnet-4-6"),
699 "budget (legacy)"
700 );
701 assert_eq!(
702 reasoning_type_for_model("claude-sonnet-4-6"),
703 "budget (legacy)"
704 );
705 assert_eq!(
707 reasoning_type_for_model("anthropic/claude-future-x"),
708 "budget (legacy)"
709 );
710 assert_eq!(
712 reasoning_type_for_model("groq/llama-3.3-70b-versatile"),
713 "unverified"
714 );
715 }
716
717 #[test]
721 fn cached_unknown_reasoning_does_not_shadow_static_capability() {
722 let mut m =
723 super::super::CatalogModel::new("anthropic", "Anthropic", "claude-opus-4-7").unwrap();
724 m.reasoning = crate::runtime::openai::catalog::ReasoningSupport::Unknown;
725 capability_cache::insert(m);
726 assert_eq!(
727 reasoning_type_for_model("anthropic/claude-opus-4-7"),
728 "adaptive",
729 "cached Unknown must fall through to the static adaptive entry"
730 );
731 }
732
733 #[test]
734 fn reasoning_type_none_for_live_no_thinking_anthropic_evidence() {
735 let mut m = super::super::CatalogModel::new(
736 "anthropic",
737 "Anthropic",
738 "claude-test-reasoning-type-nothink",
739 )
740 .unwrap();
741 m.provider_kind = super::super::CatalogProviderKind::Anthropic;
742 m.reasoning = ReasoningSupport::None;
743 m.source = super::super::CatalogSource::Live;
744 capability_cache::insert(m);
745 assert_eq!(
746 reasoning_type_for_model("anthropic/claude-test-reasoning-type-nothink"),
747 "none"
748 );
749 }
750
751 #[test]
752 fn anthropic_options_derive_from_capabilities() {
753 assert_eq!(
754 thinking_options_for_model("anthropic/claude-opus-4-7"),
755 vec!["off", "adaptive", "low", "medium", "high", "xhigh"]
756 );
757 assert_eq!(
759 thinking_options_for_model("claude-sonnet-4-6"),
760 vec!["off", "adaptive", "low", "medium", "high", "xhigh"]
761 );
762 }
763
764 #[test]
765 fn options_never_advertise_max_ultra_off_codex() {
766 for model in [
767 "anthropic/claude-opus-4-7",
768 "xai-auth/grok-4.5",
769 "groq/llama-3.3-70b-versatile",
770 ] {
771 let opts = thinking_options_for_model(model);
772 assert!(!opts.contains(&"max".to_string()), "{model}");
773 assert!(!opts.contains(&"ultra".to_string()), "{model}");
774 }
775 }
776
777 #[test]
778 fn kimi_k3_accepts_exact_efforts_and_rejects_off_medium_xhigh_ultra() {
779 let model = "kimi/kimi-k3";
780 for level in [Adaptive, Low, High, Max] {
781 assert!(
782 validate_reasoning_mutation(model, level).is_ok(),
783 "{model} {level}"
784 );
785 }
786 for level in [Off, Medium, XHigh, Ultra, UltraCode] {
787 let err = validate_reasoning_mutation(model, level).unwrap_err();
788 assert!(err.contains("kimi"), "{err}");
789 }
790 }
791
792 #[test]
793 fn kimi_always_thinking_models_accept_only_adaptive() {
794 for model in ["kimi/kimi-k2.7-code", "kimi/kimi-k2.7-code-highspeed"] {
795 assert!(
796 validate_reasoning_mutation(model, Adaptive).is_ok(),
797 "{model}"
798 );
799 for level in [Off, Low, Medium, High, Max] {
800 assert!(
801 validate_reasoning_mutation(model, level).is_err(),
802 "{model} {level}"
803 );
804 }
805 }
806 }
807
808 #[test]
809 fn kimi_toggleable_models_accept_off_and_adaptive_only() {
810 for model in ["kimi/kimi-k2.6", "kimi/kimi-k2.5"] {
811 for level in [Off, Adaptive] {
812 assert!(
813 validate_reasoning_mutation(model, level).is_ok(),
814 "{model} {level}"
815 );
816 }
817 for level in [Low, Medium, High, Max] {
818 assert!(
819 validate_reasoning_mutation(model, level).is_err(),
820 "{model} {level}"
821 );
822 }
823 }
824 }
825
826 #[test]
827 fn kimi_unknown_ids_fail_closed_except_adaptive() {
828 let model = "kimi/kimi-k9000";
829 assert!(validate_reasoning_mutation(model, Adaptive).is_ok());
830 for level in [Off, Low, Medium, High, Max, Ultra] {
831 assert!(
832 validate_reasoning_mutation(model, level).is_err(),
833 "{level}"
834 );
835 }
836 }
837
838 #[test]
839 fn kimi_options_are_exact_per_model() {
840 assert_eq!(
841 thinking_options_for_model("kimi/kimi-k3"),
842 vec!["adaptive", "low", "high", "max"]
843 );
844 for model in ["kimi/kimi-k2.7-code", "kimi/kimi-k2.7-code-highspeed"] {
845 assert_eq!(
846 thinking_options_for_model(model),
847 vec!["adaptive"],
848 "{model}"
849 );
850 }
851 for model in ["kimi/kimi-k2.6", "kimi/kimi-k2.5"] {
852 assert_eq!(
853 thinking_options_for_model(model),
854 vec!["off", "adaptive"],
855 "{model}"
856 );
857 }
858 assert_eq!(
859 thinking_options_for_model("kimi/kimi-k9000"),
860 vec!["adaptive"]
861 );
862 }
863
864 #[test]
865 fn kimi_reasoning_types_and_default_level() {
866 assert_eq!(reasoning_type_for_model("kimi/kimi-k3"), "effort");
867 assert_eq!(reasoning_type_for_model("kimi/kimi-k2.7-code"), "intrinsic");
868 assert_eq!(
869 reasoning_type_for_model("kimi/kimi-k2.6"),
870 "toggle (on/off)"
871 );
872 assert_eq!(reasoning_type_for_model("kimi/kimi-k9000"), "unknown");
873 assert_eq!(
874 default_level_for_model("kimi/kimi-k3"),
875 Some(ReasoningLevel::Max)
876 );
877 assert_eq!(
878 default_level_for_model("kimi/kimi-k2.6"),
879 Some(ReasoningLevel::Adaptive)
880 );
881 }
882}