1use std::fmt;
2use std::str::FromStr;
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum AgentKind {
7 Antigravity,
9 Gemini,
11 Claude,
13 Codex,
15}
16
17#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct AgentCliInfo {
21 pub executable_name: &'static str,
23 pub kind: AgentKind,
25 pub version: AgentCliVersion,
27}
28
29#[derive(Debug, Clone, PartialEq, Eq)]
32pub enum AgentCliVersion {
33 Loading,
36 Unknown,
39 Value(String),
41}
42
43impl AgentCliInfo {
44 pub fn new(kind: AgentKind, version: Option<String>) -> Self {
46 Self {
47 executable_name: kind.executable_name(),
48 kind,
49 version: version.map_or(AgentCliVersion::Unknown, AgentCliVersion::Value),
50 }
51 }
52
53 pub fn loading(kind: AgentKind) -> Self {
56 Self {
57 executable_name: kind.executable_name(),
58 kind,
59 version: AgentCliVersion::Loading,
60 }
61 }
62
63 pub fn from_kinds(agent_kinds: &[AgentKind]) -> Vec<Self> {
66 agent_kinds
67 .iter()
68 .copied()
69 .map(|agent_kind| Self::new(agent_kind, None))
70 .collect()
71 }
72
73 pub fn loading_from_kinds(agent_kinds: &[AgentKind]) -> Vec<Self> {
76 agent_kinds.iter().copied().map(Self::loading).collect()
77 }
78}
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86pub enum AgentModel {
87 Gpt56Sol,
89 Gpt56Terra,
91 Gpt56Luna,
93 Gpt55,
95 Gemini3FlashPreview,
97 Gemini35Flash,
99 Gemini31FlashLitePreview,
102 Gemini31ProPreview,
104 Gpt53CodexSpark,
106 ClaudeOpus48,
108 ClaudeSonnet5,
110 ClaudeFable5,
112 ClaudeHaiku4520251001,
114}
115
116#[derive(Debug, Clone, Copy, PartialEq, Eq)]
118pub struct AgentSelection {
119 kind: AgentKind,
120 model: AgentModel,
121}
122
123#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
125pub enum ReasoningLevel {
126 Low,
128 Medium,
130 #[default]
132 High,
133 XHigh,
135 Max,
137}
138
139impl AgentSelection {
140 #[must_use]
145 pub fn new(kind: AgentKind, model: AgentModel) -> Self {
146 let model = if kind.supports_model(model) {
147 model
148 } else {
149 kind.default_model()
150 };
151
152 Self { kind, model }
153 }
154
155 #[must_use]
157 pub fn kind(self) -> AgentKind {
158 self.kind
159 }
160
161 #[must_use]
163 pub fn model(self) -> AgentModel {
164 self.model
165 }
166}
167
168pub trait AgentSelectionMetadata {
170 fn name(&self) -> &'static str;
172
173 fn description(&self) -> &'static str;
175}
176
177impl AgentModel {
178 pub fn as_str(self) -> &'static str {
181 match self {
182 Self::Gpt56Sol => "gpt-5.6-sol",
183 Self::Gpt56Terra => "gpt-5.6-terra",
184 Self::Gpt56Luna => "gpt-5.6-luna",
185 Self::Gpt55 => "gpt-5.5",
186 Self::Gemini3FlashPreview => "gemini-3-flash-preview",
187 Self::Gemini35Flash => "gemini-3.5-flash",
188 Self::Gemini31FlashLitePreview => "gemini-3.1-flash-lite-preview",
189 Self::Gemini31ProPreview => "gemini-3.1-pro-preview",
190 Self::Gpt53CodexSpark => "gpt-5.3-codex-spark",
191 Self::ClaudeOpus48 => "claude-opus-4-8",
192 Self::ClaudeSonnet5 => "claude-sonnet-5",
193 Self::ClaudeFable5 => "claude-fable-5",
194 Self::ClaudeHaiku4520251001 => "claude-haiku-4-5-20251001",
195 }
196 }
197
198 pub fn provider_model_str(self) -> &'static str {
203 self.as_str()
204 }
205
206 pub fn parse_persisted(value: &str) -> Result<Self, String> {
218 match value {
219 "claude-opus-4-6" | "claude-opus-4-7" => Ok(Self::ClaudeOpus48),
220 "claude-sonnet-4-6" => Ok(Self::ClaudeSonnet5),
221 "gpt-5.4-mini" => Ok(Self::Gpt56Luna),
222 "gpt-5.4" => Ok(Self::Gpt55),
223 _ => value.parse(),
224 }
225 }
226}
227
228pub fn parse_persisted_session_agent_model(
237 agent_value: Option<&str>,
238 model_value: &str,
239) -> AgentSelection {
240 let parsed_agent = agent_value
241 .filter(|value| !value.trim().is_empty())
242 .and_then(|value| value.parse::<AgentKind>().ok());
243
244 if let Some(agent_kind) = parsed_agent {
245 return parse_model_for_persisted_agent(agent_kind, model_value);
246 }
247
248 let model = AgentModel::parse_persisted(model_value)
249 .unwrap_or_else(|_| AgentKind::Antigravity.default_model());
250 let agent_kind = legacy_agent_kind_for_model_value(model_value, model);
251
252 AgentSelection::new(agent_kind, model)
253}
254
255fn parse_model_for_persisted_agent(agent_kind: AgentKind, model_value: &str) -> AgentSelection {
258 if let Some(model) = agent_kind.parse_model(model_value) {
259 return AgentSelection::new(agent_kind, model);
260 }
261
262 if let Ok(model) = AgentModel::parse_persisted(model_value)
263 && agent_kind.supports_model(model)
264 {
265 return AgentSelection::new(agent_kind, model);
266 }
267
268 AgentSelection::new(agent_kind, agent_kind.default_model())
269}
270
271fn legacy_agent_kind_for_model_value(model_value: &str, model: AgentModel) -> AgentKind {
274 if model_value.starts_with("claude-") {
275 return AgentKind::Claude;
276 }
277
278 if model_value.starts_with("gpt-") {
279 return AgentKind::Codex;
280 }
281
282 if model_value.starts_with("gemini-") {
283 return AgentKind::Antigravity;
284 }
285
286 AgentKind::ALL
287 .iter()
288 .copied()
289 .find(|agent_kind| agent_kind.supports_model(model))
290 .unwrap_or(AgentKind::Antigravity)
291}
292
293#[must_use]
296pub fn selectable_models_for_agent_kinds(agent_kinds: &[AgentKind]) -> Vec<AgentModel> {
297 let mut models = Vec::new();
298 for model in agent_kinds
299 .iter()
300 .flat_map(|agent_kind| agent_kind.models())
301 .copied()
302 {
303 if !models.contains(&model) {
304 models.push(model);
305 }
306 }
307
308 models
309}
310
311#[must_use]
318pub fn resolve_model_for_available_agent_kinds(
319 model: AgentModel,
320 agent_kinds: &[AgentKind],
321 fallback_model: AgentModel,
322) -> AgentModel {
323 if agent_kinds
324 .iter()
325 .any(|agent_kind| agent_kind.supports_model(model))
326 {
327 return model;
328 }
329
330 if agent_kinds
331 .iter()
332 .any(|agent_kind| agent_kind.supports_model(fallback_model))
333 {
334 return fallback_model;
335 }
336
337 agent_kinds
338 .first()
339 .copied()
340 .map_or(fallback_model, AgentKind::default_model)
341}
342
343#[must_use]
350pub fn resolve_agent_kind_for_model(
351 model: AgentModel,
352 agent_kinds: &[AgentKind],
353 fallback_agent_kind: AgentKind,
354) -> AgentKind {
355 agent_kinds
356 .iter()
357 .copied()
358 .find(|agent_kind| agent_kind.supports_model(model))
359 .unwrap_or(fallback_agent_kind)
360}
361
362#[must_use]
369pub fn resolve_agent_selection_for_model(
370 model: AgentModel,
371 preferred_agent_kind: AgentKind,
372 agent_kinds: &[AgentKind],
373) -> AgentSelection {
374 let agent_kind = if preferred_agent_kind.supports_model(model) {
375 preferred_agent_kind
376 } else {
377 resolve_agent_kind_for_model(model, agent_kinds, preferred_agent_kind)
378 };
379
380 AgentSelection::new(agent_kind, model)
381}
382
383#[must_use]
389pub fn resolve_prompt_model_agent_kind(
390 session_agent_kind: AgentKind,
391 agent_kinds: &[AgentKind],
392) -> Option<AgentKind> {
393 if agent_kinds.contains(&session_agent_kind) {
394 return Some(session_agent_kind);
395 }
396
397 agent_kinds.first().copied()
398}
399
400impl ReasoningLevel {
401 pub const ALL: [Self; 5] = [Self::Low, Self::Medium, Self::High, Self::XHigh, Self::Max];
403
404 pub fn as_str(self) -> &'static str {
409 match self {
410 Self::Low => "low",
411 Self::Medium => "medium",
412 Self::High => "high",
413 Self::XHigh => "xhigh",
414 Self::Max => "max",
415 }
416 }
417
418 pub fn codex(self) -> &'static str {
420 match self {
421 Self::Low => "low",
422 Self::Medium => "medium",
423 Self::High => "high",
424 Self::XHigh => "xhigh",
425 Self::Max => "max",
426 }
427 }
428
429 pub fn claude(self) -> &'static str {
435 match self {
436 Self::Low => "low",
437 Self::Medium => "medium",
438 Self::High => "high",
439 Self::XHigh | Self::Max => "max",
440 }
441 }
442
443 pub fn description(self) -> &'static str {
445 match self {
446 Self::Low => "Fastest responses with lighter reasoning.",
447 Self::Medium => "Balanced speed and reasoning depth.",
448 Self::High => "Deeper reasoning for tougher tasks.",
449 Self::XHigh => "Extra-high reasoning for complex tasks.",
450 Self::Max => "Maximum reasoning effort for the hardest tasks.",
451 }
452 }
453}
454
455impl FromStr for ReasoningLevel {
456 type Err = String;
457
458 fn from_str(value: &str) -> Result<Self, Self::Err> {
459 match value {
460 "low" => Ok(Self::Low),
461 "medium" => Ok(Self::Medium),
462 "high" => Ok(Self::High),
463 "xhigh" => Ok(Self::XHigh),
464 "max" => Ok(Self::Max),
465 other => Err(format!("unknown reasoning level: {other}")),
466 }
467 }
468}
469
470impl FromStr for AgentModel {
471 type Err = String;
472
473 fn from_str(value: &str) -> Result<Self, Self::Err> {
474 match value {
475 "gemini-3-flash-preview" => Ok(Self::Gemini3FlashPreview),
476 "gemini-3.5-flash" => Ok(Self::Gemini35Flash),
477 "gemini-3.1-flash-lite-preview" => Ok(Self::Gemini31FlashLitePreview),
478 "gemini-3.1-pro-preview" => Ok(Self::Gemini31ProPreview),
479 "gpt-5.6-sol" => Ok(Self::Gpt56Sol),
480 "gpt-5.6-terra" => Ok(Self::Gpt56Terra),
481 "gpt-5.6-luna" => Ok(Self::Gpt56Luna),
482 "gpt-5.5" => Ok(Self::Gpt55),
483 "gpt-5.3-codex-spark" => Ok(Self::Gpt53CodexSpark),
484 "claude-opus-4-8" => Ok(Self::ClaudeOpus48),
485 "claude-sonnet-5" => Ok(Self::ClaudeSonnet5),
486 "claude-fable-5" => Ok(Self::ClaudeFable5),
487 "claude-haiku-4-5-20251001" => Ok(Self::ClaudeHaiku4520251001),
488 other => Err(format!("unknown model: {other}")),
489 }
490 }
491}
492
493impl AgentSelectionMetadata for AgentModel {
494 fn name(&self) -> &'static str {
495 (*self).provider_model_str()
496 }
497
498 fn description(&self) -> &'static str {
499 match self {
500 Self::Gemini31ProPreview => "Higher-quality Gemini model for deeper reasoning.",
501 Self::Gemini35Flash => "Fast Gemini model for current Flash workloads.",
502 Self::Gemini31FlashLitePreview => {
503 "Lightweight Gemini model for fast, cost-conscious iterations."
504 }
505 Self::Gemini3FlashPreview => "Fast Gemini model for quick iterations.",
506 Self::Gpt56Sol => "Newest Codex model for the strongest coding performance.",
507 Self::Gpt56Terra => "Current Codex model for balanced coding performance.",
508 Self::Gpt56Luna => "Current Codex model for lighter coding iterations.",
509 Self::Gpt55 => "Newer Codex model with stronger coding performance when available.",
510 Self::Gpt53CodexSpark => "Codex spark model for quick coding iterations.",
511 Self::ClaudeOpus48 => "Latest Claude Opus model for complex tasks.",
512 Self::ClaudeSonnet5 => "Balanced Claude model for quality and latency.",
513 Self::ClaudeFable5 => "Claude Fable model for creative, narrative-heavy tasks.",
514 Self::ClaudeHaiku4520251001 => "Fast Claude model for lighter tasks.",
515 }
516 }
517}
518
519impl AgentKind {
520 pub const ALL: &[AgentKind] = &[
522 AgentKind::Gemini,
523 AgentKind::Antigravity,
524 AgentKind::Claude,
525 AgentKind::Codex,
526 ];
527
528 pub fn executable_name(self) -> &'static str {
530 match self {
531 Self::Antigravity => "agy",
532 Self::Gemini => "gemini",
533 Self::Claude => "claude",
534 Self::Codex => "codex",
535 }
536 }
537
538 pub fn default_model(self) -> AgentModel {
540 match self {
541 Self::Antigravity | Self::Gemini => AgentModel::Gemini31ProPreview,
542 Self::Claude => AgentModel::ClaudeFable5,
543 Self::Codex => AgentModel::Gpt56Sol,
544 }
545 }
546
547 pub fn model_str(self, model: AgentModel) -> Option<&'static str> {
549 if !self.supports_model(model) {
550 return None;
551 }
552
553 Some(model.as_str())
554 }
555
556 pub fn models(self) -> &'static [AgentModel] {
558 const ANTIGRAVITY_MODELS: &[AgentModel] = &[
559 AgentModel::Gemini31ProPreview,
560 AgentModel::Gemini35Flash,
561 AgentModel::Gemini31FlashLitePreview,
562 AgentModel::Gemini3FlashPreview,
563 ];
564 const GEMINI_MODELS: &[AgentModel] = &[
565 AgentModel::Gemini31ProPreview,
566 AgentModel::Gemini35Flash,
567 AgentModel::Gemini31FlashLitePreview,
568 AgentModel::Gemini3FlashPreview,
569 ];
570 const CLAUDE_MODELS: &[AgentModel] = &[
571 AgentModel::ClaudeFable5,
572 AgentModel::ClaudeOpus48,
573 AgentModel::ClaudeSonnet5,
574 AgentModel::ClaudeHaiku4520251001,
575 ];
576 const CODEX_MODELS: &[AgentModel] = &[
577 AgentModel::Gpt56Sol,
578 AgentModel::Gpt56Terra,
579 AgentModel::Gpt56Luna,
580 AgentModel::Gpt55,
581 AgentModel::Gpt53CodexSpark,
582 ];
583
584 match self {
585 Self::Antigravity => ANTIGRAVITY_MODELS,
586 Self::Gemini => GEMINI_MODELS,
587 Self::Claude => CLAUDE_MODELS,
588 Self::Codex => CODEX_MODELS,
589 }
590 }
591
592 pub fn parse_model(self, value: &str) -> Option<AgentModel> {
594 let model = value.parse::<AgentModel>().ok()?;
595 if !self.supports_model(model) {
596 return None;
597 }
598
599 Some(model)
600 }
601
602 pub fn supports_model(self, model: AgentModel) -> bool {
604 self.models().contains(&model)
605 }
606}
607
608impl AgentSelectionMetadata for AgentKind {
609 fn name(&self) -> &'static str {
610 match self {
611 Self::Antigravity => "antigravity",
612 Self::Gemini => "gemini",
613 Self::Claude => "claude",
614 Self::Codex => "codex",
615 }
616 }
617
618 fn description(&self) -> &'static str {
619 match self {
620 Self::Antigravity => "Google Antigravity CLI agent.",
621 Self::Gemini => "Google Gemini CLI agent.",
622 Self::Claude => "Anthropic Claude Code agent.",
623 Self::Codex => "OpenAI Codex CLI agent.",
624 }
625 }
626}
627
628impl AgentSelectionMetadata for ReasoningLevel {
629 fn name(&self) -> &'static str {
630 (*self).as_str()
631 }
632
633 fn description(&self) -> &'static str {
634 (*self).description()
635 }
636}
637
638impl fmt::Display for AgentKind {
639 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
640 write!(f, "{}", self.name())
641 }
642}
643
644impl FromStr for AgentKind {
645 type Err = String;
646
647 fn from_str(s: &str) -> Result<Self, Self::Err> {
648 match s.to_lowercase().as_str() {
649 "antigravity" | "agy" => Ok(Self::Antigravity),
650 "gemini" => Ok(Self::Gemini),
651 "claude" => Ok(Self::Claude),
652 "codex" => Ok(Self::Codex),
653 other => Err(format!("unknown agent kind: {other}")),
654 }
655 }
656}
657
658#[cfg(test)]
659mod tests {
660 use super::*;
661
662 #[test]
663 fn test_parse_model_returns_none_for_models_from_other_providers() {
665 let claude_kind = AgentKind::Claude;
667 let antigravity_model = AgentModel::Gemini3FlashPreview.as_str();
668
669 let parsed_model = claude_kind.parse_model(antigravity_model);
671
672 assert_eq!(parsed_model, None);
674 }
675
676 #[test]
677 fn test_parse_model_parses_current_codex_models() {
679 let codex_kind = AgentKind::Codex;
681
682 let parsed_sol = codex_kind.parse_model("gpt-5.6-sol");
684 let parsed_terra = codex_kind.parse_model("gpt-5.6-terra");
685 let parsed_luna = codex_kind.parse_model("gpt-5.6-luna");
686 let parsed_55 = codex_kind.parse_model("gpt-5.5");
687 let parsed_spark = codex_kind.parse_model("gpt-5.3-codex-spark");
688
689 assert_eq!(parsed_sol, Some(AgentModel::Gpt56Sol));
691 assert_eq!(parsed_terra, Some(AgentModel::Gpt56Terra));
692 assert_eq!(parsed_luna, Some(AgentModel::Gpt56Luna));
693 assert_eq!(parsed_55, Some(AgentModel::Gpt55));
694 assert_eq!(parsed_spark, Some(AgentModel::Gpt53CodexSpark));
695 }
696
697 #[test]
698 fn test_parse_model_parses_antigravity() {
701 let antigravity_kind = AgentKind::Antigravity;
703
704 let parsed_model = antigravity_kind.parse_model("gemini-3.5-flash");
706
707 assert_eq!(parsed_model, Some(AgentModel::Gemini35Flash));
709 }
710
711 #[test]
712 fn test_parse_model_parses_direct_gemini() {
715 let gemini_kind = AgentKind::Gemini;
717
718 let parsed_model = gemini_kind.parse_model("gemini-3.5-flash");
720
721 assert_eq!(parsed_model, Some(AgentModel::Gemini35Flash));
723 }
724
725 #[test]
726 fn test_parse_model_rejects_retired_gpt_models() {
728 let codex_kind = AgentKind::Codex;
730
731 let parsed_gpt_54 = codex_kind.parse_model("gpt-5.4");
733 let parsed_gpt_54_mini = codex_kind.parse_model("gpt-5.4-mini");
734
735 assert_eq!(parsed_gpt_54, None);
737 assert_eq!(parsed_gpt_54_mini, None);
738 }
739
740 #[test]
741 fn test_parse_model_rejects_retired_claude_aliases() {
744 let claude_kind = AgentKind::Claude;
746
747 let parsed_opus_46 = claude_kind.parse_model("claude-opus-4-6");
749 let parsed_opus_47 = claude_kind.parse_model("claude-opus-4-7");
750 let parsed_sonnet_46 = claude_kind.parse_model("claude-sonnet-4-6");
751
752 assert_eq!(parsed_opus_46, None);
754 assert_eq!(parsed_opus_47, None);
755 assert_eq!(parsed_sonnet_46, None);
756 }
757
758 #[test]
759 fn test_parse_model_parses_current_claude_models() {
761 let claude_kind = AgentKind::Claude;
763
764 let parsed_sonnet_5 = claude_kind.parse_model("claude-sonnet-5");
766 let parsed_fable_5 = claude_kind.parse_model("claude-fable-5");
767 let parsed_haiku_45 = claude_kind.parse_model("claude-haiku-4-5-20251001");
768
769 assert_eq!(parsed_sonnet_5, Some(AgentModel::ClaudeSonnet5));
771 assert_eq!(parsed_fable_5, Some(AgentModel::ClaudeFable5));
772 assert_eq!(parsed_haiku_45, Some(AgentModel::ClaudeHaiku4520251001));
773 }
774
775 #[test]
776 fn test_parse_persisted_handles_supported_and_retired_models() {
779 let parsed_opus_46 = AgentModel::parse_persisted("claude-opus-4-6");
783 let parsed_opus_47 = AgentModel::parse_persisted("claude-opus-4-7");
784 let parsed_sonnet_46 = AgentModel::parse_persisted("claude-sonnet-4-6");
785 let parsed_sonnet_5 = AgentModel::parse_persisted("claude-sonnet-5");
786 let parsed_gpt_54_mini = AgentModel::parse_persisted("gpt-5.4-mini");
787 let parsed_gpt_54 = AgentModel::parse_persisted("gpt-5.4");
788 let parsed_gemini_35_flash = AgentModel::parse_persisted("gemini-3.5-flash");
789
790 assert_eq!(parsed_opus_46, Ok(AgentModel::ClaudeOpus48));
792 assert_eq!(parsed_opus_47, Ok(AgentModel::ClaudeOpus48));
793 assert_eq!(parsed_sonnet_46, Ok(AgentModel::ClaudeSonnet5));
794 assert_eq!(parsed_sonnet_5, Ok(AgentModel::ClaudeSonnet5));
795 assert_eq!(parsed_gpt_54_mini, Ok(AgentModel::Gpt56Luna));
796 assert_eq!(parsed_gpt_54, Ok(AgentModel::Gpt55));
797 assert_eq!(parsed_gemini_35_flash, Ok(AgentModel::Gemini35Flash));
798 }
799
800 #[test]
801 fn test_parse_persisted_session_agent_model_prefers_saved_agent() {
804 let selection = parse_persisted_session_agent_model(Some("codex"), "gemini-3.5-flash");
808
809 assert_eq!(selection.kind(), AgentKind::Codex);
811 assert_eq!(selection.model(), AgentKind::Codex.default_model());
812 }
813
814 #[test]
815 fn test_parse_persisted_session_agent_model_preserves_antigravity_models() {
818 let selection =
822 parse_persisted_session_agent_model(Some("antigravity"), "gemini-3.5-flash");
823
824 assert_eq!(selection.kind(), AgentKind::Antigravity);
826 assert_eq!(selection.model(), AgentModel::Gemini35Flash);
827 }
828
829 #[test]
830 fn test_parse_persisted_session_agent_model_supports_legacy_rows() {
833 let selection = parse_persisted_session_agent_model(None, "claude-opus-4-6");
837
838 assert_eq!(selection.kind(), AgentKind::Claude);
840 assert_eq!(selection.model(), AgentModel::ClaudeOpus48);
841 }
842
843 #[test]
844 fn test_parse_persisted_session_agent_model_defaults_legacy_gemini_to_antigravity() {
847 let selection = parse_persisted_session_agent_model(None, "gemini-3.5-flash");
851
852 assert_eq!(selection.kind(), AgentKind::Antigravity);
854 assert_eq!(selection.model(), AgentModel::Gemini35Flash);
855 }
856
857 #[test]
858 fn test_reasoning_level_from_str_parses_supported_values() {
860 let low_level = "low".parse::<ReasoningLevel>();
864 let medium_level = "medium".parse::<ReasoningLevel>();
865 let high_level = "high".parse::<ReasoningLevel>();
866 let xhigh_level = "xhigh".parse::<ReasoningLevel>();
867 let max_level = "max".parse::<ReasoningLevel>();
868
869 assert_eq!(low_level, Ok(ReasoningLevel::Low));
871 assert_eq!(medium_level, Ok(ReasoningLevel::Medium));
872 assert_eq!(high_level, Ok(ReasoningLevel::High));
873 assert_eq!(xhigh_level, Ok(ReasoningLevel::XHigh));
874 assert_eq!(max_level, Ok(ReasoningLevel::Max));
875 }
876
877 #[test]
878 fn test_reasoning_level_from_str_rejects_unknown_values() {
880 let parse_result = "minimal".parse::<ReasoningLevel>();
884
885 assert!(parse_result.is_err());
887 }
888
889 #[test]
890 fn test_codex_models_are_supported_by_codex() {
892 let models = [
894 AgentModel::Gpt56Sol,
895 AgentModel::Gpt56Terra,
896 AgentModel::Gpt56Luna,
897 AgentModel::Gpt55,
898 AgentModel::Gpt53CodexSpark,
899 ];
900
901 let supported = models.map(|model| AgentKind::Codex.supports_model(model));
903 let unsupported = models.map(|model| AgentKind::Claude.supports_model(model));
904
905 assert_eq!(supported, [true; 5]);
907 assert_eq!(unsupported, [false; 5]);
908 }
909
910 #[test]
911 fn test_claude_models_are_supported_by_claude() {
913 let models = [
915 AgentModel::ClaudeOpus48,
916 AgentModel::ClaudeSonnet5,
917 AgentModel::ClaudeFable5,
918 AgentModel::ClaudeHaiku4520251001,
919 ];
920
921 let supported = models.map(|model| AgentKind::Claude.supports_model(model));
923 let unsupported = models.map(|model| AgentKind::Codex.supports_model(model));
924
925 assert_eq!(supported, [true; 4]);
927 assert_eq!(unsupported, [false; 4]);
928 }
929
930 #[test]
931 fn test_gemini_models_are_supported_by_gemini_and_antigravity() {
933 let models = [
935 AgentModel::Gemini31ProPreview,
936 AgentModel::Gemini35Flash,
937 AgentModel::Gemini31FlashLitePreview,
938 AgentModel::Gemini3FlashPreview,
939 ];
940
941 let antigravity_supported =
943 models.map(|model| AgentKind::Antigravity.supports_model(model));
944 let gemini_supported = models.map(|model| AgentKind::Gemini.supports_model(model));
945 let codex_supported = models.map(|model| AgentKind::Codex.supports_model(model));
946
947 assert_eq!(antigravity_supported, [true; 4]);
949 assert_eq!(gemini_supported, [true; 4]);
950 assert_eq!(codex_supported, [false; 4]);
951 }
952
953 #[test]
954 fn test_antigravity_provider_model_str_returns_raw_gemini_model() {
957 let model = AgentModel::Gemini31FlashLitePreview;
959
960 let persisted_model = model.as_str();
962 let provider_model = model.provider_model_str();
963
964 assert_eq!(persisted_model, "gemini-3.1-flash-lite-preview");
966 assert_eq!(provider_model, "gemini-3.1-flash-lite-preview");
967 }
968
969 #[test]
970 fn test_reasoning_level_claude_maps_all_levels() {
973 assert_eq!(ReasoningLevel::Low.claude(), "low");
975 assert_eq!(ReasoningLevel::Medium.claude(), "medium");
976 assert_eq!(ReasoningLevel::High.claude(), "high");
977 assert_eq!(ReasoningLevel::XHigh.claude(), "max");
978 assert_eq!(ReasoningLevel::Max.claude(), "max");
979 }
980
981 #[test]
982 fn test_reasoning_level_codex_maps_all_levels() {
984 assert_eq!(ReasoningLevel::Low.codex(), "low");
986 assert_eq!(ReasoningLevel::Medium.codex(), "medium");
987 assert_eq!(ReasoningLevel::High.codex(), "high");
988 assert_eq!(ReasoningLevel::XHigh.codex(), "xhigh");
989 assert_eq!(ReasoningLevel::Max.codex(), "max");
990 }
991
992 #[test]
993 fn test_reasoning_level_as_str_returns_stable_persisted_values() {
996 assert_eq!(ReasoningLevel::Low.as_str(), "low");
998 assert_eq!(ReasoningLevel::Medium.as_str(), "medium");
999 assert_eq!(ReasoningLevel::High.as_str(), "high");
1000 assert_eq!(ReasoningLevel::XHigh.as_str(), "xhigh");
1001 assert_eq!(ReasoningLevel::Max.as_str(), "max");
1002 }
1003
1004 #[test]
1005 fn test_selectable_models_for_agent_kinds_uses_provider_order() {
1007 let agent_kinds = [AgentKind::Codex, AgentKind::Antigravity];
1009
1010 let selectable_models = selectable_models_for_agent_kinds(&agent_kinds);
1012
1013 assert_eq!(
1015 selectable_models,
1016 vec![
1017 AgentModel::Gpt56Sol,
1018 AgentModel::Gpt56Terra,
1019 AgentModel::Gpt56Luna,
1020 AgentModel::Gpt55,
1021 AgentModel::Gpt53CodexSpark,
1022 AgentModel::Gemini31ProPreview,
1023 AgentModel::Gemini35Flash,
1024 AgentModel::Gemini31FlashLitePreview,
1025 AgentModel::Gemini3FlashPreview,
1026 ]
1027 );
1028 }
1029
1030 #[test]
1031 fn test_selectable_models_for_agent_kinds_deduplicates_shared_models() {
1034 let agent_kinds = [AgentKind::Gemini, AgentKind::Antigravity];
1036
1037 let selectable_models = selectable_models_for_agent_kinds(&agent_kinds);
1039
1040 assert_eq!(
1042 selectable_models,
1043 vec![
1044 AgentModel::Gemini31ProPreview,
1045 AgentModel::Gemini35Flash,
1046 AgentModel::Gemini31FlashLitePreview,
1047 AgentModel::Gemini3FlashPreview,
1048 ]
1049 );
1050 }
1051
1052 #[test]
1053 fn test_resolve_model_for_available_agent_kinds_prefers_available_fallback() {
1056 let unavailable_model = AgentModel::ClaudeOpus48;
1058 let available_agent_kinds = [AgentKind::Codex, AgentKind::Antigravity];
1059 let fallback_model = AgentModel::Gpt55;
1060
1061 let resolved_model = resolve_model_for_available_agent_kinds(
1063 unavailable_model,
1064 &available_agent_kinds,
1065 fallback_model,
1066 );
1067
1068 assert_eq!(resolved_model, AgentModel::Gpt55);
1070 }
1071
1072 #[test]
1073 fn test_resolve_model_for_available_agent_kinds_uses_first_available_default() {
1076 let unavailable_model = AgentModel::ClaudeOpus48;
1078 let available_agent_kinds = [AgentKind::Codex, AgentKind::Antigravity];
1079 let unavailable_fallback_model = AgentModel::ClaudeSonnet5;
1080
1081 let resolved_model = resolve_model_for_available_agent_kinds(
1083 unavailable_model,
1084 &available_agent_kinds,
1085 unavailable_fallback_model,
1086 );
1087
1088 assert_eq!(resolved_model, AgentKind::Codex.default_model());
1090 }
1091
1092 #[test]
1093 fn test_resolve_agent_selection_for_model_preserves_preferred_shared_provider() {
1096 let model = AgentModel::Gemini35Flash;
1098 let available_agent_kinds = [AgentKind::Gemini, AgentKind::Antigravity];
1099
1100 let resolved_selection = resolve_agent_selection_for_model(
1102 model,
1103 AgentKind::Antigravity,
1104 &available_agent_kinds,
1105 );
1106
1107 assert_eq!(
1109 resolved_selection,
1110 AgentSelection::new(AgentKind::Antigravity, model)
1111 );
1112 }
1113
1114 #[test]
1115 fn test_resolve_agent_selection_for_model_uses_available_provider_order() {
1118 let model = AgentModel::Gemini35Flash;
1120 let available_agent_kinds = [AgentKind::Gemini, AgentKind::Antigravity];
1121
1122 let resolved_selection =
1124 resolve_agent_selection_for_model(model, AgentKind::Codex, &available_agent_kinds);
1125
1126 assert_eq!(
1128 resolved_selection,
1129 AgentSelection::new(AgentKind::Gemini, model)
1130 );
1131 }
1132
1133 #[test]
1134 fn test_resolve_prompt_model_agent_kind_prefers_current_agent() {
1137 let available_agent_kinds = [AgentKind::Antigravity, AgentKind::Codex];
1139
1140 let resolved_agent_kind =
1142 resolve_prompt_model_agent_kind(AgentKind::Codex, &available_agent_kinds);
1143
1144 assert_eq!(resolved_agent_kind, Some(AgentKind::Codex));
1146 }
1147
1148 #[test]
1149 fn test_resolve_prompt_model_agent_kind_uses_first_available_agent() {
1152 let available_agent_kinds = [AgentKind::Antigravity, AgentKind::Codex];
1154
1155 let resolved_agent_kind =
1157 resolve_prompt_model_agent_kind(AgentKind::Claude, &available_agent_kinds);
1158
1159 assert_eq!(resolved_agent_kind, Some(AgentKind::Antigravity));
1161 }
1162}