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 Gemini36Flash,
95 Gemini35FlashLite,
97 Gemini31Pro,
99 Gpt53CodexSpark,
101 ClaudeOpus5,
103 ClaudeSonnet5,
105 ClaudeFable5,
107 ClaudeHaiku4520251001,
109}
110
111#[derive(Debug, Clone, Copy, PartialEq, Eq)]
113pub struct AgentSelection {
114 kind: AgentKind,
115 model: AgentModel,
116}
117
118#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
120pub enum ReasoningLevel {
121 Low,
123 Medium,
125 #[default]
127 High,
128 XHigh,
130 Max,
132}
133
134impl AgentSelection {
135 #[must_use]
140 pub fn new(kind: AgentKind, model: AgentModel) -> Self {
141 let model = if kind.supports_model(model) {
142 model
143 } else {
144 kind.default_model()
145 };
146
147 Self { kind, model }
148 }
149
150 #[must_use]
152 pub fn kind(self) -> AgentKind {
153 self.kind
154 }
155
156 #[must_use]
158 pub fn model(self) -> AgentModel {
159 self.model
160 }
161}
162
163pub trait AgentSelectionMetadata {
165 fn name(&self) -> &'static str;
167
168 fn description(&self) -> &'static str;
170}
171
172impl AgentModel {
173 pub fn as_str(self) -> &'static str {
176 match self {
177 Self::Gpt56Sol => "gpt-5.6-sol",
178 Self::Gpt56Terra => "gpt-5.6-terra",
179 Self::Gpt56Luna => "gpt-5.6-luna",
180 Self::Gemini36Flash => "gemini-3.6-flash",
181 Self::Gemini35FlashLite => "gemini-3.5-flash-lite",
182 Self::Gemini31Pro => "gemini-3.1-pro",
183 Self::Gpt53CodexSpark => "gpt-5.3-codex-spark",
184 Self::ClaudeOpus5 => "claude-opus-5",
185 Self::ClaudeSonnet5 => "claude-sonnet-5",
186 Self::ClaudeFable5 => "claude-fable-5",
187 Self::ClaudeHaiku4520251001 => "claude-haiku-4-5-20251001",
188 }
189 }
190
191 pub fn provider_model_str(self) -> &'static str {
196 self.as_str()
197 }
198
199 pub fn parse_persisted(value: &str) -> Result<Self, String> {
212 if let Some(replacement) = Self::retired_replacement(value) {
213 return Ok(replacement);
214 }
215
216 value.parse()
217 }
218
219 #[must_use]
229 pub fn retired_replacement(value: &str) -> Option<Self> {
230 const RETIRED_MODEL_REPLACEMENTS: &[(&str, AgentModel)] = &[
231 ("gemini-3-pro-preview", AgentModel::Gemini31Pro),
232 ("gemini-3-flash-preview", AgentModel::Gemini36Flash),
233 ("gemini-3.5-flash", AgentModel::Gemini35FlashLite),
234 ("gemini-3.1-pro-preview", AgentModel::Gemini31Pro),
235 (
236 "gemini-3.1-flash-lite-preview",
237 AgentModel::Gemini35FlashLite,
238 ),
239 ("claude-opus-4-8", AgentModel::ClaudeOpus5),
240 ("claude-opus-4-6", AgentModel::ClaudeOpus5),
241 ("claude-opus-4-7", AgentModel::ClaudeOpus5),
242 ("claude-sonnet-4-6", AgentModel::ClaudeSonnet5),
243 ("gpt-5.5", AgentModel::Gpt56Sol),
244 ("gpt-5.4-mini", AgentModel::Gpt56Luna),
245 ("gpt-5.4", AgentModel::Gpt56Sol),
246 ("gpt-5.3-codex", AgentModel::Gpt56Sol),
247 ("gpt-5.2-codex", AgentModel::Gpt53CodexSpark),
248 ];
249
250 RETIRED_MODEL_REPLACEMENTS
251 .iter()
252 .find(|(retired_id, _)| *retired_id == value)
253 .map(|(_, replacement)| *replacement)
254 }
255}
256
257pub fn parse_persisted_session_agent_model(
266 agent_value: Option<&str>,
267 model_value: &str,
268) -> AgentSelection {
269 let parsed_agent = agent_value
270 .filter(|value| !value.trim().is_empty())
271 .and_then(|value| value.parse::<AgentKind>().ok());
272
273 if let Some(agent_kind) = parsed_agent {
274 return parse_model_for_persisted_agent(agent_kind, model_value);
275 }
276
277 let model = AgentModel::parse_persisted(model_value)
278 .unwrap_or_else(|_| AgentKind::Antigravity.default_model());
279 let agent_kind = legacy_agent_kind_for_model_value(model_value, model);
280
281 AgentSelection::new(agent_kind, model)
282}
283
284fn parse_model_for_persisted_agent(agent_kind: AgentKind, model_value: &str) -> AgentSelection {
287 if let Some(model) = agent_kind.parse_model(model_value) {
288 return AgentSelection::new(agent_kind, model);
289 }
290
291 if let Ok(model) = AgentModel::parse_persisted(model_value)
292 && agent_kind.supports_model(model)
293 {
294 return AgentSelection::new(agent_kind, model);
295 }
296
297 AgentSelection::new(agent_kind, agent_kind.default_model())
298}
299
300fn legacy_agent_kind_for_model_value(model_value: &str, model: AgentModel) -> AgentKind {
303 if model_value.starts_with("claude-") {
304 return AgentKind::Claude;
305 }
306
307 if model_value.starts_with("gpt-") {
308 return AgentKind::Codex;
309 }
310
311 if model_value.starts_with("gemini-") {
312 return AgentKind::Antigravity;
313 }
314
315 AgentKind::ALL
316 .iter()
317 .copied()
318 .find(|agent_kind| agent_kind.supports_model(model))
319 .unwrap_or(AgentKind::Antigravity)
320}
321
322#[must_use]
325pub fn selectable_models_for_agent_kinds(agent_kinds: &[AgentKind]) -> Vec<AgentModel> {
326 let mut models = Vec::new();
327 for model in agent_kinds
328 .iter()
329 .flat_map(|agent_kind| agent_kind.models())
330 .copied()
331 {
332 if !models.contains(&model) {
333 models.push(model);
334 }
335 }
336
337 models
338}
339
340#[must_use]
347pub fn resolve_model_for_available_agent_kinds(
348 model: AgentModel,
349 agent_kinds: &[AgentKind],
350 fallback_model: AgentModel,
351) -> AgentModel {
352 if agent_kinds
353 .iter()
354 .any(|agent_kind| agent_kind.supports_model(model))
355 {
356 return model;
357 }
358
359 if agent_kinds
360 .iter()
361 .any(|agent_kind| agent_kind.supports_model(fallback_model))
362 {
363 return fallback_model;
364 }
365
366 agent_kinds
367 .first()
368 .copied()
369 .map_or(fallback_model, AgentKind::default_model)
370}
371
372#[must_use]
379pub fn resolve_agent_kind_for_model(
380 model: AgentModel,
381 agent_kinds: &[AgentKind],
382 fallback_agent_kind: AgentKind,
383) -> AgentKind {
384 agent_kinds
385 .iter()
386 .copied()
387 .find(|agent_kind| agent_kind.supports_model(model))
388 .unwrap_or(fallback_agent_kind)
389}
390
391#[must_use]
398pub fn resolve_agent_selection_for_model(
399 model: AgentModel,
400 preferred_agent_kind: AgentKind,
401 agent_kinds: &[AgentKind],
402) -> AgentSelection {
403 let agent_kind = if preferred_agent_kind.supports_model(model) {
404 preferred_agent_kind
405 } else {
406 resolve_agent_kind_for_model(model, agent_kinds, preferred_agent_kind)
407 };
408
409 AgentSelection::new(agent_kind, model)
410}
411
412#[must_use]
418pub fn resolve_prompt_model_agent_kind(
419 session_agent_kind: AgentKind,
420 agent_kinds: &[AgentKind],
421) -> Option<AgentKind> {
422 if agent_kinds.contains(&session_agent_kind) {
423 return Some(session_agent_kind);
424 }
425
426 agent_kinds.first().copied()
427}
428
429impl ReasoningLevel {
430 pub const ALL: [Self; 5] = [Self::Low, Self::Medium, Self::High, Self::XHigh, Self::Max];
432
433 pub fn as_str(self) -> &'static str {
438 match self {
439 Self::Low => "low",
440 Self::Medium => "medium",
441 Self::High => "high",
442 Self::XHigh => "xhigh",
443 Self::Max => "max",
444 }
445 }
446
447 pub fn codex(self) -> &'static str {
449 match self {
450 Self::Low => "low",
451 Self::Medium => "medium",
452 Self::High => "high",
453 Self::XHigh => "xhigh",
454 Self::Max => "max",
455 }
456 }
457
458 pub fn antigravity(self) -> &'static str {
463 match self {
464 Self::Low => "low",
465 Self::Medium => "medium",
466 Self::High | Self::XHigh | Self::Max => "high",
467 }
468 }
469
470 pub fn claude(self) -> &'static str {
476 match self {
477 Self::Low => "low",
478 Self::Medium => "medium",
479 Self::High => "high",
480 Self::XHigh | Self::Max => "max",
481 }
482 }
483
484 pub fn description(self) -> &'static str {
486 match self {
487 Self::Low => "Fastest responses with lighter reasoning.",
488 Self::Medium => "Balanced speed and reasoning depth.",
489 Self::High => "Deeper reasoning for tougher tasks.",
490 Self::XHigh => "Extra-high reasoning for complex tasks.",
491 Self::Max => "Maximum reasoning effort for the hardest tasks.",
492 }
493 }
494}
495
496impl FromStr for ReasoningLevel {
497 type Err = String;
498
499 fn from_str(value: &str) -> Result<Self, Self::Err> {
500 match value {
501 "low" => Ok(Self::Low),
502 "medium" => Ok(Self::Medium),
503 "high" => Ok(Self::High),
504 "xhigh" => Ok(Self::XHigh),
505 "max" => Ok(Self::Max),
506 other => Err(format!("unknown reasoning level: {other}")),
507 }
508 }
509}
510
511impl FromStr for AgentModel {
512 type Err = String;
513
514 fn from_str(value: &str) -> Result<Self, Self::Err> {
515 match value {
516 "gemini-3.6-flash" => Ok(Self::Gemini36Flash),
517 "gemini-3.5-flash-lite" => Ok(Self::Gemini35FlashLite),
518 "gemini-3.1-pro" => Ok(Self::Gemini31Pro),
519 "gpt-5.6-sol" => Ok(Self::Gpt56Sol),
520 "gpt-5.6-terra" => Ok(Self::Gpt56Terra),
521 "gpt-5.6-luna" => Ok(Self::Gpt56Luna),
522 "gpt-5.3-codex-spark" => Ok(Self::Gpt53CodexSpark),
523 "claude-opus-5" => Ok(Self::ClaudeOpus5),
524 "claude-sonnet-5" => Ok(Self::ClaudeSonnet5),
525 "claude-fable-5" => Ok(Self::ClaudeFable5),
526 "claude-haiku-4-5-20251001" => Ok(Self::ClaudeHaiku4520251001),
527 other => Err(format!("unknown model: {other}")),
528 }
529 }
530}
531
532impl AgentSelectionMetadata for AgentModel {
533 fn name(&self) -> &'static str {
534 (*self).provider_model_str()
535 }
536
537 fn description(&self) -> &'static str {
538 match self {
539 Self::Gemini31Pro => "Higher-quality Gemini model for deeper reasoning.",
540 Self::Gemini36Flash => "Fast Gemini model for agentic and multimodal tasks.",
541 Self::Gemini35FlashLite => {
542 "Lightweight Gemini model for fast, cost-conscious workloads."
543 }
544 Self::Gpt56Sol => "Newest Codex model for the strongest coding performance.",
545 Self::Gpt56Terra => "Current Codex model for balanced coding performance.",
546 Self::Gpt56Luna => "Current Codex model for lighter coding iterations.",
547 Self::Gpt53CodexSpark => "Codex spark model for quick coding iterations.",
548 Self::ClaudeOpus5 => "Latest Claude Opus model for complex tasks.",
549 Self::ClaudeSonnet5 => "Balanced Claude model for quality and latency.",
550 Self::ClaudeFable5 => "Claude Fable model for creative, narrative-heavy tasks.",
551 Self::ClaudeHaiku4520251001 => "Fast Claude model for lighter tasks.",
552 }
553 }
554}
555
556impl AgentKind {
557 pub const ALL: &[AgentKind] = &[
559 AgentKind::Gemini,
560 AgentKind::Antigravity,
561 AgentKind::Claude,
562 AgentKind::Codex,
563 ];
564
565 pub fn executable_name(self) -> &'static str {
567 match self {
568 Self::Antigravity => "agy",
569 Self::Gemini => "gemini",
570 Self::Claude => "claude",
571 Self::Codex => "codex",
572 }
573 }
574
575 pub fn default_model(self) -> AgentModel {
577 match self {
578 Self::Antigravity | Self::Gemini => AgentModel::Gemini31Pro,
579 Self::Claude => AgentModel::ClaudeFable5,
580 Self::Codex => AgentModel::Gpt56Sol,
581 }
582 }
583
584 pub fn model_str(self, model: AgentModel) -> Option<&'static str> {
586 if !self.supports_model(model) {
587 return None;
588 }
589
590 Some(model.as_str())
591 }
592
593 pub fn models(self) -> &'static [AgentModel] {
595 const ANTIGRAVITY_MODELS: &[AgentModel] = &[
596 AgentModel::Gemini31Pro,
597 AgentModel::Gemini36Flash,
598 AgentModel::Gemini35FlashLite,
599 ];
600 const GEMINI_MODELS: &[AgentModel] = &[
601 AgentModel::Gemini31Pro,
602 AgentModel::Gemini36Flash,
603 AgentModel::Gemini35FlashLite,
604 ];
605 const CLAUDE_MODELS: &[AgentModel] = &[
606 AgentModel::ClaudeFable5,
607 AgentModel::ClaudeOpus5,
608 AgentModel::ClaudeSonnet5,
609 AgentModel::ClaudeHaiku4520251001,
610 ];
611 const CODEX_MODELS: &[AgentModel] = &[
612 AgentModel::Gpt56Sol,
613 AgentModel::Gpt56Terra,
614 AgentModel::Gpt56Luna,
615 AgentModel::Gpt53CodexSpark,
616 ];
617
618 match self {
619 Self::Antigravity => ANTIGRAVITY_MODELS,
620 Self::Gemini => GEMINI_MODELS,
621 Self::Claude => CLAUDE_MODELS,
622 Self::Codex => CODEX_MODELS,
623 }
624 }
625
626 pub fn parse_model(self, value: &str) -> Option<AgentModel> {
628 let model = value.parse::<AgentModel>().ok()?;
629 if !self.supports_model(model) {
630 return None;
631 }
632
633 Some(model)
634 }
635
636 pub fn supports_model(self, model: AgentModel) -> bool {
638 self.models().contains(&model)
639 }
640
641 pub fn supports_speed_mode(self) -> bool {
650 matches!(self, Self::Claude | Self::Codex)
651 }
652}
653
654impl AgentSelectionMetadata for AgentKind {
655 fn name(&self) -> &'static str {
656 match self {
657 Self::Antigravity => "antigravity",
658 Self::Gemini => "gemini",
659 Self::Claude => "claude",
660 Self::Codex => "codex",
661 }
662 }
663
664 fn description(&self) -> &'static str {
665 match self {
666 Self::Antigravity => "Google Antigravity CLI agent.",
667 Self::Gemini => "Google Gemini CLI agent.",
668 Self::Claude => "Anthropic Claude Code agent.",
669 Self::Codex => "OpenAI Codex CLI agent.",
670 }
671 }
672}
673
674impl AgentSelectionMetadata for ReasoningLevel {
675 fn name(&self) -> &'static str {
676 (*self).as_str()
677 }
678
679 fn description(&self) -> &'static str {
680 (*self).description()
681 }
682}
683
684impl fmt::Display for AgentKind {
685 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
686 write!(f, "{}", self.name())
687 }
688}
689
690impl FromStr for AgentKind {
691 type Err = String;
692
693 fn from_str(s: &str) -> Result<Self, Self::Err> {
694 match s.to_lowercase().as_str() {
695 "antigravity" | "agy" => Ok(Self::Antigravity),
696 "gemini" => Ok(Self::Gemini),
697 "claude" => Ok(Self::Claude),
698 "codex" => Ok(Self::Codex),
699 other => Err(format!("unknown agent kind: {other}")),
700 }
701 }
702}
703
704#[cfg(test)]
705mod tests {
706 use super::*;
707
708 #[test]
709 fn test_parse_model_returns_none_for_models_from_other_providers() {
711 let claude_kind = AgentKind::Claude;
713 let antigravity_model = AgentModel::Gemini36Flash.as_str();
714
715 let parsed_model = claude_kind.parse_model(antigravity_model);
717
718 assert_eq!(parsed_model, None);
720 }
721
722 #[test]
723 fn test_parse_model_parses_current_codex_models() {
725 let codex_kind = AgentKind::Codex;
727
728 let parsed_sol = codex_kind.parse_model("gpt-5.6-sol");
730 let parsed_terra = codex_kind.parse_model("gpt-5.6-terra");
731 let parsed_luna = codex_kind.parse_model("gpt-5.6-luna");
732 let parsed_spark = codex_kind.parse_model("gpt-5.3-codex-spark");
733
734 assert_eq!(parsed_sol, Some(AgentModel::Gpt56Sol));
736 assert_eq!(parsed_terra, Some(AgentModel::Gpt56Terra));
737 assert_eq!(parsed_luna, Some(AgentModel::Gpt56Luna));
738 assert_eq!(parsed_spark, Some(AgentModel::Gpt53CodexSpark));
739 }
740
741 #[test]
742 fn test_parse_model_parses_antigravity() {
745 let antigravity_kind = AgentKind::Antigravity;
747
748 let parsed_pro = antigravity_kind.parse_model("gemini-3.1-pro");
750 let parsed_flash_36 = antigravity_kind.parse_model("gemini-3.6-flash");
751 let parsed_flash_35_lite = antigravity_kind.parse_model("gemini-3.5-flash-lite");
752
753 assert_eq!(parsed_pro, Some(AgentModel::Gemini31Pro));
755 assert_eq!(parsed_flash_36, Some(AgentModel::Gemini36Flash));
756 assert_eq!(parsed_flash_35_lite, Some(AgentModel::Gemini35FlashLite));
757 }
758
759 #[test]
760 fn test_parse_model_parses_direct_gemini() {
763 let gemini_kind = AgentKind::Gemini;
765
766 let parsed_model = gemini_kind.parse_model("gemini-3.6-flash");
768
769 assert_eq!(parsed_model, Some(AgentModel::Gemini36Flash));
771 }
772
773 #[test]
774 fn test_current_gemini_models_have_current_descriptions() {
776 let models = [
778 AgentModel::Gemini31Pro,
779 AgentModel::Gemini36Flash,
780 AgentModel::Gemini35FlashLite,
781 ];
782
783 let descriptions = models.map(|model| model.description());
785
786 assert_eq!(
788 descriptions,
789 [
790 "Higher-quality Gemini model for deeper reasoning.",
791 "Fast Gemini model for agentic and multimodal tasks.",
792 "Lightweight Gemini model for fast, cost-conscious workloads.",
793 ]
794 );
795 }
796
797 #[test]
798 fn test_parse_model_rejects_retired_gpt_models() {
800 let codex_kind = AgentKind::Codex;
802
803 let parsed_gpt_54 = codex_kind.parse_model("gpt-5.4");
805 let parsed_gpt_54_mini = codex_kind.parse_model("gpt-5.4-mini");
806
807 assert_eq!(parsed_gpt_54, None);
809 assert_eq!(parsed_gpt_54_mini, None);
810 }
811
812 #[test]
813 fn test_parse_model_rejects_retired_claude_aliases() {
816 let claude_kind = AgentKind::Claude;
818
819 let parsed_opus_46 = claude_kind.parse_model("claude-opus-4-6");
821 let parsed_opus_47 = claude_kind.parse_model("claude-opus-4-7");
822 let parsed_sonnet_46 = claude_kind.parse_model("claude-sonnet-4-6");
823
824 assert_eq!(parsed_opus_46, None);
826 assert_eq!(parsed_opus_47, None);
827 assert_eq!(parsed_sonnet_46, None);
828 }
829
830 #[test]
831 fn test_parse_model_parses_current_claude_models() {
833 let claude_kind = AgentKind::Claude;
835
836 let parsed_opus_5 = claude_kind.parse_model("claude-opus-5");
838 let parsed_sonnet_5 = claude_kind.parse_model("claude-sonnet-5");
839 let parsed_fable_5 = claude_kind.parse_model("claude-fable-5");
840 let parsed_haiku_45 = claude_kind.parse_model("claude-haiku-4-5-20251001");
841 let opus_5_id = AgentModel::ClaudeOpus5.as_str();
842 let opus_5_description = AgentModel::ClaudeOpus5.description();
843
844 assert_eq!(parsed_opus_5, Some(AgentModel::ClaudeOpus5));
846 assert_eq!(parsed_sonnet_5, Some(AgentModel::ClaudeSonnet5));
847 assert_eq!(parsed_fable_5, Some(AgentModel::ClaudeFable5));
848 assert_eq!(parsed_haiku_45, Some(AgentModel::ClaudeHaiku4520251001));
849 assert_eq!(opus_5_id, "claude-opus-5");
850 assert_eq!(
851 opus_5_description,
852 "Latest Claude Opus model for complex tasks."
853 );
854 }
855
856 #[test]
857 fn test_retired_replacement_maps_only_retired_ids() {
860 let retired_ids = [
862 ("gemini-3-pro-preview", AgentModel::Gemini31Pro),
863 ("gemini-3-flash-preview", AgentModel::Gemini36Flash),
864 ("gemini-3.5-flash", AgentModel::Gemini35FlashLite),
865 ("gemini-3.1-pro-preview", AgentModel::Gemini31Pro),
866 (
867 "gemini-3.1-flash-lite-preview",
868 AgentModel::Gemini35FlashLite,
869 ),
870 ("claude-opus-4-8", AgentModel::ClaudeOpus5),
871 ("claude-opus-4-6", AgentModel::ClaudeOpus5),
872 ("claude-opus-4-7", AgentModel::ClaudeOpus5),
873 ("claude-sonnet-4-6", AgentModel::ClaudeSonnet5),
874 ("gpt-5.5", AgentModel::Gpt56Sol),
875 ("gpt-5.4-mini", AgentModel::Gpt56Luna),
876 ("gpt-5.4", AgentModel::Gpt56Sol),
877 ("gpt-5.3-codex", AgentModel::Gpt56Sol),
878 ("gpt-5.2-codex", AgentModel::Gpt53CodexSpark),
879 ];
880
881 let replacements =
883 retired_ids.map(|(retired_id, _)| AgentModel::retired_replacement(retired_id));
884 let selectable_parses = retired_ids.map(|(retired_id, _)| retired_id.parse::<AgentModel>());
885 let current_replacements = [
886 "gemini-3.1-pro",
887 "gemini-3.6-flash",
888 "gemini-3.5-flash-lite",
889 "claude-opus-5",
890 ]
891 .map(AgentModel::retired_replacement);
892 let unknown_replacement = AgentModel::retired_replacement("not-a-model");
893
894 assert_eq!(
896 replacements,
897 retired_ids.map(|(_, replacement)| Some(replacement))
898 );
899 assert!(selectable_parses.iter().all(Result::is_err));
900 assert_eq!(current_replacements, [None; 4]);
901 assert_eq!(unknown_replacement, None);
902 }
903
904 #[test]
905 fn test_parse_persisted_handles_supported_and_retired_models() {
908 let parsed_opus_46 = AgentModel::parse_persisted("claude-opus-4-6");
912 let parsed_opus_47 = AgentModel::parse_persisted("claude-opus-4-7");
913 let parsed_sonnet_46 = AgentModel::parse_persisted("claude-sonnet-4-6");
914 let parsed_sonnet_5 = AgentModel::parse_persisted("claude-sonnet-5");
915 let parsed_gpt_54_mini = AgentModel::parse_persisted("gpt-5.4-mini");
916 let parsed_gpt_54 = AgentModel::parse_persisted("gpt-5.4");
917 let parsed_gemini_36_flash = AgentModel::parse_persisted("gemini-3.6-flash");
918 let parsed_gemini_35_flash = AgentModel::parse_persisted("gemini-3.5-flash");
919 let parsed_gemini_3_flash_preview = AgentModel::parse_persisted("gemini-3-flash-preview");
920 let parsed_gemini_35_flash_lite = AgentModel::parse_persisted("gemini-3.5-flash-lite");
921 let parsed_gemini_31_pro_preview = AgentModel::parse_persisted("gemini-3.1-pro-preview");
922 let parsed_gemini_31_flash_lite_preview =
923 AgentModel::parse_persisted("gemini-3.1-flash-lite-preview");
924
925 assert_eq!(parsed_opus_46, Ok(AgentModel::ClaudeOpus5));
927 assert_eq!(parsed_opus_47, Ok(AgentModel::ClaudeOpus5));
928 assert_eq!(parsed_sonnet_46, Ok(AgentModel::ClaudeSonnet5));
929 assert_eq!(parsed_sonnet_5, Ok(AgentModel::ClaudeSonnet5));
930 assert_eq!(parsed_gpt_54_mini, Ok(AgentModel::Gpt56Luna));
931 assert_eq!(parsed_gpt_54, Ok(AgentModel::Gpt56Sol));
932 assert_eq!(parsed_gemini_36_flash, Ok(AgentModel::Gemini36Flash));
933 assert_eq!(parsed_gemini_35_flash, Ok(AgentModel::Gemini35FlashLite));
934 assert_eq!(parsed_gemini_3_flash_preview, Ok(AgentModel::Gemini36Flash));
935 assert_eq!(
936 parsed_gemini_35_flash_lite,
937 Ok(AgentModel::Gemini35FlashLite)
938 );
939 assert_eq!(parsed_gemini_31_pro_preview, Ok(AgentModel::Gemini31Pro));
940 assert_eq!(
941 parsed_gemini_31_flash_lite_preview,
942 Ok(AgentModel::Gemini35FlashLite)
943 );
944 }
945
946 #[test]
947 fn test_parse_persisted_session_agent_model_prefers_saved_agent() {
950 let selection = parse_persisted_session_agent_model(Some("codex"), "gemini-3.6-flash");
954
955 assert_eq!(selection.kind(), AgentKind::Codex);
957 assert_eq!(selection.model(), AgentKind::Codex.default_model());
958 }
959
960 #[test]
961 fn test_parse_persisted_session_agent_model_preserves_antigravity_models() {
964 let selection =
968 parse_persisted_session_agent_model(Some("antigravity"), "gemini-3.6-flash");
969
970 assert_eq!(selection.kind(), AgentKind::Antigravity);
972 assert_eq!(selection.model(), AgentModel::Gemini36Flash);
973 }
974
975 #[test]
976 fn test_parse_persisted_session_agent_model_resolves_saved_google_models() {
979 let persisted_selections = [
981 ("gemini", "gemini-3.5-flash-lite"),
982 ("gemini", "gemini-3.5-flash"),
983 ("antigravity", "gemini-3.5-flash-lite"),
984 ("antigravity", "gemini-3.5-flash"),
985 ];
986
987 let migrated_selections = persisted_selections.map(|(agent_value, model_value)| {
989 parse_persisted_session_agent_model(Some(agent_value), model_value)
990 });
991
992 assert_eq!(
994 migrated_selections,
995 [
996 AgentSelection::new(AgentKind::Gemini, AgentModel::Gemini35FlashLite),
997 AgentSelection::new(AgentKind::Gemini, AgentModel::Gemini35FlashLite),
998 AgentSelection::new(AgentKind::Antigravity, AgentModel::Gemini35FlashLite),
999 AgentSelection::new(AgentKind::Antigravity, AgentModel::Gemini35FlashLite),
1000 ]
1001 );
1002 }
1003
1004 #[test]
1005 fn test_parse_persisted_session_agent_model_supports_legacy_rows() {
1008 let selection = parse_persisted_session_agent_model(None, "claude-opus-4-6");
1012
1013 assert_eq!(selection.kind(), AgentKind::Claude);
1015 assert_eq!(selection.model(), AgentModel::ClaudeOpus5);
1016 }
1017
1018 #[test]
1019 fn test_parse_persisted_session_agent_model_defaults_legacy_gemini_to_antigravity() {
1022 let selection = parse_persisted_session_agent_model(None, "gemini-3.6-flash");
1026
1027 assert_eq!(selection.kind(), AgentKind::Antigravity);
1029 assert_eq!(selection.model(), AgentModel::Gemini36Flash);
1030 }
1031
1032 #[test]
1033 fn test_parse_persisted_session_agent_model_resolves_legacy_gemini_rows() {
1036 let migrated_flash = parse_persisted_session_agent_model(None, "gemini-3.5-flash");
1040 let loaded_flash_lite = parse_persisted_session_agent_model(None, "gemini-3.5-flash-lite");
1041
1042 assert_eq!(
1044 migrated_flash,
1045 AgentSelection::new(AgentKind::Antigravity, AgentModel::Gemini35FlashLite)
1046 );
1047 assert_eq!(
1048 loaded_flash_lite,
1049 AgentSelection::new(AgentKind::Antigravity, AgentModel::Gemini35FlashLite)
1050 );
1051 }
1052
1053 #[test]
1054 fn test_reasoning_level_from_str_parses_supported_values() {
1056 let low_level = "low".parse::<ReasoningLevel>();
1060 let medium_level = "medium".parse::<ReasoningLevel>();
1061 let high_level = "high".parse::<ReasoningLevel>();
1062 let xhigh_level = "xhigh".parse::<ReasoningLevel>();
1063 let max_level = "max".parse::<ReasoningLevel>();
1064
1065 assert_eq!(low_level, Ok(ReasoningLevel::Low));
1067 assert_eq!(medium_level, Ok(ReasoningLevel::Medium));
1068 assert_eq!(high_level, Ok(ReasoningLevel::High));
1069 assert_eq!(xhigh_level, Ok(ReasoningLevel::XHigh));
1070 assert_eq!(max_level, Ok(ReasoningLevel::Max));
1071 }
1072
1073 #[test]
1074 fn test_reasoning_level_from_str_rejects_unknown_values() {
1076 let parse_result = "minimal".parse::<ReasoningLevel>();
1080
1081 assert!(parse_result.is_err());
1083 }
1084
1085 #[test]
1086 fn test_codex_models_are_supported_by_codex() {
1088 let models = [
1090 AgentModel::Gpt56Sol,
1091 AgentModel::Gpt56Terra,
1092 AgentModel::Gpt56Luna,
1093 AgentModel::Gpt53CodexSpark,
1094 ];
1095
1096 let supported = models.map(|model| AgentKind::Codex.supports_model(model));
1098 let unsupported = models.map(|model| AgentKind::Claude.supports_model(model));
1099
1100 assert_eq!(supported, [true; 4]);
1102 assert_eq!(unsupported, [false; 4]);
1103 }
1104
1105 #[test]
1106 fn test_claude_models_are_supported_by_claude() {
1108 let models = [
1110 AgentModel::ClaudeOpus5,
1111 AgentModel::ClaudeSonnet5,
1112 AgentModel::ClaudeFable5,
1113 AgentModel::ClaudeHaiku4520251001,
1114 ];
1115
1116 let supported = models.map(|model| AgentKind::Claude.supports_model(model));
1118 let unsupported = models.map(|model| AgentKind::Codex.supports_model(model));
1119
1120 assert_eq!(supported, [true; 4]);
1122 assert_eq!(unsupported, [false; 4]);
1123 }
1124
1125 #[test]
1126 fn test_gemini_models_are_supported_by_gemini_and_antigravity() {
1128 let models = [
1130 AgentModel::Gemini31Pro,
1131 AgentModel::Gemini36Flash,
1132 AgentModel::Gemini35FlashLite,
1133 ];
1134
1135 let antigravity_supported =
1137 models.map(|model| AgentKind::Antigravity.supports_model(model));
1138 let gemini_supported = models.map(|model| AgentKind::Gemini.supports_model(model));
1139 let codex_supported = models.map(|model| AgentKind::Codex.supports_model(model));
1140
1141 assert_eq!(antigravity_supported, [true; 3]);
1143 assert_eq!(gemini_supported, [true; 3]);
1144 assert_eq!(codex_supported, [false; 3]);
1145 }
1146
1147 #[test]
1148 fn test_supports_speed_mode_covers_claude_and_codex_only() {
1150 let kinds = [
1152 AgentKind::Claude,
1153 AgentKind::Codex,
1154 AgentKind::Gemini,
1155 AgentKind::Antigravity,
1156 ];
1157
1158 let supported = kinds.map(AgentKind::supports_speed_mode);
1160
1161 assert_eq!(supported, [true, true, false, false]);
1163 }
1164
1165 #[test]
1166 fn test_antigravity_provider_model_str_returns_raw_gemini_model() {
1169 let model = AgentModel::Gemini35FlashLite;
1171
1172 let persisted_model = model.as_str();
1174 let provider_model = model.provider_model_str();
1175
1176 assert_eq!(persisted_model, "gemini-3.5-flash-lite");
1178 assert_eq!(provider_model, "gemini-3.5-flash-lite");
1179 }
1180
1181 #[test]
1182 fn test_reasoning_level_claude_maps_all_levels() {
1185 assert_eq!(ReasoningLevel::Low.claude(), "low");
1187 assert_eq!(ReasoningLevel::Medium.claude(), "medium");
1188 assert_eq!(ReasoningLevel::High.claude(), "high");
1189 assert_eq!(ReasoningLevel::XHigh.claude(), "max");
1190 assert_eq!(ReasoningLevel::Max.claude(), "max");
1191 }
1192
1193 #[test]
1194 fn test_reasoning_level_antigravity_maps_all_levels() {
1197 assert_eq!(ReasoningLevel::Low.antigravity(), "low");
1199 assert_eq!(ReasoningLevel::Medium.antigravity(), "medium");
1200 assert_eq!(ReasoningLevel::High.antigravity(), "high");
1201 assert_eq!(ReasoningLevel::XHigh.antigravity(), "high");
1202 assert_eq!(ReasoningLevel::Max.antigravity(), "high");
1203 }
1204
1205 #[test]
1206 fn test_reasoning_level_codex_maps_all_levels() {
1208 assert_eq!(ReasoningLevel::Low.codex(), "low");
1210 assert_eq!(ReasoningLevel::Medium.codex(), "medium");
1211 assert_eq!(ReasoningLevel::High.codex(), "high");
1212 assert_eq!(ReasoningLevel::XHigh.codex(), "xhigh");
1213 assert_eq!(ReasoningLevel::Max.codex(), "max");
1214 }
1215
1216 #[test]
1217 fn test_reasoning_level_as_str_returns_stable_persisted_values() {
1220 assert_eq!(ReasoningLevel::Low.as_str(), "low");
1222 assert_eq!(ReasoningLevel::Medium.as_str(), "medium");
1223 assert_eq!(ReasoningLevel::High.as_str(), "high");
1224 assert_eq!(ReasoningLevel::XHigh.as_str(), "xhigh");
1225 assert_eq!(ReasoningLevel::Max.as_str(), "max");
1226 }
1227
1228 #[test]
1229 fn test_selectable_models_for_agent_kinds_uses_provider_order() {
1231 let agent_kinds = [AgentKind::Codex, AgentKind::Antigravity];
1233
1234 let selectable_models = selectable_models_for_agent_kinds(&agent_kinds);
1236
1237 assert_eq!(
1239 selectable_models,
1240 vec![
1241 AgentModel::Gpt56Sol,
1242 AgentModel::Gpt56Terra,
1243 AgentModel::Gpt56Luna,
1244 AgentModel::Gpt53CodexSpark,
1245 AgentModel::Gemini31Pro,
1246 AgentModel::Gemini36Flash,
1247 AgentModel::Gemini35FlashLite,
1248 ]
1249 );
1250 }
1251
1252 #[test]
1253 fn test_selectable_models_for_agent_kinds_deduplicates_shared_models() {
1256 let agent_kinds = [AgentKind::Gemini, AgentKind::Antigravity];
1258
1259 let selectable_models = selectable_models_for_agent_kinds(&agent_kinds);
1261
1262 assert_eq!(
1264 selectable_models,
1265 vec![
1266 AgentModel::Gemini31Pro,
1267 AgentModel::Gemini36Flash,
1268 AgentModel::Gemini35FlashLite,
1269 ]
1270 );
1271 }
1272
1273 #[test]
1274 fn test_resolve_model_for_available_agent_kinds_prefers_available_fallback() {
1277 let unavailable_model = AgentModel::ClaudeOpus5;
1279 let available_agent_kinds = [AgentKind::Codex, AgentKind::Antigravity];
1280 let fallback_model = AgentModel::Gpt56Terra;
1281
1282 let resolved_model = resolve_model_for_available_agent_kinds(
1284 unavailable_model,
1285 &available_agent_kinds,
1286 fallback_model,
1287 );
1288
1289 assert_eq!(resolved_model, AgentModel::Gpt56Terra);
1291 }
1292
1293 #[test]
1294 fn test_resolve_model_for_available_agent_kinds_uses_first_available_default() {
1297 let unavailable_model = AgentModel::ClaudeOpus5;
1299 let available_agent_kinds = [AgentKind::Codex, AgentKind::Antigravity];
1300 let unavailable_fallback_model = AgentModel::ClaudeSonnet5;
1301
1302 let resolved_model = resolve_model_for_available_agent_kinds(
1304 unavailable_model,
1305 &available_agent_kinds,
1306 unavailable_fallback_model,
1307 );
1308
1309 assert_eq!(resolved_model, AgentKind::Codex.default_model());
1311 }
1312
1313 #[test]
1314 fn test_resolve_agent_selection_for_model_preserves_preferred_shared_provider() {
1317 let model = AgentModel::Gemini36Flash;
1319 let available_agent_kinds = [AgentKind::Gemini, AgentKind::Antigravity];
1320
1321 let resolved_selection = resolve_agent_selection_for_model(
1323 model,
1324 AgentKind::Antigravity,
1325 &available_agent_kinds,
1326 );
1327
1328 assert_eq!(
1330 resolved_selection,
1331 AgentSelection::new(AgentKind::Antigravity, model)
1332 );
1333 }
1334
1335 #[test]
1336 fn test_resolve_agent_selection_for_model_uses_available_provider_order() {
1339 let model = AgentModel::Gemini36Flash;
1341 let available_agent_kinds = [AgentKind::Gemini, AgentKind::Antigravity];
1342
1343 let resolved_selection =
1345 resolve_agent_selection_for_model(model, AgentKind::Codex, &available_agent_kinds);
1346
1347 assert_eq!(
1349 resolved_selection,
1350 AgentSelection::new(AgentKind::Gemini, model)
1351 );
1352 }
1353
1354 #[test]
1355 fn test_resolve_prompt_model_agent_kind_prefers_current_agent() {
1358 let available_agent_kinds = [AgentKind::Antigravity, AgentKind::Codex];
1360
1361 let resolved_agent_kind =
1363 resolve_prompt_model_agent_kind(AgentKind::Codex, &available_agent_kinds);
1364
1365 assert_eq!(resolved_agent_kind, Some(AgentKind::Codex));
1367 }
1368
1369 #[test]
1370 fn test_resolve_prompt_model_agent_kind_uses_first_available_agent() {
1373 let available_agent_kinds = [AgentKind::Antigravity, AgentKind::Codex];
1375
1376 let resolved_agent_kind =
1378 resolve_prompt_model_agent_kind(AgentKind::Claude, &available_agent_kinds);
1379
1380 assert_eq!(resolved_agent_kind, Some(AgentKind::Antigravity));
1382 }
1383}