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 Gemini37Flash,
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::Gemini37Flash => "gemini-3.7-flash",
181 Self::Gemini35FlashLite => "gemini-3.5-flash-lite",
182 Self::Gemini31Pro => "gemini-3.1-pro-preview",
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.1-pro", AgentModel::Gemini31Pro),
233 ("gemini-3-flash-preview", AgentModel::Gemini37Flash),
234 ("gemini-3.6-flash", AgentModel::Gemini37Flash),
235 ("gemini-3.5-flash", AgentModel::Gemini35FlashLite),
236 (
237 "gemini-3.1-flash-lite-preview",
238 AgentModel::Gemini35FlashLite,
239 ),
240 ("claude-opus-4-8", AgentModel::ClaudeOpus5),
241 ("claude-opus-4-6", AgentModel::ClaudeOpus5),
242 ("claude-opus-4-7", AgentModel::ClaudeOpus5),
243 ("claude-sonnet-4-6", AgentModel::ClaudeSonnet5),
244 ("gpt-5.5", AgentModel::Gpt56Sol),
245 ("gpt-5.4-mini", AgentModel::Gpt56Luna),
246 ("gpt-5.4", AgentModel::Gpt56Sol),
247 ("gpt-5.3-codex", AgentModel::Gpt56Sol),
248 ("gpt-5.2-codex", AgentModel::Gpt53CodexSpark),
249 ];
250
251 RETIRED_MODEL_REPLACEMENTS
252 .iter()
253 .find(|(retired_id, _)| *retired_id == value)
254 .map(|(_, replacement)| *replacement)
255 }
256}
257
258pub fn parse_persisted_session_agent_model(
267 agent_value: Option<&str>,
268 model_value: &str,
269) -> AgentSelection {
270 let parsed_agent = agent_value
271 .filter(|value| !value.trim().is_empty())
272 .and_then(|value| value.parse::<AgentKind>().ok());
273
274 if let Some(agent_kind) = parsed_agent {
275 return parse_model_for_persisted_agent(agent_kind, model_value);
276 }
277
278 let model = AgentModel::parse_persisted(model_value)
279 .unwrap_or_else(|_| AgentKind::Antigravity.default_model());
280 let agent_kind = legacy_agent_kind_for_model_value(model_value, model);
281
282 AgentSelection::new(agent_kind, model)
283}
284
285fn parse_model_for_persisted_agent(agent_kind: AgentKind, model_value: &str) -> AgentSelection {
288 if let Some(model) = agent_kind.parse_model(model_value) {
289 return AgentSelection::new(agent_kind, model);
290 }
291
292 if let Ok(model) = AgentModel::parse_persisted(model_value)
293 && agent_kind.supports_model(model)
294 {
295 return AgentSelection::new(agent_kind, model);
296 }
297
298 AgentSelection::new(agent_kind, agent_kind.default_model())
299}
300
301fn legacy_agent_kind_for_model_value(model_value: &str, model: AgentModel) -> AgentKind {
304 if model_value.starts_with("claude-") {
305 return AgentKind::Claude;
306 }
307
308 if model_value.starts_with("gpt-") {
309 return AgentKind::Codex;
310 }
311
312 if model_value.starts_with("gemini-") {
313 return AgentKind::Antigravity;
314 }
315
316 AgentKind::ALL
317 .iter()
318 .copied()
319 .find(|agent_kind| agent_kind.supports_model(model))
320 .unwrap_or(AgentKind::Antigravity)
321}
322
323#[must_use]
326pub fn selectable_models_for_agent_kinds(agent_kinds: &[AgentKind]) -> Vec<AgentModel> {
327 let mut models = Vec::new();
328 for model in agent_kinds
329 .iter()
330 .flat_map(|agent_kind| agent_kind.models())
331 .copied()
332 {
333 if !models.contains(&model) {
334 models.push(model);
335 }
336 }
337
338 models
339}
340
341#[must_use]
348pub fn resolve_model_for_available_agent_kinds(
349 model: AgentModel,
350 agent_kinds: &[AgentKind],
351 fallback_model: AgentModel,
352) -> AgentModel {
353 if agent_kinds
354 .iter()
355 .any(|agent_kind| agent_kind.supports_model(model))
356 {
357 return model;
358 }
359
360 if agent_kinds
361 .iter()
362 .any(|agent_kind| agent_kind.supports_model(fallback_model))
363 {
364 return fallback_model;
365 }
366
367 agent_kinds
368 .first()
369 .copied()
370 .map_or(fallback_model, AgentKind::default_model)
371}
372
373#[must_use]
380pub fn resolve_agent_kind_for_model(
381 model: AgentModel,
382 agent_kinds: &[AgentKind],
383 fallback_agent_kind: AgentKind,
384) -> AgentKind {
385 agent_kinds
386 .iter()
387 .copied()
388 .find(|agent_kind| agent_kind.supports_model(model))
389 .unwrap_or(fallback_agent_kind)
390}
391
392#[must_use]
399pub fn resolve_agent_selection_for_model(
400 model: AgentModel,
401 preferred_agent_kind: AgentKind,
402 agent_kinds: &[AgentKind],
403) -> AgentSelection {
404 let agent_kind = if preferred_agent_kind.supports_model(model) {
405 preferred_agent_kind
406 } else {
407 resolve_agent_kind_for_model(model, agent_kinds, preferred_agent_kind)
408 };
409
410 AgentSelection::new(agent_kind, model)
411}
412
413#[must_use]
419pub fn resolve_prompt_model_agent_kind(
420 session_agent_kind: AgentKind,
421 agent_kinds: &[AgentKind],
422) -> Option<AgentKind> {
423 if agent_kinds.contains(&session_agent_kind) {
424 return Some(session_agent_kind);
425 }
426
427 agent_kinds.first().copied()
428}
429
430impl ReasoningLevel {
431 pub const ALL: [Self; 5] = [Self::Low, Self::Medium, Self::High, Self::XHigh, Self::Max];
433
434 pub fn as_str(self) -> &'static str {
439 match self {
440 Self::Low => "low",
441 Self::Medium => "medium",
442 Self::High => "high",
443 Self::XHigh => "xhigh",
444 Self::Max => "max",
445 }
446 }
447
448 pub fn codex(self) -> &'static str {
450 match self {
451 Self::Low => "low",
452 Self::Medium => "medium",
453 Self::High => "high",
454 Self::XHigh => "xhigh",
455 Self::Max => "max",
456 }
457 }
458
459 pub fn antigravity(self) -> &'static str {
464 match self {
465 Self::Low => "low",
466 Self::Medium => "medium",
467 Self::High | Self::XHigh | Self::Max => "high",
468 }
469 }
470
471 pub fn claude(self) -> &'static str {
477 match self {
478 Self::Low => "low",
479 Self::Medium => "medium",
480 Self::High => "high",
481 Self::XHigh | Self::Max => "max",
482 }
483 }
484
485 pub fn description(self) -> &'static str {
487 match self {
488 Self::Low => "Fastest responses with lighter reasoning.",
489 Self::Medium => "Balanced speed and reasoning depth.",
490 Self::High => "Deeper reasoning for tougher tasks.",
491 Self::XHigh => "Extra-high reasoning for complex tasks.",
492 Self::Max => "Maximum reasoning effort for the hardest tasks.",
493 }
494 }
495}
496
497impl FromStr for ReasoningLevel {
498 type Err = String;
499
500 fn from_str(value: &str) -> Result<Self, Self::Err> {
501 match value {
502 "low" => Ok(Self::Low),
503 "medium" => Ok(Self::Medium),
504 "high" => Ok(Self::High),
505 "xhigh" => Ok(Self::XHigh),
506 "max" => Ok(Self::Max),
507 other => Err(format!("unknown reasoning level: {other}")),
508 }
509 }
510}
511
512impl FromStr for AgentModel {
513 type Err = String;
514
515 fn from_str(value: &str) -> Result<Self, Self::Err> {
516 match value {
517 "gemini-3.7-flash" => Ok(Self::Gemini37Flash),
518 "gemini-3.5-flash-lite" => Ok(Self::Gemini35FlashLite),
519 "gemini-3.1-pro-preview" => Ok(Self::Gemini31Pro),
520 "gpt-5.6-sol" => Ok(Self::Gpt56Sol),
521 "gpt-5.6-terra" => Ok(Self::Gpt56Terra),
522 "gpt-5.6-luna" => Ok(Self::Gpt56Luna),
523 "gpt-5.3-codex-spark" => Ok(Self::Gpt53CodexSpark),
524 "claude-opus-5" => Ok(Self::ClaudeOpus5),
525 "claude-sonnet-5" => Ok(Self::ClaudeSonnet5),
526 "claude-fable-5" => Ok(Self::ClaudeFable5),
527 "claude-haiku-4-5-20251001" => Ok(Self::ClaudeHaiku4520251001),
528 other => Err(format!("unknown model: {other}")),
529 }
530 }
531}
532
533impl AgentSelectionMetadata for AgentModel {
534 fn name(&self) -> &'static str {
535 (*self).provider_model_str()
536 }
537
538 fn description(&self) -> &'static str {
539 match self {
540 Self::Gemini31Pro => "Higher-quality Gemini model for deeper reasoning.",
541 Self::Gemini37Flash => "Fast Gemini model for agentic and multimodal tasks.",
542 Self::Gemini35FlashLite => {
543 "Lightweight Gemini model for fast, cost-conscious workloads."
544 }
545 Self::Gpt56Sol => "Newest Codex model for the strongest coding performance.",
546 Self::Gpt56Terra => "Current Codex model for balanced coding performance.",
547 Self::Gpt56Luna => "Current Codex model for lighter coding iterations.",
548 Self::Gpt53CodexSpark => "Codex spark model for quick coding iterations.",
549 Self::ClaudeOpus5 => "Latest Claude Opus model for complex tasks.",
550 Self::ClaudeSonnet5 => "Balanced Claude model for quality and latency.",
551 Self::ClaudeFable5 => "Claude Fable model for creative, narrative-heavy tasks.",
552 Self::ClaudeHaiku4520251001 => "Fast Claude model for lighter tasks.",
553 }
554 }
555}
556
557impl AgentKind {
558 pub const ALL: &[AgentKind] = &[
560 AgentKind::Gemini,
561 AgentKind::Antigravity,
562 AgentKind::Claude,
563 AgentKind::Codex,
564 ];
565
566 pub fn executable_name(self) -> &'static str {
568 match self {
569 Self::Antigravity => "agy",
570 Self::Gemini => "gemini",
571 Self::Claude => "claude",
572 Self::Codex => "codex",
573 }
574 }
575
576 pub fn default_model(self) -> AgentModel {
578 match self {
579 Self::Antigravity | Self::Gemini => AgentModel::Gemini31Pro,
580 Self::Claude => AgentModel::ClaudeFable5,
581 Self::Codex => AgentModel::Gpt56Sol,
582 }
583 }
584
585 pub fn model_str(self, model: AgentModel) -> Option<&'static str> {
587 if !self.supports_model(model) {
588 return None;
589 }
590
591 Some(model.as_str())
592 }
593
594 pub fn models(self) -> &'static [AgentModel] {
596 const ANTIGRAVITY_MODELS: &[AgentModel] = &[
597 AgentModel::Gemini31Pro,
598 AgentModel::Gemini37Flash,
599 AgentModel::Gemini35FlashLite,
600 ];
601 const GEMINI_MODELS: &[AgentModel] = &[
602 AgentModel::Gemini31Pro,
603 AgentModel::Gemini37Flash,
604 AgentModel::Gemini35FlashLite,
605 ];
606 const CLAUDE_MODELS: &[AgentModel] = &[
607 AgentModel::ClaudeFable5,
608 AgentModel::ClaudeOpus5,
609 AgentModel::ClaudeSonnet5,
610 AgentModel::ClaudeHaiku4520251001,
611 ];
612 const CODEX_MODELS: &[AgentModel] = &[
613 AgentModel::Gpt56Sol,
614 AgentModel::Gpt56Terra,
615 AgentModel::Gpt56Luna,
616 AgentModel::Gpt53CodexSpark,
617 ];
618
619 match self {
620 Self::Antigravity => ANTIGRAVITY_MODELS,
621 Self::Gemini => GEMINI_MODELS,
622 Self::Claude => CLAUDE_MODELS,
623 Self::Codex => CODEX_MODELS,
624 }
625 }
626
627 pub fn parse_model(self, value: &str) -> Option<AgentModel> {
629 let model = value.parse::<AgentModel>().ok()?;
630 if !self.supports_model(model) {
631 return None;
632 }
633
634 Some(model)
635 }
636
637 pub fn supports_model(self, model: AgentModel) -> bool {
639 self.models().contains(&model)
640 }
641
642 pub fn supports_speed_mode(self) -> bool {
651 matches!(self, Self::Claude | Self::Codex)
652 }
653}
654
655impl AgentSelectionMetadata for AgentKind {
656 fn name(&self) -> &'static str {
657 match self {
658 Self::Antigravity => "antigravity",
659 Self::Gemini => "gemini",
660 Self::Claude => "claude",
661 Self::Codex => "codex",
662 }
663 }
664
665 fn description(&self) -> &'static str {
666 match self {
667 Self::Antigravity => "Google Antigravity CLI agent.",
668 Self::Gemini => "Google Gemini CLI agent.",
669 Self::Claude => "Anthropic Claude Code agent.",
670 Self::Codex => "OpenAI Codex CLI agent.",
671 }
672 }
673}
674
675impl AgentSelectionMetadata for ReasoningLevel {
676 fn name(&self) -> &'static str {
677 (*self).as_str()
678 }
679
680 fn description(&self) -> &'static str {
681 (*self).description()
682 }
683}
684
685impl fmt::Display for AgentKind {
686 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
687 write!(f, "{}", self.name())
688 }
689}
690
691impl FromStr for AgentKind {
692 type Err = String;
693
694 fn from_str(s: &str) -> Result<Self, Self::Err> {
695 match s.to_lowercase().as_str() {
696 "antigravity" | "agy" => Ok(Self::Antigravity),
697 "gemini" => Ok(Self::Gemini),
698 "claude" => Ok(Self::Claude),
699 "codex" => Ok(Self::Codex),
700 other => Err(format!("unknown agent kind: {other}")),
701 }
702 }
703}
704
705#[cfg(test)]
706mod tests {
707 use super::*;
708
709 #[test]
710 fn test_parse_model_returns_none_for_models_from_other_providers() {
712 let claude_kind = AgentKind::Claude;
714 let antigravity_model = AgentModel::Gemini37Flash.as_str();
715
716 let parsed_model = claude_kind.parse_model(antigravity_model);
718
719 assert_eq!(parsed_model, None);
721 }
722
723 #[test]
724 fn test_parse_model_parses_current_codex_models() {
726 let codex_kind = AgentKind::Codex;
728
729 let parsed_sol = codex_kind.parse_model("gpt-5.6-sol");
731 let parsed_terra = codex_kind.parse_model("gpt-5.6-terra");
732 let parsed_luna = codex_kind.parse_model("gpt-5.6-luna");
733 let parsed_spark = codex_kind.parse_model("gpt-5.3-codex-spark");
734
735 assert_eq!(parsed_sol, Some(AgentModel::Gpt56Sol));
737 assert_eq!(parsed_terra, Some(AgentModel::Gpt56Terra));
738 assert_eq!(parsed_luna, Some(AgentModel::Gpt56Luna));
739 assert_eq!(parsed_spark, Some(AgentModel::Gpt53CodexSpark));
740 }
741
742 #[test]
743 fn test_parse_model_parses_antigravity() {
746 let antigravity_kind = AgentKind::Antigravity;
748
749 let parsed_pro = antigravity_kind.parse_model("gemini-3.1-pro-preview");
751 let parsed_flash_37 = antigravity_kind.parse_model("gemini-3.7-flash");
752 let parsed_flash_35_lite = antigravity_kind.parse_model("gemini-3.5-flash-lite");
753
754 assert_eq!(parsed_pro, Some(AgentModel::Gemini31Pro));
756 assert_eq!(parsed_flash_37, Some(AgentModel::Gemini37Flash));
757 assert_eq!(parsed_flash_35_lite, Some(AgentModel::Gemini35FlashLite));
758 }
759
760 #[test]
761 fn test_parse_model_parses_direct_gemini() {
764 let gemini_kind = AgentKind::Gemini;
766
767 let parsed_model = gemini_kind.parse_model("gemini-3.7-flash");
769
770 assert_eq!(parsed_model, Some(AgentModel::Gemini37Flash));
772 }
773
774 #[test]
775 fn test_parse_model_rejects_non_preview_gemini_31_pro() {
778 let agent_kinds = [AgentKind::Gemini, AgentKind::Antigravity];
780
781 let parsed_models = agent_kinds.map(|kind| kind.parse_model("gemini-3.1-pro"));
783
784 assert_eq!(parsed_models, [None; 2]);
786 }
787
788 #[test]
789 fn test_current_gemini_models_have_current_descriptions() {
791 let models = [
793 AgentModel::Gemini31Pro,
794 AgentModel::Gemini37Flash,
795 AgentModel::Gemini35FlashLite,
796 ];
797
798 let descriptions = models.map(|model| model.description());
800
801 assert_eq!(
803 descriptions,
804 [
805 "Higher-quality Gemini model for deeper reasoning.",
806 "Fast Gemini model for agentic and multimodal tasks.",
807 "Lightweight Gemini model for fast, cost-conscious workloads.",
808 ]
809 );
810 }
811
812 #[test]
813 fn test_parse_model_rejects_retired_gpt_models() {
815 let codex_kind = AgentKind::Codex;
817
818 let parsed_gpt_54 = codex_kind.parse_model("gpt-5.4");
820 let parsed_gpt_54_mini = codex_kind.parse_model("gpt-5.4-mini");
821
822 assert_eq!(parsed_gpt_54, None);
824 assert_eq!(parsed_gpt_54_mini, None);
825 }
826
827 #[test]
828 fn test_parse_model_rejects_retired_claude_aliases() {
831 let claude_kind = AgentKind::Claude;
833
834 let parsed_opus_46 = claude_kind.parse_model("claude-opus-4-6");
836 let parsed_opus_47 = claude_kind.parse_model("claude-opus-4-7");
837 let parsed_sonnet_46 = claude_kind.parse_model("claude-sonnet-4-6");
838
839 assert_eq!(parsed_opus_46, None);
841 assert_eq!(parsed_opus_47, None);
842 assert_eq!(parsed_sonnet_46, None);
843 }
844
845 #[test]
846 fn test_parse_model_parses_current_claude_models() {
848 let claude_kind = AgentKind::Claude;
850
851 let parsed_opus_5 = claude_kind.parse_model("claude-opus-5");
853 let parsed_sonnet_5 = claude_kind.parse_model("claude-sonnet-5");
854 let parsed_fable_5 = claude_kind.parse_model("claude-fable-5");
855 let parsed_haiku_45 = claude_kind.parse_model("claude-haiku-4-5-20251001");
856 let opus_5_id = AgentModel::ClaudeOpus5.as_str();
857 let opus_5_description = AgentModel::ClaudeOpus5.description();
858
859 assert_eq!(parsed_opus_5, Some(AgentModel::ClaudeOpus5));
861 assert_eq!(parsed_sonnet_5, Some(AgentModel::ClaudeSonnet5));
862 assert_eq!(parsed_fable_5, Some(AgentModel::ClaudeFable5));
863 assert_eq!(parsed_haiku_45, Some(AgentModel::ClaudeHaiku4520251001));
864 assert_eq!(opus_5_id, "claude-opus-5");
865 assert_eq!(
866 opus_5_description,
867 "Latest Claude Opus model for complex tasks."
868 );
869 }
870
871 #[test]
872 fn test_retired_replacement_maps_only_retired_ids() {
875 let retired_ids = [
877 ("gemini-3-pro-preview", AgentModel::Gemini31Pro),
878 ("gemini-3.1-pro", AgentModel::Gemini31Pro),
879 ("gemini-3-flash-preview", AgentModel::Gemini37Flash),
880 ("gemini-3.6-flash", AgentModel::Gemini37Flash),
881 ("gemini-3.5-flash", AgentModel::Gemini35FlashLite),
882 (
883 "gemini-3.1-flash-lite-preview",
884 AgentModel::Gemini35FlashLite,
885 ),
886 ("claude-opus-4-8", AgentModel::ClaudeOpus5),
887 ("claude-opus-4-6", AgentModel::ClaudeOpus5),
888 ("claude-opus-4-7", AgentModel::ClaudeOpus5),
889 ("claude-sonnet-4-6", AgentModel::ClaudeSonnet5),
890 ("gpt-5.5", AgentModel::Gpt56Sol),
891 ("gpt-5.4-mini", AgentModel::Gpt56Luna),
892 ("gpt-5.4", AgentModel::Gpt56Sol),
893 ("gpt-5.3-codex", AgentModel::Gpt56Sol),
894 ("gpt-5.2-codex", AgentModel::Gpt53CodexSpark),
895 ];
896
897 let replacements =
899 retired_ids.map(|(retired_id, _)| AgentModel::retired_replacement(retired_id));
900 let selectable_parses = retired_ids.map(|(retired_id, _)| retired_id.parse::<AgentModel>());
901 let current_replacements = [
902 "gemini-3.1-pro-preview",
903 "gemini-3.7-flash",
904 "gemini-3.5-flash-lite",
905 "claude-opus-5",
906 ]
907 .map(AgentModel::retired_replacement);
908 let unknown_replacement = AgentModel::retired_replacement("not-a-model");
909
910 assert_eq!(
912 replacements,
913 retired_ids.map(|(_, replacement)| Some(replacement))
914 );
915 assert!(selectable_parses.iter().all(Result::is_err));
916 assert_eq!(current_replacements, [None; 4]);
917 assert_eq!(unknown_replacement, None);
918 }
919
920 #[test]
921 fn test_parse_persisted_handles_supported_and_retired_models() {
924 let parsed_opus_46 = AgentModel::parse_persisted("claude-opus-4-6");
928 let parsed_opus_47 = AgentModel::parse_persisted("claude-opus-4-7");
929 let parsed_sonnet_46 = AgentModel::parse_persisted("claude-sonnet-4-6");
930 let parsed_sonnet_5 = AgentModel::parse_persisted("claude-sonnet-5");
931 let parsed_gpt_54_mini = AgentModel::parse_persisted("gpt-5.4-mini");
932 let parsed_gpt_54 = AgentModel::parse_persisted("gpt-5.4");
933 let parsed_gemini_37_flash = AgentModel::parse_persisted("gemini-3.7-flash");
934 let parsed_gemini_36_flash = AgentModel::parse_persisted("gemini-3.6-flash");
935 let parsed_gemini_35_flash = AgentModel::parse_persisted("gemini-3.5-flash");
936 let parsed_gemini_3_flash_preview = AgentModel::parse_persisted("gemini-3-flash-preview");
937 let parsed_gemini_35_flash_lite = AgentModel::parse_persisted("gemini-3.5-flash-lite");
938 let parsed_gemini_31_pro = AgentModel::parse_persisted("gemini-3.1-pro");
939 let parsed_gemini_31_pro_preview = AgentModel::parse_persisted("gemini-3.1-pro-preview");
940 let parsed_gemini_31_flash_lite_preview =
941 AgentModel::parse_persisted("gemini-3.1-flash-lite-preview");
942
943 assert_eq!(parsed_opus_46, Ok(AgentModel::ClaudeOpus5));
945 assert_eq!(parsed_opus_47, Ok(AgentModel::ClaudeOpus5));
946 assert_eq!(parsed_sonnet_46, Ok(AgentModel::ClaudeSonnet5));
947 assert_eq!(parsed_sonnet_5, Ok(AgentModel::ClaudeSonnet5));
948 assert_eq!(parsed_gpt_54_mini, Ok(AgentModel::Gpt56Luna));
949 assert_eq!(parsed_gpt_54, Ok(AgentModel::Gpt56Sol));
950 assert_eq!(parsed_gemini_37_flash, Ok(AgentModel::Gemini37Flash));
951 assert_eq!(parsed_gemini_36_flash, Ok(AgentModel::Gemini37Flash));
952 assert_eq!(parsed_gemini_35_flash, Ok(AgentModel::Gemini35FlashLite));
953 assert_eq!(parsed_gemini_3_flash_preview, Ok(AgentModel::Gemini37Flash));
954 assert_eq!(
955 parsed_gemini_35_flash_lite,
956 Ok(AgentModel::Gemini35FlashLite)
957 );
958 assert_eq!(parsed_gemini_31_pro, Ok(AgentModel::Gemini31Pro));
959 assert_eq!(parsed_gemini_31_pro_preview, Ok(AgentModel::Gemini31Pro));
960 assert_eq!(
961 parsed_gemini_31_flash_lite_preview,
962 Ok(AgentModel::Gemini35FlashLite)
963 );
964 }
965
966 #[test]
967 fn test_parse_persisted_session_agent_model_prefers_saved_agent() {
970 let selection = parse_persisted_session_agent_model(Some("codex"), "gemini-3.7-flash");
974
975 assert_eq!(selection.kind(), AgentKind::Codex);
977 assert_eq!(selection.model(), AgentKind::Codex.default_model());
978 }
979
980 #[test]
981 fn test_parse_persisted_session_agent_model_preserves_antigravity_models() {
984 let selection =
988 parse_persisted_session_agent_model(Some("antigravity"), "gemini-3.7-flash");
989
990 assert_eq!(selection.kind(), AgentKind::Antigravity);
992 assert_eq!(selection.model(), AgentModel::Gemini37Flash);
993 }
994
995 #[test]
996 fn test_parse_persisted_session_agent_model_resolves_saved_google_models() {
999 let persisted_selections = [
1001 ("gemini", "gemini-3.5-flash-lite"),
1002 ("gemini", "gemini-3.5-flash"),
1003 ("antigravity", "gemini-3.5-flash-lite"),
1004 ("antigravity", "gemini-3.5-flash"),
1005 ];
1006
1007 let migrated_selections = persisted_selections.map(|(agent_value, model_value)| {
1009 parse_persisted_session_agent_model(Some(agent_value), model_value)
1010 });
1011
1012 assert_eq!(
1014 migrated_selections,
1015 [
1016 AgentSelection::new(AgentKind::Gemini, AgentModel::Gemini35FlashLite),
1017 AgentSelection::new(AgentKind::Gemini, AgentModel::Gemini35FlashLite),
1018 AgentSelection::new(AgentKind::Antigravity, AgentModel::Gemini35FlashLite),
1019 AgentSelection::new(AgentKind::Antigravity, AgentModel::Gemini35FlashLite),
1020 ]
1021 );
1022 }
1023
1024 #[test]
1025 fn test_parse_persisted_session_agent_model_supports_legacy_rows() {
1028 let selection = parse_persisted_session_agent_model(None, "claude-opus-4-6");
1032
1033 assert_eq!(selection.kind(), AgentKind::Claude);
1035 assert_eq!(selection.model(), AgentModel::ClaudeOpus5);
1036 }
1037
1038 #[test]
1039 fn test_parse_persisted_session_agent_model_defaults_legacy_gemini_to_antigravity() {
1042 let selection = parse_persisted_session_agent_model(None, "gemini-3.7-flash");
1046
1047 assert_eq!(selection.kind(), AgentKind::Antigravity);
1049 assert_eq!(selection.model(), AgentModel::Gemini37Flash);
1050 }
1051
1052 #[test]
1053 fn test_parse_persisted_session_agent_model_resolves_legacy_gemini_rows() {
1056 let migrated_flash = parse_persisted_session_agent_model(None, "gemini-3.5-flash");
1060 let loaded_flash_lite = parse_persisted_session_agent_model(None, "gemini-3.5-flash-lite");
1061
1062 assert_eq!(
1064 migrated_flash,
1065 AgentSelection::new(AgentKind::Antigravity, AgentModel::Gemini35FlashLite)
1066 );
1067 assert_eq!(
1068 loaded_flash_lite,
1069 AgentSelection::new(AgentKind::Antigravity, AgentModel::Gemini35FlashLite)
1070 );
1071 }
1072
1073 #[test]
1074 fn test_reasoning_level_from_str_parses_supported_values() {
1076 let low_level = "low".parse::<ReasoningLevel>();
1080 let medium_level = "medium".parse::<ReasoningLevel>();
1081 let high_level = "high".parse::<ReasoningLevel>();
1082 let xhigh_level = "xhigh".parse::<ReasoningLevel>();
1083 let max_level = "max".parse::<ReasoningLevel>();
1084
1085 assert_eq!(low_level, Ok(ReasoningLevel::Low));
1087 assert_eq!(medium_level, Ok(ReasoningLevel::Medium));
1088 assert_eq!(high_level, Ok(ReasoningLevel::High));
1089 assert_eq!(xhigh_level, Ok(ReasoningLevel::XHigh));
1090 assert_eq!(max_level, Ok(ReasoningLevel::Max));
1091 }
1092
1093 #[test]
1094 fn test_reasoning_level_from_str_rejects_unknown_values() {
1096 let parse_result = "minimal".parse::<ReasoningLevel>();
1100
1101 assert!(parse_result.is_err());
1103 }
1104
1105 #[test]
1106 fn test_codex_models_are_supported_by_codex() {
1108 let models = [
1110 AgentModel::Gpt56Sol,
1111 AgentModel::Gpt56Terra,
1112 AgentModel::Gpt56Luna,
1113 AgentModel::Gpt53CodexSpark,
1114 ];
1115
1116 let supported = models.map(|model| AgentKind::Codex.supports_model(model));
1118 let unsupported = models.map(|model| AgentKind::Claude.supports_model(model));
1119
1120 assert_eq!(supported, [true; 4]);
1122 assert_eq!(unsupported, [false; 4]);
1123 }
1124
1125 #[test]
1126 fn test_claude_models_are_supported_by_claude() {
1128 let models = [
1130 AgentModel::ClaudeOpus5,
1131 AgentModel::ClaudeSonnet5,
1132 AgentModel::ClaudeFable5,
1133 AgentModel::ClaudeHaiku4520251001,
1134 ];
1135
1136 let supported = models.map(|model| AgentKind::Claude.supports_model(model));
1138 let unsupported = models.map(|model| AgentKind::Codex.supports_model(model));
1139
1140 assert_eq!(supported, [true; 4]);
1142 assert_eq!(unsupported, [false; 4]);
1143 }
1144
1145 #[test]
1146 fn test_gemini_models_are_supported_by_gemini_and_antigravity() {
1148 let models = [
1150 AgentModel::Gemini31Pro,
1151 AgentModel::Gemini37Flash,
1152 AgentModel::Gemini35FlashLite,
1153 ];
1154
1155 let antigravity_supported =
1157 models.map(|model| AgentKind::Antigravity.supports_model(model));
1158 let gemini_supported = models.map(|model| AgentKind::Gemini.supports_model(model));
1159 let codex_supported = models.map(|model| AgentKind::Codex.supports_model(model));
1160
1161 assert_eq!(antigravity_supported, [true; 3]);
1163 assert_eq!(gemini_supported, [true; 3]);
1164 assert_eq!(codex_supported, [false; 3]);
1165 }
1166
1167 #[test]
1168 fn test_supports_speed_mode_covers_claude_and_codex_only() {
1170 let kinds = [
1172 AgentKind::Claude,
1173 AgentKind::Codex,
1174 AgentKind::Gemini,
1175 AgentKind::Antigravity,
1176 ];
1177
1178 let supported = kinds.map(AgentKind::supports_speed_mode);
1180
1181 assert_eq!(supported, [true, true, false, false]);
1183 }
1184
1185 #[test]
1186 fn test_antigravity_provider_model_str_returns_raw_gemini_model() {
1189 let model = AgentModel::Gemini35FlashLite;
1191
1192 let persisted_model = model.as_str();
1194 let provider_model = model.provider_model_str();
1195
1196 assert_eq!(persisted_model, "gemini-3.5-flash-lite");
1198 assert_eq!(provider_model, "gemini-3.5-flash-lite");
1199 }
1200
1201 #[test]
1202 fn test_reasoning_level_claude_maps_all_levels() {
1205 assert_eq!(ReasoningLevel::Low.claude(), "low");
1207 assert_eq!(ReasoningLevel::Medium.claude(), "medium");
1208 assert_eq!(ReasoningLevel::High.claude(), "high");
1209 assert_eq!(ReasoningLevel::XHigh.claude(), "max");
1210 assert_eq!(ReasoningLevel::Max.claude(), "max");
1211 }
1212
1213 #[test]
1214 fn test_reasoning_level_antigravity_maps_all_levels() {
1217 assert_eq!(ReasoningLevel::Low.antigravity(), "low");
1219 assert_eq!(ReasoningLevel::Medium.antigravity(), "medium");
1220 assert_eq!(ReasoningLevel::High.antigravity(), "high");
1221 assert_eq!(ReasoningLevel::XHigh.antigravity(), "high");
1222 assert_eq!(ReasoningLevel::Max.antigravity(), "high");
1223 }
1224
1225 #[test]
1226 fn test_reasoning_level_codex_maps_all_levels() {
1228 assert_eq!(ReasoningLevel::Low.codex(), "low");
1230 assert_eq!(ReasoningLevel::Medium.codex(), "medium");
1231 assert_eq!(ReasoningLevel::High.codex(), "high");
1232 assert_eq!(ReasoningLevel::XHigh.codex(), "xhigh");
1233 assert_eq!(ReasoningLevel::Max.codex(), "max");
1234 }
1235
1236 #[test]
1237 fn test_reasoning_level_as_str_returns_stable_persisted_values() {
1240 assert_eq!(ReasoningLevel::Low.as_str(), "low");
1242 assert_eq!(ReasoningLevel::Medium.as_str(), "medium");
1243 assert_eq!(ReasoningLevel::High.as_str(), "high");
1244 assert_eq!(ReasoningLevel::XHigh.as_str(), "xhigh");
1245 assert_eq!(ReasoningLevel::Max.as_str(), "max");
1246 }
1247
1248 #[test]
1249 fn test_selectable_models_for_agent_kinds_uses_provider_order() {
1251 let agent_kinds = [AgentKind::Codex, AgentKind::Antigravity];
1253
1254 let selectable_models = selectable_models_for_agent_kinds(&agent_kinds);
1256
1257 assert_eq!(
1259 selectable_models,
1260 vec![
1261 AgentModel::Gpt56Sol,
1262 AgentModel::Gpt56Terra,
1263 AgentModel::Gpt56Luna,
1264 AgentModel::Gpt53CodexSpark,
1265 AgentModel::Gemini31Pro,
1266 AgentModel::Gemini37Flash,
1267 AgentModel::Gemini35FlashLite,
1268 ]
1269 );
1270 }
1271
1272 #[test]
1273 fn test_selectable_models_for_agent_kinds_deduplicates_shared_models() {
1276 let agent_kinds = [AgentKind::Gemini, AgentKind::Antigravity];
1278
1279 let selectable_models = selectable_models_for_agent_kinds(&agent_kinds);
1281
1282 assert_eq!(
1284 selectable_models,
1285 vec![
1286 AgentModel::Gemini31Pro,
1287 AgentModel::Gemini37Flash,
1288 AgentModel::Gemini35FlashLite,
1289 ]
1290 );
1291 }
1292
1293 #[test]
1294 fn test_resolve_model_for_available_agent_kinds_prefers_available_fallback() {
1297 let unavailable_model = AgentModel::ClaudeOpus5;
1299 let available_agent_kinds = [AgentKind::Codex, AgentKind::Antigravity];
1300 let fallback_model = AgentModel::Gpt56Terra;
1301
1302 let resolved_model = resolve_model_for_available_agent_kinds(
1304 unavailable_model,
1305 &available_agent_kinds,
1306 fallback_model,
1307 );
1308
1309 assert_eq!(resolved_model, AgentModel::Gpt56Terra);
1311 }
1312
1313 #[test]
1314 fn test_resolve_model_for_available_agent_kinds_uses_first_available_default() {
1317 let unavailable_model = AgentModel::ClaudeOpus5;
1319 let available_agent_kinds = [AgentKind::Codex, AgentKind::Antigravity];
1320 let unavailable_fallback_model = AgentModel::ClaudeSonnet5;
1321
1322 let resolved_model = resolve_model_for_available_agent_kinds(
1324 unavailable_model,
1325 &available_agent_kinds,
1326 unavailable_fallback_model,
1327 );
1328
1329 assert_eq!(resolved_model, AgentKind::Codex.default_model());
1331 }
1332
1333 #[test]
1334 fn test_resolve_agent_selection_for_model_preserves_preferred_shared_provider() {
1337 let model = AgentModel::Gemini37Flash;
1339 let available_agent_kinds = [AgentKind::Gemini, AgentKind::Antigravity];
1340
1341 let resolved_selection = resolve_agent_selection_for_model(
1343 model,
1344 AgentKind::Antigravity,
1345 &available_agent_kinds,
1346 );
1347
1348 assert_eq!(
1350 resolved_selection,
1351 AgentSelection::new(AgentKind::Antigravity, model)
1352 );
1353 }
1354
1355 #[test]
1356 fn test_resolve_agent_selection_for_model_uses_available_provider_order() {
1359 let model = AgentModel::Gemini37Flash;
1361 let available_agent_kinds = [AgentKind::Gemini, AgentKind::Antigravity];
1362
1363 let resolved_selection =
1365 resolve_agent_selection_for_model(model, AgentKind::Codex, &available_agent_kinds);
1366
1367 assert_eq!(
1369 resolved_selection,
1370 AgentSelection::new(AgentKind::Gemini, model)
1371 );
1372 }
1373
1374 #[test]
1375 fn test_resolve_prompt_model_agent_kind_prefers_current_agent() {
1378 let available_agent_kinds = [AgentKind::Antigravity, AgentKind::Codex];
1380
1381 let resolved_agent_kind =
1383 resolve_prompt_model_agent_kind(AgentKind::Codex, &available_agent_kinds);
1384
1385 assert_eq!(resolved_agent_kind, Some(AgentKind::Codex));
1387 }
1388
1389 #[test]
1390 fn test_resolve_prompt_model_agent_kind_uses_first_available_agent() {
1393 let available_agent_kinds = [AgentKind::Antigravity, AgentKind::Codex];
1395
1396 let resolved_agent_kind =
1398 resolve_prompt_model_agent_kind(AgentKind::Claude, &available_agent_kinds);
1399
1400 assert_eq!(resolved_agent_kind, Some(AgentKind::Antigravity));
1402 }
1403}