1use std::fmt;
2use std::str::FromStr;
3
4use super::session::SpeedMode;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum AgentKind {
9 Antigravity,
11 Gemini,
13 Claude,
15 Codex,
17}
18
19#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct AgentCliInfo {
23 pub executable_name: &'static str,
25 pub kind: AgentKind,
27 pub version: AgentCliVersion,
29}
30
31#[derive(Debug, Clone, PartialEq, Eq)]
34pub enum AgentCliVersion {
35 Loading,
38 Unknown,
41 Value(String),
43}
44
45impl AgentCliInfo {
46 pub fn new(kind: AgentKind, version: Option<String>) -> Self {
48 Self {
49 executable_name: kind.executable_name(),
50 kind,
51 version: version.map_or(AgentCliVersion::Unknown, AgentCliVersion::Value),
52 }
53 }
54
55 pub fn loading(kind: AgentKind) -> Self {
58 Self {
59 executable_name: kind.executable_name(),
60 kind,
61 version: AgentCliVersion::Loading,
62 }
63 }
64
65 pub fn from_kinds(agent_kinds: &[AgentKind]) -> Vec<Self> {
68 agent_kinds
69 .iter()
70 .copied()
71 .map(|agent_kind| Self::new(agent_kind, None))
72 .collect()
73 }
74
75 pub fn loading_from_kinds(agent_kinds: &[AgentKind]) -> Vec<Self> {
78 agent_kinds.iter().copied().map(Self::loading).collect()
79 }
80}
81
82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
88pub enum AgentModel {
89 Gpt56Sol,
91 Gpt56Terra,
93 Gpt56Luna,
95 Gemini37Flash,
97 Gemini35FlashLite,
99 Gemini31Pro,
101 Gpt53CodexSpark,
103 ClaudeOpus5,
105 ClaudeSonnet5,
107 ClaudeFable5,
109 ClaudeHaiku4520251001,
111}
112
113#[derive(Debug, Clone, Copy, PartialEq, Eq)]
115pub struct AgentSelection {
116 kind: AgentKind,
117 model: AgentModel,
118}
119
120#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
122pub enum ReasoningLevel {
123 Low,
125 Medium,
127 #[default]
129 High,
130 XHigh,
132 Max,
134}
135
136impl AgentSelection {
137 #[must_use]
142 pub fn new(kind: AgentKind, model: AgentModel) -> Self {
143 let model = if kind.supports_model(model) {
144 model
145 } else {
146 kind.default_model()
147 };
148
149 Self { kind, model }
150 }
151
152 #[must_use]
154 pub fn kind(self) -> AgentKind {
155 self.kind
156 }
157
158 #[must_use]
160 pub fn model(self) -> AgentModel {
161 self.model
162 }
163
164 #[must_use]
166 pub fn supports_fast_mode(self) -> bool {
167 matches!(
168 (self.kind, self.model),
169 (AgentKind::Claude, AgentModel::ClaudeOpus5)
170 | (
171 AgentKind::Codex,
172 AgentModel::Gpt56Sol | AgentModel::Gpt56Terra | AgentModel::Gpt56Luna
173 )
174 )
175 }
176
177 #[must_use]
183 pub fn compatible_with_speed_mode(self, speed_mode: SpeedMode) -> Self {
184 if speed_mode == SpeedMode::Normal || self.supports_fast_mode() {
185 return self;
186 }
187
188 match self.kind {
189 AgentKind::Claude => Self::new(AgentKind::Claude, AgentModel::ClaudeOpus5),
190 AgentKind::Codex if self.model == AgentModel::Gpt53CodexSpark => {
191 Self::new(AgentKind::Codex, AgentModel::Gpt56Sol)
192 }
193 AgentKind::Antigravity | AgentKind::Gemini | AgentKind::Codex => self,
194 }
195 }
196}
197
198pub trait AgentSelectionMetadata {
200 fn name(&self) -> &'static str;
202
203 fn description(&self) -> &'static str;
205}
206
207impl AgentModel {
208 pub fn as_str(self) -> &'static str {
211 match self {
212 Self::Gpt56Sol => "gpt-5.6-sol",
213 Self::Gpt56Terra => "gpt-5.6-terra",
214 Self::Gpt56Luna => "gpt-5.6-luna",
215 Self::Gemini37Flash => "gemini-3.7-flash",
216 Self::Gemini35FlashLite => "gemini-3.5-flash-lite",
217 Self::Gemini31Pro => "gemini-3.1-pro-preview",
218 Self::Gpt53CodexSpark => "gpt-5.3-codex-spark",
219 Self::ClaudeOpus5 => "claude-opus-5",
220 Self::ClaudeSonnet5 => "claude-sonnet-5",
221 Self::ClaudeFable5 => "claude-fable-5",
222 Self::ClaudeHaiku4520251001 => "claude-haiku-4-5-20251001",
223 }
224 }
225
226 pub fn provider_model_str(self) -> &'static str {
231 self.as_str()
232 }
233
234 pub fn parse_persisted(value: &str) -> Result<Self, String> {
247 if let Some(replacement) = Self::retired_replacement(value) {
248 return Ok(replacement);
249 }
250
251 value.parse()
252 }
253
254 #[must_use]
264 pub fn retired_replacement(value: &str) -> Option<Self> {
265 const RETIRED_MODEL_REPLACEMENTS: &[(&str, AgentModel)] = &[
266 ("gemini-3-pro-preview", AgentModel::Gemini31Pro),
267 ("gemini-3.1-pro", AgentModel::Gemini31Pro),
268 ("gemini-3-flash-preview", AgentModel::Gemini37Flash),
269 ("gemini-3.6-flash", AgentModel::Gemini37Flash),
270 ("gemini-3.5-flash", AgentModel::Gemini35FlashLite),
271 (
272 "gemini-3.1-flash-lite-preview",
273 AgentModel::Gemini35FlashLite,
274 ),
275 ("claude-opus-4-8", AgentModel::ClaudeOpus5),
276 ("claude-opus-4-6", AgentModel::ClaudeOpus5),
277 ("claude-opus-4-7", AgentModel::ClaudeOpus5),
278 ("claude-sonnet-4-6", AgentModel::ClaudeSonnet5),
279 ("gpt-5.5", AgentModel::Gpt56Sol),
280 ("gpt-5.4-mini", AgentModel::Gpt56Luna),
281 ("gpt-5.4", AgentModel::Gpt56Sol),
282 ("gpt-5.3-codex", AgentModel::Gpt56Sol),
283 ("gpt-5.2-codex", AgentModel::Gpt53CodexSpark),
284 ];
285
286 RETIRED_MODEL_REPLACEMENTS
287 .iter()
288 .find(|(retired_id, _)| *retired_id == value)
289 .map(|(_, replacement)| *replacement)
290 }
291}
292
293pub fn parse_persisted_session_agent_model(
302 agent_value: Option<&str>,
303 model_value: &str,
304) -> AgentSelection {
305 let parsed_agent = agent_value
306 .filter(|value| !value.trim().is_empty())
307 .and_then(|value| value.parse::<AgentKind>().ok());
308
309 if let Some(agent_kind) = parsed_agent {
310 return parse_model_for_persisted_agent(agent_kind, model_value);
311 }
312
313 let model = AgentModel::parse_persisted(model_value)
314 .unwrap_or_else(|_| AgentKind::Antigravity.default_model());
315 let agent_kind = legacy_agent_kind_for_model_value(model_value, model);
316
317 AgentSelection::new(agent_kind, model)
318}
319
320fn parse_model_for_persisted_agent(agent_kind: AgentKind, model_value: &str) -> AgentSelection {
323 if let Some(model) = agent_kind.parse_model(model_value) {
324 return AgentSelection::new(agent_kind, model);
325 }
326
327 if let Ok(model) = AgentModel::parse_persisted(model_value)
328 && agent_kind.supports_model(model)
329 {
330 return AgentSelection::new(agent_kind, model);
331 }
332
333 AgentSelection::new(agent_kind, agent_kind.default_model())
334}
335
336fn legacy_agent_kind_for_model_value(model_value: &str, model: AgentModel) -> AgentKind {
339 if model_value.starts_with("claude-") {
340 return AgentKind::Claude;
341 }
342
343 if model_value.starts_with("gpt-") {
344 return AgentKind::Codex;
345 }
346
347 if model_value.starts_with("gemini-") {
348 return AgentKind::Antigravity;
349 }
350
351 AgentKind::ALL
352 .iter()
353 .copied()
354 .find(|agent_kind| agent_kind.supports_model(model))
355 .unwrap_or(AgentKind::Antigravity)
356}
357
358#[must_use]
361pub fn selectable_models_for_agent_kinds(agent_kinds: &[AgentKind]) -> Vec<AgentModel> {
362 let mut models = Vec::new();
363 for model in agent_kinds
364 .iter()
365 .flat_map(|agent_kind| agent_kind.models())
366 .copied()
367 {
368 if !models.contains(&model) {
369 models.push(model);
370 }
371 }
372
373 models
374}
375
376#[must_use]
383pub fn resolve_model_for_available_agent_kinds(
384 model: AgentModel,
385 agent_kinds: &[AgentKind],
386 fallback_model: AgentModel,
387) -> AgentModel {
388 if agent_kinds
389 .iter()
390 .any(|agent_kind| agent_kind.supports_model(model))
391 {
392 return model;
393 }
394
395 if agent_kinds
396 .iter()
397 .any(|agent_kind| agent_kind.supports_model(fallback_model))
398 {
399 return fallback_model;
400 }
401
402 agent_kinds
403 .first()
404 .copied()
405 .map_or(fallback_model, AgentKind::default_model)
406}
407
408#[must_use]
415pub fn resolve_agent_kind_for_model(
416 model: AgentModel,
417 agent_kinds: &[AgentKind],
418 fallback_agent_kind: AgentKind,
419) -> AgentKind {
420 agent_kinds
421 .iter()
422 .copied()
423 .find(|agent_kind| agent_kind.supports_model(model))
424 .unwrap_or(fallback_agent_kind)
425}
426
427#[must_use]
434pub fn resolve_agent_selection_for_model(
435 model: AgentModel,
436 preferred_agent_kind: AgentKind,
437 agent_kinds: &[AgentKind],
438) -> AgentSelection {
439 let agent_kind = if preferred_agent_kind.supports_model(model) {
440 preferred_agent_kind
441 } else {
442 resolve_agent_kind_for_model(model, agent_kinds, preferred_agent_kind)
443 };
444
445 AgentSelection::new(agent_kind, model)
446}
447
448#[must_use]
454pub fn resolve_prompt_model_agent_kind(
455 session_agent_kind: AgentKind,
456 agent_kinds: &[AgentKind],
457) -> Option<AgentKind> {
458 if agent_kinds.contains(&session_agent_kind) {
459 return Some(session_agent_kind);
460 }
461
462 agent_kinds.first().copied()
463}
464
465impl ReasoningLevel {
466 pub const ALL: [Self; 5] = [Self::Low, Self::Medium, Self::High, Self::XHigh, Self::Max];
468
469 pub fn as_str(self) -> &'static str {
474 match self {
475 Self::Low => "low",
476 Self::Medium => "medium",
477 Self::High => "high",
478 Self::XHigh => "xhigh",
479 Self::Max => "max",
480 }
481 }
482
483 pub fn codex(self) -> &'static str {
485 match self {
486 Self::Low => "low",
487 Self::Medium => "medium",
488 Self::High => "high",
489 Self::XHigh => "xhigh",
490 Self::Max => "max",
491 }
492 }
493
494 pub fn antigravity(self) -> &'static str {
499 match self {
500 Self::Low => "low",
501 Self::Medium => "medium",
502 Self::High | Self::XHigh | Self::Max => "high",
503 }
504 }
505
506 pub fn claude(self) -> &'static str {
512 match self {
513 Self::Low => "low",
514 Self::Medium => "medium",
515 Self::High => "high",
516 Self::XHigh | Self::Max => "max",
517 }
518 }
519
520 pub fn description(self) -> &'static str {
522 match self {
523 Self::Low => "Fastest responses with lighter reasoning.",
524 Self::Medium => "Balanced speed and reasoning depth.",
525 Self::High => "Deeper reasoning for tougher tasks.",
526 Self::XHigh => "Extra-high reasoning for complex tasks.",
527 Self::Max => "Maximum reasoning effort for the hardest tasks.",
528 }
529 }
530}
531
532impl FromStr for ReasoningLevel {
533 type Err = String;
534
535 fn from_str(value: &str) -> Result<Self, Self::Err> {
536 match value {
537 "low" => Ok(Self::Low),
538 "medium" => Ok(Self::Medium),
539 "high" => Ok(Self::High),
540 "xhigh" => Ok(Self::XHigh),
541 "max" => Ok(Self::Max),
542 other => Err(format!("unknown reasoning level: {other}")),
543 }
544 }
545}
546
547impl FromStr for AgentModel {
548 type Err = String;
549
550 fn from_str(value: &str) -> Result<Self, Self::Err> {
551 match value {
552 "gemini-3.7-flash" => Ok(Self::Gemini37Flash),
553 "gemini-3.5-flash-lite" => Ok(Self::Gemini35FlashLite),
554 "gemini-3.1-pro-preview" => Ok(Self::Gemini31Pro),
555 "gpt-5.6-sol" => Ok(Self::Gpt56Sol),
556 "gpt-5.6-terra" => Ok(Self::Gpt56Terra),
557 "gpt-5.6-luna" => Ok(Self::Gpt56Luna),
558 "gpt-5.3-codex-spark" => Ok(Self::Gpt53CodexSpark),
559 "claude-opus-5" => Ok(Self::ClaudeOpus5),
560 "claude-sonnet-5" => Ok(Self::ClaudeSonnet5),
561 "claude-fable-5" => Ok(Self::ClaudeFable5),
562 "claude-haiku-4-5-20251001" => Ok(Self::ClaudeHaiku4520251001),
563 other => Err(format!("unknown model: {other}")),
564 }
565 }
566}
567
568impl AgentSelectionMetadata for AgentModel {
569 fn name(&self) -> &'static str {
570 (*self).provider_model_str()
571 }
572
573 fn description(&self) -> &'static str {
574 match self {
575 Self::Gemini31Pro => "Higher-quality Gemini model for deeper reasoning.",
576 Self::Gemini37Flash => "Fast Gemini model for agentic and multimodal tasks.",
577 Self::Gemini35FlashLite => {
578 "Lightweight Gemini model for fast, cost-conscious workloads."
579 }
580 Self::Gpt56Sol => "Newest Codex model for the strongest coding performance.",
581 Self::Gpt56Terra => "Current Codex model for balanced coding performance.",
582 Self::Gpt56Luna => "Current Codex model for lighter coding iterations.",
583 Self::Gpt53CodexSpark => "Codex spark model for quick coding iterations.",
584 Self::ClaudeOpus5 => "Latest Claude Opus model for complex tasks.",
585 Self::ClaudeSonnet5 => "Balanced Claude model for quality and latency.",
586 Self::ClaudeFable5 => "Claude Fable model for creative, narrative-heavy tasks.",
587 Self::ClaudeHaiku4520251001 => "Fast Claude model for lighter tasks.",
588 }
589 }
590}
591
592impl AgentKind {
593 pub const ALL: &[AgentKind] = &[
595 AgentKind::Gemini,
596 AgentKind::Antigravity,
597 AgentKind::Claude,
598 AgentKind::Codex,
599 ];
600
601 pub fn executable_name(self) -> &'static str {
603 match self {
604 Self::Antigravity => "agy",
605 Self::Gemini => "gemini",
606 Self::Claude => "claude",
607 Self::Codex => "codex",
608 }
609 }
610
611 pub fn default_model(self) -> AgentModel {
613 match self {
614 Self::Antigravity | Self::Gemini => AgentModel::Gemini31Pro,
615 Self::Claude => AgentModel::ClaudeFable5,
616 Self::Codex => AgentModel::Gpt56Sol,
617 }
618 }
619
620 pub fn model_str(self, model: AgentModel) -> Option<&'static str> {
622 if !self.supports_model(model) {
623 return None;
624 }
625
626 Some(model.as_str())
627 }
628
629 pub fn models(self) -> &'static [AgentModel] {
631 const ANTIGRAVITY_MODELS: &[AgentModel] = &[
632 AgentModel::Gemini31Pro,
633 AgentModel::Gemini37Flash,
634 AgentModel::Gemini35FlashLite,
635 ];
636 const GEMINI_MODELS: &[AgentModel] = &[
637 AgentModel::Gemini31Pro,
638 AgentModel::Gemini37Flash,
639 AgentModel::Gemini35FlashLite,
640 ];
641 const CLAUDE_MODELS: &[AgentModel] = &[
642 AgentModel::ClaudeFable5,
643 AgentModel::ClaudeOpus5,
644 AgentModel::ClaudeSonnet5,
645 AgentModel::ClaudeHaiku4520251001,
646 ];
647 const CODEX_MODELS: &[AgentModel] = &[
648 AgentModel::Gpt56Sol,
649 AgentModel::Gpt56Terra,
650 AgentModel::Gpt56Luna,
651 AgentModel::Gpt53CodexSpark,
652 ];
653
654 match self {
655 Self::Antigravity => ANTIGRAVITY_MODELS,
656 Self::Gemini => GEMINI_MODELS,
657 Self::Claude => CLAUDE_MODELS,
658 Self::Codex => CODEX_MODELS,
659 }
660 }
661
662 pub fn parse_model(self, value: &str) -> Option<AgentModel> {
664 let model = value.parse::<AgentModel>().ok()?;
665 if !self.supports_model(model) {
666 return None;
667 }
668
669 Some(model)
670 }
671
672 pub fn supports_model(self, model: AgentModel) -> bool {
674 self.models().contains(&model)
675 }
676
677 pub fn supports_speed_mode(self) -> bool {
686 matches!(self, Self::Claude | Self::Codex)
687 }
688}
689
690impl AgentSelectionMetadata for AgentKind {
691 fn name(&self) -> &'static str {
692 match self {
693 Self::Antigravity => "antigravity",
694 Self::Gemini => "gemini",
695 Self::Claude => "claude",
696 Self::Codex => "codex",
697 }
698 }
699
700 fn description(&self) -> &'static str {
701 match self {
702 Self::Antigravity => "Google Antigravity CLI agent.",
703 Self::Gemini => "Google Gemini CLI agent.",
704 Self::Claude => "Anthropic Claude Code agent.",
705 Self::Codex => "OpenAI Codex CLI agent.",
706 }
707 }
708}
709
710impl AgentSelectionMetadata for ReasoningLevel {
711 fn name(&self) -> &'static str {
712 (*self).as_str()
713 }
714
715 fn description(&self) -> &'static str {
716 (*self).description()
717 }
718}
719
720impl fmt::Display for AgentKind {
721 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
722 write!(f, "{}", self.name())
723 }
724}
725
726impl FromStr for AgentKind {
727 type Err = String;
728
729 fn from_str(s: &str) -> Result<Self, Self::Err> {
730 match s.to_lowercase().as_str() {
731 "antigravity" | "agy" => Ok(Self::Antigravity),
732 "gemini" => Ok(Self::Gemini),
733 "claude" => Ok(Self::Claude),
734 "codex" => Ok(Self::Codex),
735 other => Err(format!("unknown agent kind: {other}")),
736 }
737 }
738}
739
740#[cfg(test)]
741mod tests {
742 use super::*;
743
744 #[test]
745 fn test_parse_model_returns_none_for_models_from_other_providers() {
747 let claude_kind = AgentKind::Claude;
749 let antigravity_model = AgentModel::Gemini37Flash.as_str();
750
751 let parsed_model = claude_kind.parse_model(antigravity_model);
753
754 assert_eq!(parsed_model, None);
756 }
757
758 #[test]
759 fn test_parse_model_parses_current_codex_models() {
761 let codex_kind = AgentKind::Codex;
763
764 let parsed_sol = codex_kind.parse_model("gpt-5.6-sol");
766 let parsed_terra = codex_kind.parse_model("gpt-5.6-terra");
767 let parsed_luna = codex_kind.parse_model("gpt-5.6-luna");
768 let parsed_spark = codex_kind.parse_model("gpt-5.3-codex-spark");
769
770 assert_eq!(parsed_sol, Some(AgentModel::Gpt56Sol));
772 assert_eq!(parsed_terra, Some(AgentModel::Gpt56Terra));
773 assert_eq!(parsed_luna, Some(AgentModel::Gpt56Luna));
774 assert_eq!(parsed_spark, Some(AgentModel::Gpt53CodexSpark));
775 }
776
777 #[test]
778 fn test_parse_model_parses_antigravity() {
781 let antigravity_kind = AgentKind::Antigravity;
783
784 let parsed_pro = antigravity_kind.parse_model("gemini-3.1-pro-preview");
786 let parsed_flash_37 = antigravity_kind.parse_model("gemini-3.7-flash");
787 let parsed_flash_35_lite = antigravity_kind.parse_model("gemini-3.5-flash-lite");
788
789 assert_eq!(parsed_pro, Some(AgentModel::Gemini31Pro));
791 assert_eq!(parsed_flash_37, Some(AgentModel::Gemini37Flash));
792 assert_eq!(parsed_flash_35_lite, Some(AgentModel::Gemini35FlashLite));
793 }
794
795 #[test]
796 fn test_parse_model_parses_direct_gemini() {
799 let gemini_kind = AgentKind::Gemini;
801
802 let parsed_model = gemini_kind.parse_model("gemini-3.7-flash");
804
805 assert_eq!(parsed_model, Some(AgentModel::Gemini37Flash));
807 }
808
809 #[test]
810 fn test_parse_model_rejects_non_preview_gemini_31_pro() {
813 let agent_kinds = [AgentKind::Gemini, AgentKind::Antigravity];
815
816 let parsed_models = agent_kinds.map(|kind| kind.parse_model("gemini-3.1-pro"));
818
819 assert_eq!(parsed_models, [None; 2]);
821 }
822
823 #[test]
824 fn test_current_gemini_models_have_current_descriptions() {
826 let models = [
828 AgentModel::Gemini31Pro,
829 AgentModel::Gemini37Flash,
830 AgentModel::Gemini35FlashLite,
831 ];
832
833 let descriptions = models.map(|model| model.description());
835
836 assert_eq!(
838 descriptions,
839 [
840 "Higher-quality Gemini model for deeper reasoning.",
841 "Fast Gemini model for agentic and multimodal tasks.",
842 "Lightweight Gemini model for fast, cost-conscious workloads.",
843 ]
844 );
845 }
846
847 #[test]
848 fn test_parse_model_rejects_retired_gpt_models() {
850 let codex_kind = AgentKind::Codex;
852
853 let parsed_gpt_54 = codex_kind.parse_model("gpt-5.4");
855 let parsed_gpt_54_mini = codex_kind.parse_model("gpt-5.4-mini");
856
857 assert_eq!(parsed_gpt_54, None);
859 assert_eq!(parsed_gpt_54_mini, None);
860 }
861
862 #[test]
863 fn test_parse_model_rejects_retired_claude_aliases() {
866 let claude_kind = AgentKind::Claude;
868
869 let parsed_opus_46 = claude_kind.parse_model("claude-opus-4-6");
871 let parsed_opus_47 = claude_kind.parse_model("claude-opus-4-7");
872 let parsed_sonnet_46 = claude_kind.parse_model("claude-sonnet-4-6");
873
874 assert_eq!(parsed_opus_46, None);
876 assert_eq!(parsed_opus_47, None);
877 assert_eq!(parsed_sonnet_46, None);
878 }
879
880 #[test]
881 fn test_parse_model_parses_current_claude_models() {
883 let claude_kind = AgentKind::Claude;
885
886 let parsed_opus_5 = claude_kind.parse_model("claude-opus-5");
888 let parsed_sonnet_5 = claude_kind.parse_model("claude-sonnet-5");
889 let parsed_fable_5 = claude_kind.parse_model("claude-fable-5");
890 let parsed_haiku_45 = claude_kind.parse_model("claude-haiku-4-5-20251001");
891 let opus_5_id = AgentModel::ClaudeOpus5.as_str();
892 let opus_5_description = AgentModel::ClaudeOpus5.description();
893
894 assert_eq!(parsed_opus_5, Some(AgentModel::ClaudeOpus5));
896 assert_eq!(parsed_sonnet_5, Some(AgentModel::ClaudeSonnet5));
897 assert_eq!(parsed_fable_5, Some(AgentModel::ClaudeFable5));
898 assert_eq!(parsed_haiku_45, Some(AgentModel::ClaudeHaiku4520251001));
899 assert_eq!(opus_5_id, "claude-opus-5");
900 assert_eq!(
901 opus_5_description,
902 "Latest Claude Opus model for complex tasks."
903 );
904 }
905
906 #[test]
907 fn test_retired_replacement_maps_only_retired_ids() {
910 let retired_ids = [
912 ("gemini-3-pro-preview", AgentModel::Gemini31Pro),
913 ("gemini-3.1-pro", AgentModel::Gemini31Pro),
914 ("gemini-3-flash-preview", AgentModel::Gemini37Flash),
915 ("gemini-3.6-flash", AgentModel::Gemini37Flash),
916 ("gemini-3.5-flash", AgentModel::Gemini35FlashLite),
917 (
918 "gemini-3.1-flash-lite-preview",
919 AgentModel::Gemini35FlashLite,
920 ),
921 ("claude-opus-4-8", AgentModel::ClaudeOpus5),
922 ("claude-opus-4-6", AgentModel::ClaudeOpus5),
923 ("claude-opus-4-7", AgentModel::ClaudeOpus5),
924 ("claude-sonnet-4-6", AgentModel::ClaudeSonnet5),
925 ("gpt-5.5", AgentModel::Gpt56Sol),
926 ("gpt-5.4-mini", AgentModel::Gpt56Luna),
927 ("gpt-5.4", AgentModel::Gpt56Sol),
928 ("gpt-5.3-codex", AgentModel::Gpt56Sol),
929 ("gpt-5.2-codex", AgentModel::Gpt53CodexSpark),
930 ];
931
932 let replacements =
934 retired_ids.map(|(retired_id, _)| AgentModel::retired_replacement(retired_id));
935 let selectable_parses = retired_ids.map(|(retired_id, _)| retired_id.parse::<AgentModel>());
936 let current_replacements = [
937 "gemini-3.1-pro-preview",
938 "gemini-3.7-flash",
939 "gemini-3.5-flash-lite",
940 "claude-opus-5",
941 ]
942 .map(AgentModel::retired_replacement);
943 let unknown_replacement = AgentModel::retired_replacement("not-a-model");
944
945 assert_eq!(
947 replacements,
948 retired_ids.map(|(_, replacement)| Some(replacement))
949 );
950 assert!(selectable_parses.iter().all(Result::is_err));
951 assert_eq!(current_replacements, [None; 4]);
952 assert_eq!(unknown_replacement, None);
953 }
954
955 #[test]
956 fn test_parse_persisted_handles_supported_and_retired_models() {
959 let parsed_opus_46 = AgentModel::parse_persisted("claude-opus-4-6");
963 let parsed_opus_47 = AgentModel::parse_persisted("claude-opus-4-7");
964 let parsed_sonnet_46 = AgentModel::parse_persisted("claude-sonnet-4-6");
965 let parsed_sonnet_5 = AgentModel::parse_persisted("claude-sonnet-5");
966 let parsed_gpt_54_mini = AgentModel::parse_persisted("gpt-5.4-mini");
967 let parsed_gpt_54 = AgentModel::parse_persisted("gpt-5.4");
968 let parsed_gemini_37_flash = AgentModel::parse_persisted("gemini-3.7-flash");
969 let parsed_gemini_36_flash = AgentModel::parse_persisted("gemini-3.6-flash");
970 let parsed_gemini_35_flash = AgentModel::parse_persisted("gemini-3.5-flash");
971 let parsed_gemini_3_flash_preview = AgentModel::parse_persisted("gemini-3-flash-preview");
972 let parsed_gemini_35_flash_lite = AgentModel::parse_persisted("gemini-3.5-flash-lite");
973 let parsed_gemini_31_pro = AgentModel::parse_persisted("gemini-3.1-pro");
974 let parsed_gemini_31_pro_preview = AgentModel::parse_persisted("gemini-3.1-pro-preview");
975 let parsed_gemini_31_flash_lite_preview =
976 AgentModel::parse_persisted("gemini-3.1-flash-lite-preview");
977
978 assert_eq!(parsed_opus_46, Ok(AgentModel::ClaudeOpus5));
980 assert_eq!(parsed_opus_47, Ok(AgentModel::ClaudeOpus5));
981 assert_eq!(parsed_sonnet_46, Ok(AgentModel::ClaudeSonnet5));
982 assert_eq!(parsed_sonnet_5, Ok(AgentModel::ClaudeSonnet5));
983 assert_eq!(parsed_gpt_54_mini, Ok(AgentModel::Gpt56Luna));
984 assert_eq!(parsed_gpt_54, Ok(AgentModel::Gpt56Sol));
985 assert_eq!(parsed_gemini_37_flash, Ok(AgentModel::Gemini37Flash));
986 assert_eq!(parsed_gemini_36_flash, Ok(AgentModel::Gemini37Flash));
987 assert_eq!(parsed_gemini_35_flash, Ok(AgentModel::Gemini35FlashLite));
988 assert_eq!(parsed_gemini_3_flash_preview, Ok(AgentModel::Gemini37Flash));
989 assert_eq!(
990 parsed_gemini_35_flash_lite,
991 Ok(AgentModel::Gemini35FlashLite)
992 );
993 assert_eq!(parsed_gemini_31_pro, Ok(AgentModel::Gemini31Pro));
994 assert_eq!(parsed_gemini_31_pro_preview, Ok(AgentModel::Gemini31Pro));
995 assert_eq!(
996 parsed_gemini_31_flash_lite_preview,
997 Ok(AgentModel::Gemini35FlashLite)
998 );
999 }
1000
1001 #[test]
1002 fn test_parse_persisted_session_agent_model_prefers_saved_agent() {
1005 let selection = parse_persisted_session_agent_model(Some("codex"), "gemini-3.7-flash");
1009
1010 assert_eq!(selection.kind(), AgentKind::Codex);
1012 assert_eq!(selection.model(), AgentKind::Codex.default_model());
1013 }
1014
1015 #[test]
1016 fn test_parse_persisted_session_agent_model_preserves_antigravity_models() {
1019 let selection =
1023 parse_persisted_session_agent_model(Some("antigravity"), "gemini-3.7-flash");
1024
1025 assert_eq!(selection.kind(), AgentKind::Antigravity);
1027 assert_eq!(selection.model(), AgentModel::Gemini37Flash);
1028 }
1029
1030 #[test]
1031 fn test_parse_persisted_session_agent_model_resolves_saved_google_models() {
1034 let persisted_selections = [
1036 ("gemini", "gemini-3.5-flash-lite"),
1037 ("gemini", "gemini-3.5-flash"),
1038 ("antigravity", "gemini-3.5-flash-lite"),
1039 ("antigravity", "gemini-3.5-flash"),
1040 ];
1041
1042 let migrated_selections = persisted_selections.map(|(agent_value, model_value)| {
1044 parse_persisted_session_agent_model(Some(agent_value), model_value)
1045 });
1046
1047 assert_eq!(
1049 migrated_selections,
1050 [
1051 AgentSelection::new(AgentKind::Gemini, AgentModel::Gemini35FlashLite),
1052 AgentSelection::new(AgentKind::Gemini, AgentModel::Gemini35FlashLite),
1053 AgentSelection::new(AgentKind::Antigravity, AgentModel::Gemini35FlashLite),
1054 AgentSelection::new(AgentKind::Antigravity, AgentModel::Gemini35FlashLite),
1055 ]
1056 );
1057 }
1058
1059 #[test]
1060 fn test_parse_persisted_session_agent_model_supports_legacy_rows() {
1063 let selection = parse_persisted_session_agent_model(None, "claude-opus-4-6");
1067
1068 assert_eq!(selection.kind(), AgentKind::Claude);
1070 assert_eq!(selection.model(), AgentModel::ClaudeOpus5);
1071 }
1072
1073 #[test]
1074 fn test_parse_persisted_session_agent_model_defaults_legacy_gemini_to_antigravity() {
1077 let selection = parse_persisted_session_agent_model(None, "gemini-3.7-flash");
1081
1082 assert_eq!(selection.kind(), AgentKind::Antigravity);
1084 assert_eq!(selection.model(), AgentModel::Gemini37Flash);
1085 }
1086
1087 #[test]
1088 fn test_parse_persisted_session_agent_model_resolves_legacy_gemini_rows() {
1091 let migrated_flash = parse_persisted_session_agent_model(None, "gemini-3.5-flash");
1095 let loaded_flash_lite = parse_persisted_session_agent_model(None, "gemini-3.5-flash-lite");
1096
1097 assert_eq!(
1099 migrated_flash,
1100 AgentSelection::new(AgentKind::Antigravity, AgentModel::Gemini35FlashLite)
1101 );
1102 assert_eq!(
1103 loaded_flash_lite,
1104 AgentSelection::new(AgentKind::Antigravity, AgentModel::Gemini35FlashLite)
1105 );
1106 }
1107
1108 #[test]
1109 fn test_reasoning_level_from_str_parses_supported_values() {
1111 let low_level = "low".parse::<ReasoningLevel>();
1115 let medium_level = "medium".parse::<ReasoningLevel>();
1116 let high_level = "high".parse::<ReasoningLevel>();
1117 let xhigh_level = "xhigh".parse::<ReasoningLevel>();
1118 let max_level = "max".parse::<ReasoningLevel>();
1119
1120 assert_eq!(low_level, Ok(ReasoningLevel::Low));
1122 assert_eq!(medium_level, Ok(ReasoningLevel::Medium));
1123 assert_eq!(high_level, Ok(ReasoningLevel::High));
1124 assert_eq!(xhigh_level, Ok(ReasoningLevel::XHigh));
1125 assert_eq!(max_level, Ok(ReasoningLevel::Max));
1126 }
1127
1128 #[test]
1129 fn test_reasoning_level_from_str_rejects_unknown_values() {
1131 let parse_result = "minimal".parse::<ReasoningLevel>();
1135
1136 assert!(parse_result.is_err());
1138 }
1139
1140 #[test]
1141 fn test_codex_models_are_supported_by_codex() {
1143 let models = [
1145 AgentModel::Gpt56Sol,
1146 AgentModel::Gpt56Terra,
1147 AgentModel::Gpt56Luna,
1148 AgentModel::Gpt53CodexSpark,
1149 ];
1150
1151 let supported = models.map(|model| AgentKind::Codex.supports_model(model));
1153 let unsupported = models.map(|model| AgentKind::Claude.supports_model(model));
1154
1155 assert_eq!(supported, [true; 4]);
1157 assert_eq!(unsupported, [false; 4]);
1158 }
1159
1160 #[test]
1161 fn test_claude_models_are_supported_by_claude() {
1163 let models = [
1165 AgentModel::ClaudeOpus5,
1166 AgentModel::ClaudeSonnet5,
1167 AgentModel::ClaudeFable5,
1168 AgentModel::ClaudeHaiku4520251001,
1169 ];
1170
1171 let supported = models.map(|model| AgentKind::Claude.supports_model(model));
1173 let unsupported = models.map(|model| AgentKind::Codex.supports_model(model));
1174
1175 assert_eq!(supported, [true; 4]);
1177 assert_eq!(unsupported, [false; 4]);
1178 }
1179
1180 #[test]
1181 fn test_gemini_models_are_supported_by_gemini_and_antigravity() {
1183 let models = [
1185 AgentModel::Gemini31Pro,
1186 AgentModel::Gemini37Flash,
1187 AgentModel::Gemini35FlashLite,
1188 ];
1189
1190 let antigravity_supported =
1192 models.map(|model| AgentKind::Antigravity.supports_model(model));
1193 let gemini_supported = models.map(|model| AgentKind::Gemini.supports_model(model));
1194 let codex_supported = models.map(|model| AgentKind::Codex.supports_model(model));
1195
1196 assert_eq!(antigravity_supported, [true; 3]);
1198 assert_eq!(gemini_supported, [true; 3]);
1199 assert_eq!(codex_supported, [false; 3]);
1200 }
1201
1202 #[test]
1203 fn test_supports_speed_mode_covers_claude_and_codex_only() {
1205 let kinds = [
1207 AgentKind::Claude,
1208 AgentKind::Codex,
1209 AgentKind::Gemini,
1210 AgentKind::Antigravity,
1211 ];
1212
1213 let supported = kinds.map(AgentKind::supports_speed_mode);
1215
1216 assert_eq!(supported, [true, true, false, false]);
1218 }
1219
1220 #[test]
1221 fn test_agent_selection_speed_compatibility() {
1224 let cases = [
1226 (
1227 AgentSelection::new(AgentKind::Claude, AgentModel::ClaudeFable5),
1228 SpeedMode::Fast,
1229 AgentSelection::new(AgentKind::Claude, AgentModel::ClaudeOpus5),
1230 false,
1231 ),
1232 (
1233 AgentSelection::new(AgentKind::Codex, AgentModel::Gpt53CodexSpark),
1234 SpeedMode::Fast,
1235 AgentSelection::new(AgentKind::Codex, AgentModel::Gpt56Sol),
1236 false,
1237 ),
1238 (
1239 AgentSelection::new(AgentKind::Codex, AgentModel::Gpt56Terra),
1240 SpeedMode::Fast,
1241 AgentSelection::new(AgentKind::Codex, AgentModel::Gpt56Terra),
1242 true,
1243 ),
1244 (
1245 AgentSelection::new(AgentKind::Gemini, AgentModel::Gemini31Pro),
1246 SpeedMode::Fast,
1247 AgentSelection::new(AgentKind::Gemini, AgentModel::Gemini31Pro),
1248 false,
1249 ),
1250 (
1251 AgentSelection::new(AgentKind::Claude, AgentModel::ClaudeFable5),
1252 SpeedMode::Normal,
1253 AgentSelection::new(AgentKind::Claude, AgentModel::ClaudeFable5),
1254 false,
1255 ),
1256 ];
1257
1258 for (selection, speed_mode, expected_selection, supports_fast_mode) in cases {
1260 assert_eq!(
1261 selection.compatible_with_speed_mode(speed_mode),
1262 expected_selection
1263 );
1264 assert_eq!(selection.supports_fast_mode(), supports_fast_mode);
1265 }
1266 }
1267
1268 #[test]
1269 fn test_antigravity_provider_model_str_returns_raw_gemini_model() {
1272 let model = AgentModel::Gemini35FlashLite;
1274
1275 let persisted_model = model.as_str();
1277 let provider_model = model.provider_model_str();
1278
1279 assert_eq!(persisted_model, "gemini-3.5-flash-lite");
1281 assert_eq!(provider_model, "gemini-3.5-flash-lite");
1282 }
1283
1284 #[test]
1285 fn test_reasoning_level_claude_maps_all_levels() {
1288 assert_eq!(ReasoningLevel::Low.claude(), "low");
1290 assert_eq!(ReasoningLevel::Medium.claude(), "medium");
1291 assert_eq!(ReasoningLevel::High.claude(), "high");
1292 assert_eq!(ReasoningLevel::XHigh.claude(), "max");
1293 assert_eq!(ReasoningLevel::Max.claude(), "max");
1294 }
1295
1296 #[test]
1297 fn test_reasoning_level_antigravity_maps_all_levels() {
1300 assert_eq!(ReasoningLevel::Low.antigravity(), "low");
1302 assert_eq!(ReasoningLevel::Medium.antigravity(), "medium");
1303 assert_eq!(ReasoningLevel::High.antigravity(), "high");
1304 assert_eq!(ReasoningLevel::XHigh.antigravity(), "high");
1305 assert_eq!(ReasoningLevel::Max.antigravity(), "high");
1306 }
1307
1308 #[test]
1309 fn test_reasoning_level_codex_maps_all_levels() {
1311 assert_eq!(ReasoningLevel::Low.codex(), "low");
1313 assert_eq!(ReasoningLevel::Medium.codex(), "medium");
1314 assert_eq!(ReasoningLevel::High.codex(), "high");
1315 assert_eq!(ReasoningLevel::XHigh.codex(), "xhigh");
1316 assert_eq!(ReasoningLevel::Max.codex(), "max");
1317 }
1318
1319 #[test]
1320 fn test_reasoning_level_as_str_returns_stable_persisted_values() {
1323 assert_eq!(ReasoningLevel::Low.as_str(), "low");
1325 assert_eq!(ReasoningLevel::Medium.as_str(), "medium");
1326 assert_eq!(ReasoningLevel::High.as_str(), "high");
1327 assert_eq!(ReasoningLevel::XHigh.as_str(), "xhigh");
1328 assert_eq!(ReasoningLevel::Max.as_str(), "max");
1329 }
1330
1331 #[test]
1332 fn test_selectable_models_for_agent_kinds_uses_provider_order() {
1334 let agent_kinds = [AgentKind::Codex, AgentKind::Antigravity];
1336
1337 let selectable_models = selectable_models_for_agent_kinds(&agent_kinds);
1339
1340 assert_eq!(
1342 selectable_models,
1343 vec![
1344 AgentModel::Gpt56Sol,
1345 AgentModel::Gpt56Terra,
1346 AgentModel::Gpt56Luna,
1347 AgentModel::Gpt53CodexSpark,
1348 AgentModel::Gemini31Pro,
1349 AgentModel::Gemini37Flash,
1350 AgentModel::Gemini35FlashLite,
1351 ]
1352 );
1353 }
1354
1355 #[test]
1356 fn test_selectable_models_for_agent_kinds_deduplicates_shared_models() {
1359 let agent_kinds = [AgentKind::Gemini, AgentKind::Antigravity];
1361
1362 let selectable_models = selectable_models_for_agent_kinds(&agent_kinds);
1364
1365 assert_eq!(
1367 selectable_models,
1368 vec![
1369 AgentModel::Gemini31Pro,
1370 AgentModel::Gemini37Flash,
1371 AgentModel::Gemini35FlashLite,
1372 ]
1373 );
1374 }
1375
1376 #[test]
1377 fn test_resolve_model_for_available_agent_kinds_prefers_available_fallback() {
1380 let unavailable_model = AgentModel::ClaudeOpus5;
1382 let available_agent_kinds = [AgentKind::Codex, AgentKind::Antigravity];
1383 let fallback_model = AgentModel::Gpt56Terra;
1384
1385 let resolved_model = resolve_model_for_available_agent_kinds(
1387 unavailable_model,
1388 &available_agent_kinds,
1389 fallback_model,
1390 );
1391
1392 assert_eq!(resolved_model, AgentModel::Gpt56Terra);
1394 }
1395
1396 #[test]
1397 fn test_resolve_model_for_available_agent_kinds_uses_first_available_default() {
1400 let unavailable_model = AgentModel::ClaudeOpus5;
1402 let available_agent_kinds = [AgentKind::Codex, AgentKind::Antigravity];
1403 let unavailable_fallback_model = AgentModel::ClaudeSonnet5;
1404
1405 let resolved_model = resolve_model_for_available_agent_kinds(
1407 unavailable_model,
1408 &available_agent_kinds,
1409 unavailable_fallback_model,
1410 );
1411
1412 assert_eq!(resolved_model, AgentKind::Codex.default_model());
1414 }
1415
1416 #[test]
1417 fn test_resolve_agent_selection_for_model_preserves_preferred_shared_provider() {
1420 let model = AgentModel::Gemini37Flash;
1422 let available_agent_kinds = [AgentKind::Gemini, AgentKind::Antigravity];
1423
1424 let resolved_selection = resolve_agent_selection_for_model(
1426 model,
1427 AgentKind::Antigravity,
1428 &available_agent_kinds,
1429 );
1430
1431 assert_eq!(
1433 resolved_selection,
1434 AgentSelection::new(AgentKind::Antigravity, model)
1435 );
1436 }
1437
1438 #[test]
1439 fn test_resolve_agent_selection_for_model_uses_available_provider_order() {
1442 let model = AgentModel::Gemini37Flash;
1444 let available_agent_kinds = [AgentKind::Gemini, AgentKind::Antigravity];
1445
1446 let resolved_selection =
1448 resolve_agent_selection_for_model(model, AgentKind::Codex, &available_agent_kinds);
1449
1450 assert_eq!(
1452 resolved_selection,
1453 AgentSelection::new(AgentKind::Gemini, model)
1454 );
1455 }
1456
1457 #[test]
1458 fn test_resolve_prompt_model_agent_kind_prefers_current_agent() {
1461 let available_agent_kinds = [AgentKind::Antigravity, AgentKind::Codex];
1463
1464 let resolved_agent_kind =
1466 resolve_prompt_model_agent_kind(AgentKind::Codex, &available_agent_kinds);
1467
1468 assert_eq!(resolved_agent_kind, Some(AgentKind::Codex));
1470 }
1471
1472 #[test]
1473 fn test_resolve_prompt_model_agent_kind_uses_first_available_agent() {
1476 let available_agent_kinds = [AgentKind::Antigravity, AgentKind::Codex];
1478
1479 let resolved_agent_kind =
1481 resolve_prompt_model_agent_kind(AgentKind::Claude, &available_agent_kinds);
1482
1483 assert_eq!(resolved_agent_kind, Some(AgentKind::Antigravity));
1485 }
1486}