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-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::Gemini36Flash),
234 ("gemini-3.5-flash", AgentModel::Gemini35FlashLite),
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-preview" => 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-preview");
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_parse_model_rejects_non_preview_gemini_31_pro() {
777 let agent_kinds = [AgentKind::Gemini, AgentKind::Antigravity];
779
780 let parsed_models = agent_kinds.map(|kind| kind.parse_model("gemini-3.1-pro"));
782
783 assert_eq!(parsed_models, [None; 2]);
785 }
786
787 #[test]
788 fn test_current_gemini_models_have_current_descriptions() {
790 let models = [
792 AgentModel::Gemini31Pro,
793 AgentModel::Gemini36Flash,
794 AgentModel::Gemini35FlashLite,
795 ];
796
797 let descriptions = models.map(|model| model.description());
799
800 assert_eq!(
802 descriptions,
803 [
804 "Higher-quality Gemini model for deeper reasoning.",
805 "Fast Gemini model for agentic and multimodal tasks.",
806 "Lightweight Gemini model for fast, cost-conscious workloads.",
807 ]
808 );
809 }
810
811 #[test]
812 fn test_parse_model_rejects_retired_gpt_models() {
814 let codex_kind = AgentKind::Codex;
816
817 let parsed_gpt_54 = codex_kind.parse_model("gpt-5.4");
819 let parsed_gpt_54_mini = codex_kind.parse_model("gpt-5.4-mini");
820
821 assert_eq!(parsed_gpt_54, None);
823 assert_eq!(parsed_gpt_54_mini, None);
824 }
825
826 #[test]
827 fn test_parse_model_rejects_retired_claude_aliases() {
830 let claude_kind = AgentKind::Claude;
832
833 let parsed_opus_46 = claude_kind.parse_model("claude-opus-4-6");
835 let parsed_opus_47 = claude_kind.parse_model("claude-opus-4-7");
836 let parsed_sonnet_46 = claude_kind.parse_model("claude-sonnet-4-6");
837
838 assert_eq!(parsed_opus_46, None);
840 assert_eq!(parsed_opus_47, None);
841 assert_eq!(parsed_sonnet_46, None);
842 }
843
844 #[test]
845 fn test_parse_model_parses_current_claude_models() {
847 let claude_kind = AgentKind::Claude;
849
850 let parsed_opus_5 = claude_kind.parse_model("claude-opus-5");
852 let parsed_sonnet_5 = claude_kind.parse_model("claude-sonnet-5");
853 let parsed_fable_5 = claude_kind.parse_model("claude-fable-5");
854 let parsed_haiku_45 = claude_kind.parse_model("claude-haiku-4-5-20251001");
855 let opus_5_id = AgentModel::ClaudeOpus5.as_str();
856 let opus_5_description = AgentModel::ClaudeOpus5.description();
857
858 assert_eq!(parsed_opus_5, Some(AgentModel::ClaudeOpus5));
860 assert_eq!(parsed_sonnet_5, Some(AgentModel::ClaudeSonnet5));
861 assert_eq!(parsed_fable_5, Some(AgentModel::ClaudeFable5));
862 assert_eq!(parsed_haiku_45, Some(AgentModel::ClaudeHaiku4520251001));
863 assert_eq!(opus_5_id, "claude-opus-5");
864 assert_eq!(
865 opus_5_description,
866 "Latest Claude Opus model for complex tasks."
867 );
868 }
869
870 #[test]
871 fn test_retired_replacement_maps_only_retired_ids() {
874 let retired_ids = [
876 ("gemini-3-pro-preview", AgentModel::Gemini31Pro),
877 ("gemini-3.1-pro", AgentModel::Gemini31Pro),
878 ("gemini-3-flash-preview", AgentModel::Gemini36Flash),
879 ("gemini-3.5-flash", AgentModel::Gemini35FlashLite),
880 (
881 "gemini-3.1-flash-lite-preview",
882 AgentModel::Gemini35FlashLite,
883 ),
884 ("claude-opus-4-8", AgentModel::ClaudeOpus5),
885 ("claude-opus-4-6", AgentModel::ClaudeOpus5),
886 ("claude-opus-4-7", AgentModel::ClaudeOpus5),
887 ("claude-sonnet-4-6", AgentModel::ClaudeSonnet5),
888 ("gpt-5.5", AgentModel::Gpt56Sol),
889 ("gpt-5.4-mini", AgentModel::Gpt56Luna),
890 ("gpt-5.4", AgentModel::Gpt56Sol),
891 ("gpt-5.3-codex", AgentModel::Gpt56Sol),
892 ("gpt-5.2-codex", AgentModel::Gpt53CodexSpark),
893 ];
894
895 let replacements =
897 retired_ids.map(|(retired_id, _)| AgentModel::retired_replacement(retired_id));
898 let selectable_parses = retired_ids.map(|(retired_id, _)| retired_id.parse::<AgentModel>());
899 let current_replacements = [
900 "gemini-3.1-pro-preview",
901 "gemini-3.6-flash",
902 "gemini-3.5-flash-lite",
903 "claude-opus-5",
904 ]
905 .map(AgentModel::retired_replacement);
906 let unknown_replacement = AgentModel::retired_replacement("not-a-model");
907
908 assert_eq!(
910 replacements,
911 retired_ids.map(|(_, replacement)| Some(replacement))
912 );
913 assert!(selectable_parses.iter().all(Result::is_err));
914 assert_eq!(current_replacements, [None; 4]);
915 assert_eq!(unknown_replacement, None);
916 }
917
918 #[test]
919 fn test_parse_persisted_handles_supported_and_retired_models() {
922 let parsed_opus_46 = AgentModel::parse_persisted("claude-opus-4-6");
926 let parsed_opus_47 = AgentModel::parse_persisted("claude-opus-4-7");
927 let parsed_sonnet_46 = AgentModel::parse_persisted("claude-sonnet-4-6");
928 let parsed_sonnet_5 = AgentModel::parse_persisted("claude-sonnet-5");
929 let parsed_gpt_54_mini = AgentModel::parse_persisted("gpt-5.4-mini");
930 let parsed_gpt_54 = AgentModel::parse_persisted("gpt-5.4");
931 let parsed_gemini_36_flash = AgentModel::parse_persisted("gemini-3.6-flash");
932 let parsed_gemini_35_flash = AgentModel::parse_persisted("gemini-3.5-flash");
933 let parsed_gemini_3_flash_preview = AgentModel::parse_persisted("gemini-3-flash-preview");
934 let parsed_gemini_35_flash_lite = AgentModel::parse_persisted("gemini-3.5-flash-lite");
935 let parsed_gemini_31_pro = AgentModel::parse_persisted("gemini-3.1-pro");
936 let parsed_gemini_31_pro_preview = AgentModel::parse_persisted("gemini-3.1-pro-preview");
937 let parsed_gemini_31_flash_lite_preview =
938 AgentModel::parse_persisted("gemini-3.1-flash-lite-preview");
939
940 assert_eq!(parsed_opus_46, Ok(AgentModel::ClaudeOpus5));
942 assert_eq!(parsed_opus_47, Ok(AgentModel::ClaudeOpus5));
943 assert_eq!(parsed_sonnet_46, Ok(AgentModel::ClaudeSonnet5));
944 assert_eq!(parsed_sonnet_5, Ok(AgentModel::ClaudeSonnet5));
945 assert_eq!(parsed_gpt_54_mini, Ok(AgentModel::Gpt56Luna));
946 assert_eq!(parsed_gpt_54, Ok(AgentModel::Gpt56Sol));
947 assert_eq!(parsed_gemini_36_flash, Ok(AgentModel::Gemini36Flash));
948 assert_eq!(parsed_gemini_35_flash, Ok(AgentModel::Gemini35FlashLite));
949 assert_eq!(parsed_gemini_3_flash_preview, Ok(AgentModel::Gemini36Flash));
950 assert_eq!(
951 parsed_gemini_35_flash_lite,
952 Ok(AgentModel::Gemini35FlashLite)
953 );
954 assert_eq!(parsed_gemini_31_pro, Ok(AgentModel::Gemini31Pro));
955 assert_eq!(parsed_gemini_31_pro_preview, Ok(AgentModel::Gemini31Pro));
956 assert_eq!(
957 parsed_gemini_31_flash_lite_preview,
958 Ok(AgentModel::Gemini35FlashLite)
959 );
960 }
961
962 #[test]
963 fn test_parse_persisted_session_agent_model_prefers_saved_agent() {
966 let selection = parse_persisted_session_agent_model(Some("codex"), "gemini-3.6-flash");
970
971 assert_eq!(selection.kind(), AgentKind::Codex);
973 assert_eq!(selection.model(), AgentKind::Codex.default_model());
974 }
975
976 #[test]
977 fn test_parse_persisted_session_agent_model_preserves_antigravity_models() {
980 let selection =
984 parse_persisted_session_agent_model(Some("antigravity"), "gemini-3.6-flash");
985
986 assert_eq!(selection.kind(), AgentKind::Antigravity);
988 assert_eq!(selection.model(), AgentModel::Gemini36Flash);
989 }
990
991 #[test]
992 fn test_parse_persisted_session_agent_model_resolves_saved_google_models() {
995 let persisted_selections = [
997 ("gemini", "gemini-3.5-flash-lite"),
998 ("gemini", "gemini-3.5-flash"),
999 ("antigravity", "gemini-3.5-flash-lite"),
1000 ("antigravity", "gemini-3.5-flash"),
1001 ];
1002
1003 let migrated_selections = persisted_selections.map(|(agent_value, model_value)| {
1005 parse_persisted_session_agent_model(Some(agent_value), model_value)
1006 });
1007
1008 assert_eq!(
1010 migrated_selections,
1011 [
1012 AgentSelection::new(AgentKind::Gemini, AgentModel::Gemini35FlashLite),
1013 AgentSelection::new(AgentKind::Gemini, AgentModel::Gemini35FlashLite),
1014 AgentSelection::new(AgentKind::Antigravity, AgentModel::Gemini35FlashLite),
1015 AgentSelection::new(AgentKind::Antigravity, AgentModel::Gemini35FlashLite),
1016 ]
1017 );
1018 }
1019
1020 #[test]
1021 fn test_parse_persisted_session_agent_model_supports_legacy_rows() {
1024 let selection = parse_persisted_session_agent_model(None, "claude-opus-4-6");
1028
1029 assert_eq!(selection.kind(), AgentKind::Claude);
1031 assert_eq!(selection.model(), AgentModel::ClaudeOpus5);
1032 }
1033
1034 #[test]
1035 fn test_parse_persisted_session_agent_model_defaults_legacy_gemini_to_antigravity() {
1038 let selection = parse_persisted_session_agent_model(None, "gemini-3.6-flash");
1042
1043 assert_eq!(selection.kind(), AgentKind::Antigravity);
1045 assert_eq!(selection.model(), AgentModel::Gemini36Flash);
1046 }
1047
1048 #[test]
1049 fn test_parse_persisted_session_agent_model_resolves_legacy_gemini_rows() {
1052 let migrated_flash = parse_persisted_session_agent_model(None, "gemini-3.5-flash");
1056 let loaded_flash_lite = parse_persisted_session_agent_model(None, "gemini-3.5-flash-lite");
1057
1058 assert_eq!(
1060 migrated_flash,
1061 AgentSelection::new(AgentKind::Antigravity, AgentModel::Gemini35FlashLite)
1062 );
1063 assert_eq!(
1064 loaded_flash_lite,
1065 AgentSelection::new(AgentKind::Antigravity, AgentModel::Gemini35FlashLite)
1066 );
1067 }
1068
1069 #[test]
1070 fn test_reasoning_level_from_str_parses_supported_values() {
1072 let low_level = "low".parse::<ReasoningLevel>();
1076 let medium_level = "medium".parse::<ReasoningLevel>();
1077 let high_level = "high".parse::<ReasoningLevel>();
1078 let xhigh_level = "xhigh".parse::<ReasoningLevel>();
1079 let max_level = "max".parse::<ReasoningLevel>();
1080
1081 assert_eq!(low_level, Ok(ReasoningLevel::Low));
1083 assert_eq!(medium_level, Ok(ReasoningLevel::Medium));
1084 assert_eq!(high_level, Ok(ReasoningLevel::High));
1085 assert_eq!(xhigh_level, Ok(ReasoningLevel::XHigh));
1086 assert_eq!(max_level, Ok(ReasoningLevel::Max));
1087 }
1088
1089 #[test]
1090 fn test_reasoning_level_from_str_rejects_unknown_values() {
1092 let parse_result = "minimal".parse::<ReasoningLevel>();
1096
1097 assert!(parse_result.is_err());
1099 }
1100
1101 #[test]
1102 fn test_codex_models_are_supported_by_codex() {
1104 let models = [
1106 AgentModel::Gpt56Sol,
1107 AgentModel::Gpt56Terra,
1108 AgentModel::Gpt56Luna,
1109 AgentModel::Gpt53CodexSpark,
1110 ];
1111
1112 let supported = models.map(|model| AgentKind::Codex.supports_model(model));
1114 let unsupported = models.map(|model| AgentKind::Claude.supports_model(model));
1115
1116 assert_eq!(supported, [true; 4]);
1118 assert_eq!(unsupported, [false; 4]);
1119 }
1120
1121 #[test]
1122 fn test_claude_models_are_supported_by_claude() {
1124 let models = [
1126 AgentModel::ClaudeOpus5,
1127 AgentModel::ClaudeSonnet5,
1128 AgentModel::ClaudeFable5,
1129 AgentModel::ClaudeHaiku4520251001,
1130 ];
1131
1132 let supported = models.map(|model| AgentKind::Claude.supports_model(model));
1134 let unsupported = models.map(|model| AgentKind::Codex.supports_model(model));
1135
1136 assert_eq!(supported, [true; 4]);
1138 assert_eq!(unsupported, [false; 4]);
1139 }
1140
1141 #[test]
1142 fn test_gemini_models_are_supported_by_gemini_and_antigravity() {
1144 let models = [
1146 AgentModel::Gemini31Pro,
1147 AgentModel::Gemini36Flash,
1148 AgentModel::Gemini35FlashLite,
1149 ];
1150
1151 let antigravity_supported =
1153 models.map(|model| AgentKind::Antigravity.supports_model(model));
1154 let gemini_supported = models.map(|model| AgentKind::Gemini.supports_model(model));
1155 let codex_supported = models.map(|model| AgentKind::Codex.supports_model(model));
1156
1157 assert_eq!(antigravity_supported, [true; 3]);
1159 assert_eq!(gemini_supported, [true; 3]);
1160 assert_eq!(codex_supported, [false; 3]);
1161 }
1162
1163 #[test]
1164 fn test_supports_speed_mode_covers_claude_and_codex_only() {
1166 let kinds = [
1168 AgentKind::Claude,
1169 AgentKind::Codex,
1170 AgentKind::Gemini,
1171 AgentKind::Antigravity,
1172 ];
1173
1174 let supported = kinds.map(AgentKind::supports_speed_mode);
1176
1177 assert_eq!(supported, [true, true, false, false]);
1179 }
1180
1181 #[test]
1182 fn test_antigravity_provider_model_str_returns_raw_gemini_model() {
1185 let model = AgentModel::Gemini35FlashLite;
1187
1188 let persisted_model = model.as_str();
1190 let provider_model = model.provider_model_str();
1191
1192 assert_eq!(persisted_model, "gemini-3.5-flash-lite");
1194 assert_eq!(provider_model, "gemini-3.5-flash-lite");
1195 }
1196
1197 #[test]
1198 fn test_reasoning_level_claude_maps_all_levels() {
1201 assert_eq!(ReasoningLevel::Low.claude(), "low");
1203 assert_eq!(ReasoningLevel::Medium.claude(), "medium");
1204 assert_eq!(ReasoningLevel::High.claude(), "high");
1205 assert_eq!(ReasoningLevel::XHigh.claude(), "max");
1206 assert_eq!(ReasoningLevel::Max.claude(), "max");
1207 }
1208
1209 #[test]
1210 fn test_reasoning_level_antigravity_maps_all_levels() {
1213 assert_eq!(ReasoningLevel::Low.antigravity(), "low");
1215 assert_eq!(ReasoningLevel::Medium.antigravity(), "medium");
1216 assert_eq!(ReasoningLevel::High.antigravity(), "high");
1217 assert_eq!(ReasoningLevel::XHigh.antigravity(), "high");
1218 assert_eq!(ReasoningLevel::Max.antigravity(), "high");
1219 }
1220
1221 #[test]
1222 fn test_reasoning_level_codex_maps_all_levels() {
1224 assert_eq!(ReasoningLevel::Low.codex(), "low");
1226 assert_eq!(ReasoningLevel::Medium.codex(), "medium");
1227 assert_eq!(ReasoningLevel::High.codex(), "high");
1228 assert_eq!(ReasoningLevel::XHigh.codex(), "xhigh");
1229 assert_eq!(ReasoningLevel::Max.codex(), "max");
1230 }
1231
1232 #[test]
1233 fn test_reasoning_level_as_str_returns_stable_persisted_values() {
1236 assert_eq!(ReasoningLevel::Low.as_str(), "low");
1238 assert_eq!(ReasoningLevel::Medium.as_str(), "medium");
1239 assert_eq!(ReasoningLevel::High.as_str(), "high");
1240 assert_eq!(ReasoningLevel::XHigh.as_str(), "xhigh");
1241 assert_eq!(ReasoningLevel::Max.as_str(), "max");
1242 }
1243
1244 #[test]
1245 fn test_selectable_models_for_agent_kinds_uses_provider_order() {
1247 let agent_kinds = [AgentKind::Codex, AgentKind::Antigravity];
1249
1250 let selectable_models = selectable_models_for_agent_kinds(&agent_kinds);
1252
1253 assert_eq!(
1255 selectable_models,
1256 vec![
1257 AgentModel::Gpt56Sol,
1258 AgentModel::Gpt56Terra,
1259 AgentModel::Gpt56Luna,
1260 AgentModel::Gpt53CodexSpark,
1261 AgentModel::Gemini31Pro,
1262 AgentModel::Gemini36Flash,
1263 AgentModel::Gemini35FlashLite,
1264 ]
1265 );
1266 }
1267
1268 #[test]
1269 fn test_selectable_models_for_agent_kinds_deduplicates_shared_models() {
1272 let agent_kinds = [AgentKind::Gemini, AgentKind::Antigravity];
1274
1275 let selectable_models = selectable_models_for_agent_kinds(&agent_kinds);
1277
1278 assert_eq!(
1280 selectable_models,
1281 vec![
1282 AgentModel::Gemini31Pro,
1283 AgentModel::Gemini36Flash,
1284 AgentModel::Gemini35FlashLite,
1285 ]
1286 );
1287 }
1288
1289 #[test]
1290 fn test_resolve_model_for_available_agent_kinds_prefers_available_fallback() {
1293 let unavailable_model = AgentModel::ClaudeOpus5;
1295 let available_agent_kinds = [AgentKind::Codex, AgentKind::Antigravity];
1296 let fallback_model = AgentModel::Gpt56Terra;
1297
1298 let resolved_model = resolve_model_for_available_agent_kinds(
1300 unavailable_model,
1301 &available_agent_kinds,
1302 fallback_model,
1303 );
1304
1305 assert_eq!(resolved_model, AgentModel::Gpt56Terra);
1307 }
1308
1309 #[test]
1310 fn test_resolve_model_for_available_agent_kinds_uses_first_available_default() {
1313 let unavailable_model = AgentModel::ClaudeOpus5;
1315 let available_agent_kinds = [AgentKind::Codex, AgentKind::Antigravity];
1316 let unavailable_fallback_model = AgentModel::ClaudeSonnet5;
1317
1318 let resolved_model = resolve_model_for_available_agent_kinds(
1320 unavailable_model,
1321 &available_agent_kinds,
1322 unavailable_fallback_model,
1323 );
1324
1325 assert_eq!(resolved_model, AgentKind::Codex.default_model());
1327 }
1328
1329 #[test]
1330 fn test_resolve_agent_selection_for_model_preserves_preferred_shared_provider() {
1333 let model = AgentModel::Gemini36Flash;
1335 let available_agent_kinds = [AgentKind::Gemini, AgentKind::Antigravity];
1336
1337 let resolved_selection = resolve_agent_selection_for_model(
1339 model,
1340 AgentKind::Antigravity,
1341 &available_agent_kinds,
1342 );
1343
1344 assert_eq!(
1346 resolved_selection,
1347 AgentSelection::new(AgentKind::Antigravity, model)
1348 );
1349 }
1350
1351 #[test]
1352 fn test_resolve_agent_selection_for_model_uses_available_provider_order() {
1355 let model = AgentModel::Gemini36Flash;
1357 let available_agent_kinds = [AgentKind::Gemini, AgentKind::Antigravity];
1358
1359 let resolved_selection =
1361 resolve_agent_selection_for_model(model, AgentKind::Codex, &available_agent_kinds);
1362
1363 assert_eq!(
1365 resolved_selection,
1366 AgentSelection::new(AgentKind::Gemini, model)
1367 );
1368 }
1369
1370 #[test]
1371 fn test_resolve_prompt_model_agent_kind_prefers_current_agent() {
1374 let available_agent_kinds = [AgentKind::Antigravity, AgentKind::Codex];
1376
1377 let resolved_agent_kind =
1379 resolve_prompt_model_agent_kind(AgentKind::Codex, &available_agent_kinds);
1380
1381 assert_eq!(resolved_agent_kind, Some(AgentKind::Codex));
1383 }
1384
1385 #[test]
1386 fn test_resolve_prompt_model_agent_kind_uses_first_available_agent() {
1389 let available_agent_kinds = [AgentKind::Antigravity, AgentKind::Codex];
1391
1392 let resolved_agent_kind =
1394 resolve_prompt_model_agent_kind(AgentKind::Claude, &available_agent_kinds);
1395
1396 assert_eq!(resolved_agent_kind, Some(AgentKind::Antigravity));
1398 }
1399}