1use std::fmt;
2use std::str::FromStr;
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum AgentKind {
7 Antigravity,
9 Gemini,
11 Claude,
13 Codex,
15}
16
17#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct AgentCliInfo {
21 pub executable_name: &'static str,
23 pub kind: AgentKind,
25 pub version: AgentCliVersion,
27}
28
29#[derive(Debug, Clone, PartialEq, Eq)]
32pub enum AgentCliVersion {
33 Loading,
36 Unknown,
39 Value(String),
41}
42
43impl AgentCliInfo {
44 pub fn new(kind: AgentKind, version: Option<String>) -> Self {
46 Self {
47 executable_name: kind.executable_name(),
48 kind,
49 version: version.map_or(AgentCliVersion::Unknown, AgentCliVersion::Value),
50 }
51 }
52
53 pub fn loading(kind: AgentKind) -> Self {
56 Self {
57 executable_name: kind.executable_name(),
58 kind,
59 version: AgentCliVersion::Loading,
60 }
61 }
62
63 pub fn from_kinds(agent_kinds: &[AgentKind]) -> Vec<Self> {
66 agent_kinds
67 .iter()
68 .copied()
69 .map(|agent_kind| Self::new(agent_kind, None))
70 .collect()
71 }
72
73 pub fn loading_from_kinds(agent_kinds: &[AgentKind]) -> Vec<Self> {
76 agent_kinds.iter().copied().map(Self::loading).collect()
77 }
78}
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86pub enum AgentModel {
87 Gpt56Sol,
89 Gpt56Terra,
91 Gpt56Luna,
93 Gpt55,
95 Gemini3FlashPreview,
97 Gemini36Flash,
99 Gemini35FlashLite,
101 Gemini31ProPreview,
103 Gpt53CodexSpark,
105 ClaudeOpus5,
107 ClaudeOpus48,
109 ClaudeSonnet5,
111 ClaudeFable5,
113 ClaudeHaiku4520251001,
115}
116
117#[derive(Debug, Clone, Copy, PartialEq, Eq)]
119pub struct AgentSelection {
120 kind: AgentKind,
121 model: AgentModel,
122}
123
124#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
126pub enum ReasoningLevel {
127 Low,
129 Medium,
131 #[default]
133 High,
134 XHigh,
136 Max,
138}
139
140impl AgentSelection {
141 #[must_use]
146 pub fn new(kind: AgentKind, model: AgentModel) -> Self {
147 let model = if kind.supports_model(model) {
148 model
149 } else {
150 kind.default_model()
151 };
152
153 Self { kind, model }
154 }
155
156 #[must_use]
158 pub fn kind(self) -> AgentKind {
159 self.kind
160 }
161
162 #[must_use]
164 pub fn model(self) -> AgentModel {
165 self.model
166 }
167}
168
169pub trait AgentSelectionMetadata {
171 fn name(&self) -> &'static str;
173
174 fn description(&self) -> &'static str;
176}
177
178impl AgentModel {
179 pub fn as_str(self) -> &'static str {
182 match self {
183 Self::Gpt56Sol => "gpt-5.6-sol",
184 Self::Gpt56Terra => "gpt-5.6-terra",
185 Self::Gpt56Luna => "gpt-5.6-luna",
186 Self::Gpt55 => "gpt-5.5",
187 Self::Gemini3FlashPreview => "gemini-3-flash-preview",
188 Self::Gemini36Flash => "gemini-3.6-flash",
189 Self::Gemini35FlashLite => "gemini-3.5-flash-lite",
190 Self::Gemini31ProPreview => "gemini-3.1-pro-preview",
191 Self::Gpt53CodexSpark => "gpt-5.3-codex-spark",
192 Self::ClaudeOpus5 => "claude-opus-5",
193 Self::ClaudeOpus48 => "claude-opus-4-8",
194 Self::ClaudeSonnet5 => "claude-sonnet-5",
195 Self::ClaudeFable5 => "claude-fable-5",
196 Self::ClaudeHaiku4520251001 => "claude-haiku-4-5-20251001",
197 }
198 }
199
200 pub fn provider_model_str(self) -> &'static str {
205 self.as_str()
206 }
207
208 pub fn parse_persisted(value: &str) -> Result<Self, String> {
220 match value {
221 "gemini-3.5-flash" => Ok(Self::Gemini36Flash),
222 "gemini-3.1-flash-lite-preview" => Ok(Self::Gemini35FlashLite),
223 "claude-opus-4-6" | "claude-opus-4-7" => Ok(Self::ClaudeOpus48),
224 "claude-sonnet-4-6" => Ok(Self::ClaudeSonnet5),
225 "gpt-5.4-mini" => Ok(Self::Gpt56Luna),
226 "gpt-5.4" => Ok(Self::Gpt55),
227 _ => value.parse(),
228 }
229 }
230}
231
232pub fn parse_persisted_session_agent_model(
241 agent_value: Option<&str>,
242 model_value: &str,
243) -> AgentSelection {
244 let parsed_agent = agent_value
245 .filter(|value| !value.trim().is_empty())
246 .and_then(|value| value.parse::<AgentKind>().ok());
247
248 if let Some(agent_kind) = parsed_agent {
249 return parse_model_for_persisted_agent(agent_kind, model_value);
250 }
251
252 let model = AgentModel::parse_persisted(model_value)
253 .unwrap_or_else(|_| AgentKind::Antigravity.default_model());
254 let agent_kind = legacy_agent_kind_for_model_value(model_value, model);
255
256 AgentSelection::new(agent_kind, model)
257}
258
259fn parse_model_for_persisted_agent(agent_kind: AgentKind, model_value: &str) -> AgentSelection {
262 if let Some(model) = agent_kind.parse_model(model_value) {
263 return AgentSelection::new(agent_kind, model);
264 }
265
266 if let Ok(model) = AgentModel::parse_persisted(model_value)
267 && agent_kind.supports_model(model)
268 {
269 return AgentSelection::new(agent_kind, model);
270 }
271
272 AgentSelection::new(agent_kind, agent_kind.default_model())
273}
274
275fn legacy_agent_kind_for_model_value(model_value: &str, model: AgentModel) -> AgentKind {
278 if model_value.starts_with("claude-") {
279 return AgentKind::Claude;
280 }
281
282 if model_value.starts_with("gpt-") {
283 return AgentKind::Codex;
284 }
285
286 if model_value.starts_with("gemini-") {
287 return AgentKind::Antigravity;
288 }
289
290 AgentKind::ALL
291 .iter()
292 .copied()
293 .find(|agent_kind| agent_kind.supports_model(model))
294 .unwrap_or(AgentKind::Antigravity)
295}
296
297#[must_use]
300pub fn selectable_models_for_agent_kinds(agent_kinds: &[AgentKind]) -> Vec<AgentModel> {
301 let mut models = Vec::new();
302 for model in agent_kinds
303 .iter()
304 .flat_map(|agent_kind| agent_kind.models())
305 .copied()
306 {
307 if !models.contains(&model) {
308 models.push(model);
309 }
310 }
311
312 models
313}
314
315#[must_use]
322pub fn resolve_model_for_available_agent_kinds(
323 model: AgentModel,
324 agent_kinds: &[AgentKind],
325 fallback_model: AgentModel,
326) -> AgentModel {
327 if agent_kinds
328 .iter()
329 .any(|agent_kind| agent_kind.supports_model(model))
330 {
331 return model;
332 }
333
334 if agent_kinds
335 .iter()
336 .any(|agent_kind| agent_kind.supports_model(fallback_model))
337 {
338 return fallback_model;
339 }
340
341 agent_kinds
342 .first()
343 .copied()
344 .map_or(fallback_model, AgentKind::default_model)
345}
346
347#[must_use]
354pub fn resolve_agent_kind_for_model(
355 model: AgentModel,
356 agent_kinds: &[AgentKind],
357 fallback_agent_kind: AgentKind,
358) -> AgentKind {
359 agent_kinds
360 .iter()
361 .copied()
362 .find(|agent_kind| agent_kind.supports_model(model))
363 .unwrap_or(fallback_agent_kind)
364}
365
366#[must_use]
373pub fn resolve_agent_selection_for_model(
374 model: AgentModel,
375 preferred_agent_kind: AgentKind,
376 agent_kinds: &[AgentKind],
377) -> AgentSelection {
378 let agent_kind = if preferred_agent_kind.supports_model(model) {
379 preferred_agent_kind
380 } else {
381 resolve_agent_kind_for_model(model, agent_kinds, preferred_agent_kind)
382 };
383
384 AgentSelection::new(agent_kind, model)
385}
386
387#[must_use]
393pub fn resolve_prompt_model_agent_kind(
394 session_agent_kind: AgentKind,
395 agent_kinds: &[AgentKind],
396) -> Option<AgentKind> {
397 if agent_kinds.contains(&session_agent_kind) {
398 return Some(session_agent_kind);
399 }
400
401 agent_kinds.first().copied()
402}
403
404impl ReasoningLevel {
405 pub const ALL: [Self; 5] = [Self::Low, Self::Medium, Self::High, Self::XHigh, Self::Max];
407
408 pub fn as_str(self) -> &'static str {
413 match self {
414 Self::Low => "low",
415 Self::Medium => "medium",
416 Self::High => "high",
417 Self::XHigh => "xhigh",
418 Self::Max => "max",
419 }
420 }
421
422 pub fn codex(self) -> &'static str {
424 match self {
425 Self::Low => "low",
426 Self::Medium => "medium",
427 Self::High => "high",
428 Self::XHigh => "xhigh",
429 Self::Max => "max",
430 }
431 }
432
433 pub fn antigravity(self) -> &'static str {
438 match self {
439 Self::Low => "low",
440 Self::Medium => "medium",
441 Self::High | Self::XHigh | Self::Max => "high",
442 }
443 }
444
445 pub fn claude(self) -> &'static str {
451 match self {
452 Self::Low => "low",
453 Self::Medium => "medium",
454 Self::High => "high",
455 Self::XHigh | Self::Max => "max",
456 }
457 }
458
459 pub fn description(self) -> &'static str {
461 match self {
462 Self::Low => "Fastest responses with lighter reasoning.",
463 Self::Medium => "Balanced speed and reasoning depth.",
464 Self::High => "Deeper reasoning for tougher tasks.",
465 Self::XHigh => "Extra-high reasoning for complex tasks.",
466 Self::Max => "Maximum reasoning effort for the hardest tasks.",
467 }
468 }
469}
470
471impl FromStr for ReasoningLevel {
472 type Err = String;
473
474 fn from_str(value: &str) -> Result<Self, Self::Err> {
475 match value {
476 "low" => Ok(Self::Low),
477 "medium" => Ok(Self::Medium),
478 "high" => Ok(Self::High),
479 "xhigh" => Ok(Self::XHigh),
480 "max" => Ok(Self::Max),
481 other => Err(format!("unknown reasoning level: {other}")),
482 }
483 }
484}
485
486impl FromStr for AgentModel {
487 type Err = String;
488
489 fn from_str(value: &str) -> Result<Self, Self::Err> {
490 match value {
491 "gemini-3-flash-preview" => Ok(Self::Gemini3FlashPreview),
492 "gemini-3.6-flash" => Ok(Self::Gemini36Flash),
493 "gemini-3.5-flash-lite" => Ok(Self::Gemini35FlashLite),
494 "gemini-3.1-pro-preview" => Ok(Self::Gemini31ProPreview),
495 "gpt-5.6-sol" => Ok(Self::Gpt56Sol),
496 "gpt-5.6-terra" => Ok(Self::Gpt56Terra),
497 "gpt-5.6-luna" => Ok(Self::Gpt56Luna),
498 "gpt-5.5" => Ok(Self::Gpt55),
499 "gpt-5.3-codex-spark" => Ok(Self::Gpt53CodexSpark),
500 "claude-opus-5" => Ok(Self::ClaudeOpus5),
501 "claude-opus-4-8" => Ok(Self::ClaudeOpus48),
502 "claude-sonnet-5" => Ok(Self::ClaudeSonnet5),
503 "claude-fable-5" => Ok(Self::ClaudeFable5),
504 "claude-haiku-4-5-20251001" => Ok(Self::ClaudeHaiku4520251001),
505 other => Err(format!("unknown model: {other}")),
506 }
507 }
508}
509
510impl AgentSelectionMetadata for AgentModel {
511 fn name(&self) -> &'static str {
512 (*self).provider_model_str()
513 }
514
515 fn description(&self) -> &'static str {
516 match self {
517 Self::Gemini31ProPreview => "Higher-quality Gemini model for deeper reasoning.",
518 Self::Gemini36Flash => "Fast Gemini model for agentic and multimodal tasks.",
519 Self::Gemini35FlashLite => {
520 "Lightweight Gemini model for fast, cost-conscious workloads."
521 }
522 Self::Gemini3FlashPreview => "Fast Gemini model for quick iterations.",
523 Self::Gpt56Sol => "Newest Codex model for the strongest coding performance.",
524 Self::Gpt56Terra => "Current Codex model for balanced coding performance.",
525 Self::Gpt56Luna => "Current Codex model for lighter coding iterations.",
526 Self::Gpt55 => "Newer Codex model with stronger coding performance when available.",
527 Self::Gpt53CodexSpark => "Codex spark model for quick coding iterations.",
528 Self::ClaudeOpus5 => "Latest Claude Opus model for complex tasks.",
529 Self::ClaudeOpus48 => "Claude Opus 4.8 model for complex tasks.",
530 Self::ClaudeSonnet5 => "Balanced Claude model for quality and latency.",
531 Self::ClaudeFable5 => "Claude Fable model for creative, narrative-heavy tasks.",
532 Self::ClaudeHaiku4520251001 => "Fast Claude model for lighter tasks.",
533 }
534 }
535}
536
537impl AgentKind {
538 pub const ALL: &[AgentKind] = &[
540 AgentKind::Gemini,
541 AgentKind::Antigravity,
542 AgentKind::Claude,
543 AgentKind::Codex,
544 ];
545
546 pub fn executable_name(self) -> &'static str {
548 match self {
549 Self::Antigravity => "agy",
550 Self::Gemini => "gemini",
551 Self::Claude => "claude",
552 Self::Codex => "codex",
553 }
554 }
555
556 pub fn default_model(self) -> AgentModel {
558 match self {
559 Self::Antigravity | Self::Gemini => AgentModel::Gemini31ProPreview,
560 Self::Claude => AgentModel::ClaudeFable5,
561 Self::Codex => AgentModel::Gpt56Sol,
562 }
563 }
564
565 pub fn model_str(self, model: AgentModel) -> Option<&'static str> {
567 if !self.supports_model(model) {
568 return None;
569 }
570
571 Some(model.as_str())
572 }
573
574 pub fn models(self) -> &'static [AgentModel] {
576 const ANTIGRAVITY_MODELS: &[AgentModel] = &[
577 AgentModel::Gemini31ProPreview,
578 AgentModel::Gemini36Flash,
579 AgentModel::Gemini35FlashLite,
580 AgentModel::Gemini3FlashPreview,
581 ];
582 const GEMINI_MODELS: &[AgentModel] = &[
583 AgentModel::Gemini31ProPreview,
584 AgentModel::Gemini36Flash,
585 AgentModel::Gemini35FlashLite,
586 AgentModel::Gemini3FlashPreview,
587 ];
588 const CLAUDE_MODELS: &[AgentModel] = &[
589 AgentModel::ClaudeFable5,
590 AgentModel::ClaudeOpus5,
591 AgentModel::ClaudeOpus48,
592 AgentModel::ClaudeSonnet5,
593 AgentModel::ClaudeHaiku4520251001,
594 ];
595 const CODEX_MODELS: &[AgentModel] = &[
596 AgentModel::Gpt56Sol,
597 AgentModel::Gpt56Terra,
598 AgentModel::Gpt56Luna,
599 AgentModel::Gpt55,
600 AgentModel::Gpt53CodexSpark,
601 ];
602
603 match self {
604 Self::Antigravity => ANTIGRAVITY_MODELS,
605 Self::Gemini => GEMINI_MODELS,
606 Self::Claude => CLAUDE_MODELS,
607 Self::Codex => CODEX_MODELS,
608 }
609 }
610
611 pub fn parse_model(self, value: &str) -> Option<AgentModel> {
613 let model = value.parse::<AgentModel>().ok()?;
614 if !self.supports_model(model) {
615 return None;
616 }
617
618 Some(model)
619 }
620
621 pub fn supports_model(self, model: AgentModel) -> bool {
623 self.models().contains(&model)
624 }
625}
626
627impl AgentSelectionMetadata for AgentKind {
628 fn name(&self) -> &'static str {
629 match self {
630 Self::Antigravity => "antigravity",
631 Self::Gemini => "gemini",
632 Self::Claude => "claude",
633 Self::Codex => "codex",
634 }
635 }
636
637 fn description(&self) -> &'static str {
638 match self {
639 Self::Antigravity => "Google Antigravity CLI agent.",
640 Self::Gemini => "Google Gemini CLI agent.",
641 Self::Claude => "Anthropic Claude Code agent.",
642 Self::Codex => "OpenAI Codex CLI agent.",
643 }
644 }
645}
646
647impl AgentSelectionMetadata for ReasoningLevel {
648 fn name(&self) -> &'static str {
649 (*self).as_str()
650 }
651
652 fn description(&self) -> &'static str {
653 (*self).description()
654 }
655}
656
657impl fmt::Display for AgentKind {
658 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
659 write!(f, "{}", self.name())
660 }
661}
662
663impl FromStr for AgentKind {
664 type Err = String;
665
666 fn from_str(s: &str) -> Result<Self, Self::Err> {
667 match s.to_lowercase().as_str() {
668 "antigravity" | "agy" => Ok(Self::Antigravity),
669 "gemini" => Ok(Self::Gemini),
670 "claude" => Ok(Self::Claude),
671 "codex" => Ok(Self::Codex),
672 other => Err(format!("unknown agent kind: {other}")),
673 }
674 }
675}
676
677#[cfg(test)]
678mod tests {
679 use super::*;
680
681 #[test]
682 fn test_parse_model_returns_none_for_models_from_other_providers() {
684 let claude_kind = AgentKind::Claude;
686 let antigravity_model = AgentModel::Gemini3FlashPreview.as_str();
687
688 let parsed_model = claude_kind.parse_model(antigravity_model);
690
691 assert_eq!(parsed_model, None);
693 }
694
695 #[test]
696 fn test_parse_model_parses_current_codex_models() {
698 let codex_kind = AgentKind::Codex;
700
701 let parsed_sol = codex_kind.parse_model("gpt-5.6-sol");
703 let parsed_terra = codex_kind.parse_model("gpt-5.6-terra");
704 let parsed_luna = codex_kind.parse_model("gpt-5.6-luna");
705 let parsed_55 = codex_kind.parse_model("gpt-5.5");
706 let parsed_spark = codex_kind.parse_model("gpt-5.3-codex-spark");
707
708 assert_eq!(parsed_sol, Some(AgentModel::Gpt56Sol));
710 assert_eq!(parsed_terra, Some(AgentModel::Gpt56Terra));
711 assert_eq!(parsed_luna, Some(AgentModel::Gpt56Luna));
712 assert_eq!(parsed_55, Some(AgentModel::Gpt55));
713 assert_eq!(parsed_spark, Some(AgentModel::Gpt53CodexSpark));
714 }
715
716 #[test]
717 fn test_parse_model_parses_antigravity() {
720 let antigravity_kind = AgentKind::Antigravity;
722
723 let parsed_model = antigravity_kind.parse_model("gemini-3.6-flash");
725
726 assert_eq!(parsed_model, Some(AgentModel::Gemini36Flash));
728 }
729
730 #[test]
731 fn test_parse_model_parses_direct_gemini() {
734 let gemini_kind = AgentKind::Gemini;
736
737 let parsed_model = gemini_kind.parse_model("gemini-3.6-flash");
739
740 assert_eq!(parsed_model, Some(AgentModel::Gemini36Flash));
742 }
743
744 #[test]
745 fn test_current_gemini_models_have_current_descriptions() {
747 let models = [AgentModel::Gemini36Flash, AgentModel::Gemini35FlashLite];
749
750 let descriptions = models.map(|model| model.description());
752
753 assert_eq!(
755 descriptions,
756 [
757 "Fast Gemini model for agentic and multimodal tasks.",
758 "Lightweight Gemini model for fast, cost-conscious workloads.",
759 ]
760 );
761 }
762
763 #[test]
764 fn test_parse_model_rejects_retired_gpt_models() {
766 let codex_kind = AgentKind::Codex;
768
769 let parsed_gpt_54 = codex_kind.parse_model("gpt-5.4");
771 let parsed_gpt_54_mini = codex_kind.parse_model("gpt-5.4-mini");
772
773 assert_eq!(parsed_gpt_54, None);
775 assert_eq!(parsed_gpt_54_mini, None);
776 }
777
778 #[test]
779 fn test_parse_model_rejects_retired_claude_aliases() {
782 let claude_kind = AgentKind::Claude;
784
785 let parsed_opus_46 = claude_kind.parse_model("claude-opus-4-6");
787 let parsed_opus_47 = claude_kind.parse_model("claude-opus-4-7");
788 let parsed_sonnet_46 = claude_kind.parse_model("claude-sonnet-4-6");
789
790 assert_eq!(parsed_opus_46, None);
792 assert_eq!(parsed_opus_47, None);
793 assert_eq!(parsed_sonnet_46, None);
794 }
795
796 #[test]
797 fn test_parse_model_parses_current_claude_models() {
799 let claude_kind = AgentKind::Claude;
801
802 let parsed_opus_5 = claude_kind.parse_model("claude-opus-5");
804 let parsed_sonnet_5 = claude_kind.parse_model("claude-sonnet-5");
805 let parsed_fable_5 = claude_kind.parse_model("claude-fable-5");
806 let parsed_haiku_45 = claude_kind.parse_model("claude-haiku-4-5-20251001");
807 let opus_5_id = AgentModel::ClaudeOpus5.as_str();
808 let opus_5_description = AgentModel::ClaudeOpus5.description();
809 let opus_48_description = AgentModel::ClaudeOpus48.description();
810
811 assert_eq!(parsed_opus_5, Some(AgentModel::ClaudeOpus5));
813 assert_eq!(parsed_sonnet_5, Some(AgentModel::ClaudeSonnet5));
814 assert_eq!(parsed_fable_5, Some(AgentModel::ClaudeFable5));
815 assert_eq!(parsed_haiku_45, Some(AgentModel::ClaudeHaiku4520251001));
816 assert_eq!(opus_5_id, "claude-opus-5");
817 assert_eq!(
818 opus_5_description,
819 "Latest Claude Opus model for complex tasks."
820 );
821 assert_eq!(
822 opus_48_description,
823 "Claude Opus 4.8 model for complex tasks."
824 );
825 }
826
827 #[test]
828 fn test_parse_persisted_handles_supported_and_retired_models() {
831 let parsed_opus_46 = AgentModel::parse_persisted("claude-opus-4-6");
835 let parsed_opus_47 = AgentModel::parse_persisted("claude-opus-4-7");
836 let parsed_sonnet_46 = AgentModel::parse_persisted("claude-sonnet-4-6");
837 let parsed_sonnet_5 = AgentModel::parse_persisted("claude-sonnet-5");
838 let parsed_gpt_54_mini = AgentModel::parse_persisted("gpt-5.4-mini");
839 let parsed_gpt_54 = AgentModel::parse_persisted("gpt-5.4");
840 let parsed_gemini_36_flash = AgentModel::parse_persisted("gemini-3.6-flash");
841 let parsed_gemini_35_flash = AgentModel::parse_persisted("gemini-3.5-flash");
842 let parsed_gemini_31_flash_lite_preview =
843 AgentModel::parse_persisted("gemini-3.1-flash-lite-preview");
844
845 assert_eq!(parsed_opus_46, Ok(AgentModel::ClaudeOpus48));
847 assert_eq!(parsed_opus_47, Ok(AgentModel::ClaudeOpus48));
848 assert_eq!(parsed_sonnet_46, Ok(AgentModel::ClaudeSonnet5));
849 assert_eq!(parsed_sonnet_5, Ok(AgentModel::ClaudeSonnet5));
850 assert_eq!(parsed_gpt_54_mini, Ok(AgentModel::Gpt56Luna));
851 assert_eq!(parsed_gpt_54, Ok(AgentModel::Gpt55));
852 assert_eq!(parsed_gemini_36_flash, Ok(AgentModel::Gemini36Flash));
853 assert_eq!(parsed_gemini_35_flash, Ok(AgentModel::Gemini36Flash));
854 assert_eq!(
855 parsed_gemini_31_flash_lite_preview,
856 Ok(AgentModel::Gemini35FlashLite)
857 );
858 }
859
860 #[test]
861 fn test_parse_persisted_session_agent_model_prefers_saved_agent() {
864 let selection = parse_persisted_session_agent_model(Some("codex"), "gemini-3.6-flash");
868
869 assert_eq!(selection.kind(), AgentKind::Codex);
871 assert_eq!(selection.model(), AgentKind::Codex.default_model());
872 }
873
874 #[test]
875 fn test_parse_persisted_session_agent_model_preserves_antigravity_models() {
878 let selection =
882 parse_persisted_session_agent_model(Some("antigravity"), "gemini-3.6-flash");
883
884 assert_eq!(selection.kind(), AgentKind::Antigravity);
886 assert_eq!(selection.model(), AgentModel::Gemini36Flash);
887 }
888
889 #[test]
890 fn test_parse_persisted_session_agent_model_migrates_retired_saved_google_models() {
893 let persisted_selections = [
895 ("gemini", "gemini-3.5-flash"),
896 ("gemini", "gemini-3.1-flash-lite-preview"),
897 ("antigravity", "gemini-3.5-flash"),
898 ("antigravity", "gemini-3.1-flash-lite-preview"),
899 ];
900
901 let migrated_selections = persisted_selections.map(|(agent_value, model_value)| {
903 parse_persisted_session_agent_model(Some(agent_value), model_value)
904 });
905
906 assert_eq!(
908 migrated_selections,
909 [
910 AgentSelection::new(AgentKind::Gemini, AgentModel::Gemini36Flash),
911 AgentSelection::new(AgentKind::Gemini, AgentModel::Gemini35FlashLite),
912 AgentSelection::new(AgentKind::Antigravity, AgentModel::Gemini36Flash),
913 AgentSelection::new(AgentKind::Antigravity, AgentModel::Gemini35FlashLite),
914 ]
915 );
916 }
917
918 #[test]
919 fn test_parse_persisted_session_agent_model_supports_legacy_rows() {
922 let selection = parse_persisted_session_agent_model(None, "claude-opus-4-6");
926
927 assert_eq!(selection.kind(), AgentKind::Claude);
929 assert_eq!(selection.model(), AgentModel::ClaudeOpus48);
930 }
931
932 #[test]
933 fn test_parse_persisted_session_agent_model_defaults_legacy_gemini_to_antigravity() {
936 let selection = parse_persisted_session_agent_model(None, "gemini-3.6-flash");
940
941 assert_eq!(selection.kind(), AgentKind::Antigravity);
943 assert_eq!(selection.model(), AgentModel::Gemini36Flash);
944 }
945
946 #[test]
947 fn test_parse_persisted_session_agent_model_migrates_retired_legacy_gemini_rows() {
950 let migrated_flash = parse_persisted_session_agent_model(None, "gemini-3.5-flash");
954 let migrated_flash_lite =
955 parse_persisted_session_agent_model(None, "gemini-3.1-flash-lite-preview");
956
957 assert_eq!(
959 migrated_flash,
960 AgentSelection::new(AgentKind::Antigravity, AgentModel::Gemini36Flash)
961 );
962 assert_eq!(
963 migrated_flash_lite,
964 AgentSelection::new(AgentKind::Antigravity, AgentModel::Gemini35FlashLite)
965 );
966 }
967
968 #[test]
969 fn test_reasoning_level_from_str_parses_supported_values() {
971 let low_level = "low".parse::<ReasoningLevel>();
975 let medium_level = "medium".parse::<ReasoningLevel>();
976 let high_level = "high".parse::<ReasoningLevel>();
977 let xhigh_level = "xhigh".parse::<ReasoningLevel>();
978 let max_level = "max".parse::<ReasoningLevel>();
979
980 assert_eq!(low_level, Ok(ReasoningLevel::Low));
982 assert_eq!(medium_level, Ok(ReasoningLevel::Medium));
983 assert_eq!(high_level, Ok(ReasoningLevel::High));
984 assert_eq!(xhigh_level, Ok(ReasoningLevel::XHigh));
985 assert_eq!(max_level, Ok(ReasoningLevel::Max));
986 }
987
988 #[test]
989 fn test_reasoning_level_from_str_rejects_unknown_values() {
991 let parse_result = "minimal".parse::<ReasoningLevel>();
995
996 assert!(parse_result.is_err());
998 }
999
1000 #[test]
1001 fn test_codex_models_are_supported_by_codex() {
1003 let models = [
1005 AgentModel::Gpt56Sol,
1006 AgentModel::Gpt56Terra,
1007 AgentModel::Gpt56Luna,
1008 AgentModel::Gpt55,
1009 AgentModel::Gpt53CodexSpark,
1010 ];
1011
1012 let supported = models.map(|model| AgentKind::Codex.supports_model(model));
1014 let unsupported = models.map(|model| AgentKind::Claude.supports_model(model));
1015
1016 assert_eq!(supported, [true; 5]);
1018 assert_eq!(unsupported, [false; 5]);
1019 }
1020
1021 #[test]
1022 fn test_claude_models_are_supported_by_claude() {
1024 let models = [
1026 AgentModel::ClaudeOpus5,
1027 AgentModel::ClaudeOpus48,
1028 AgentModel::ClaudeSonnet5,
1029 AgentModel::ClaudeFable5,
1030 AgentModel::ClaudeHaiku4520251001,
1031 ];
1032
1033 let supported = models.map(|model| AgentKind::Claude.supports_model(model));
1035 let unsupported = models.map(|model| AgentKind::Codex.supports_model(model));
1036
1037 assert_eq!(supported, [true; 5]);
1039 assert_eq!(unsupported, [false; 5]);
1040 }
1041
1042 #[test]
1043 fn test_gemini_models_are_supported_by_gemini_and_antigravity() {
1045 let models = [
1047 AgentModel::Gemini31ProPreview,
1048 AgentModel::Gemini36Flash,
1049 AgentModel::Gemini35FlashLite,
1050 AgentModel::Gemini3FlashPreview,
1051 ];
1052
1053 let antigravity_supported =
1055 models.map(|model| AgentKind::Antigravity.supports_model(model));
1056 let gemini_supported = models.map(|model| AgentKind::Gemini.supports_model(model));
1057 let codex_supported = models.map(|model| AgentKind::Codex.supports_model(model));
1058
1059 assert_eq!(antigravity_supported, [true; 4]);
1061 assert_eq!(gemini_supported, [true; 4]);
1062 assert_eq!(codex_supported, [false; 4]);
1063 }
1064
1065 #[test]
1066 fn test_antigravity_provider_model_str_returns_raw_gemini_model() {
1069 let model = AgentModel::Gemini35FlashLite;
1071
1072 let persisted_model = model.as_str();
1074 let provider_model = model.provider_model_str();
1075
1076 assert_eq!(persisted_model, "gemini-3.5-flash-lite");
1078 assert_eq!(provider_model, "gemini-3.5-flash-lite");
1079 }
1080
1081 #[test]
1082 fn test_reasoning_level_claude_maps_all_levels() {
1085 assert_eq!(ReasoningLevel::Low.claude(), "low");
1087 assert_eq!(ReasoningLevel::Medium.claude(), "medium");
1088 assert_eq!(ReasoningLevel::High.claude(), "high");
1089 assert_eq!(ReasoningLevel::XHigh.claude(), "max");
1090 assert_eq!(ReasoningLevel::Max.claude(), "max");
1091 }
1092
1093 #[test]
1094 fn test_reasoning_level_antigravity_maps_all_levels() {
1097 assert_eq!(ReasoningLevel::Low.antigravity(), "low");
1099 assert_eq!(ReasoningLevel::Medium.antigravity(), "medium");
1100 assert_eq!(ReasoningLevel::High.antigravity(), "high");
1101 assert_eq!(ReasoningLevel::XHigh.antigravity(), "high");
1102 assert_eq!(ReasoningLevel::Max.antigravity(), "high");
1103 }
1104
1105 #[test]
1106 fn test_reasoning_level_codex_maps_all_levels() {
1108 assert_eq!(ReasoningLevel::Low.codex(), "low");
1110 assert_eq!(ReasoningLevel::Medium.codex(), "medium");
1111 assert_eq!(ReasoningLevel::High.codex(), "high");
1112 assert_eq!(ReasoningLevel::XHigh.codex(), "xhigh");
1113 assert_eq!(ReasoningLevel::Max.codex(), "max");
1114 }
1115
1116 #[test]
1117 fn test_reasoning_level_as_str_returns_stable_persisted_values() {
1120 assert_eq!(ReasoningLevel::Low.as_str(), "low");
1122 assert_eq!(ReasoningLevel::Medium.as_str(), "medium");
1123 assert_eq!(ReasoningLevel::High.as_str(), "high");
1124 assert_eq!(ReasoningLevel::XHigh.as_str(), "xhigh");
1125 assert_eq!(ReasoningLevel::Max.as_str(), "max");
1126 }
1127
1128 #[test]
1129 fn test_selectable_models_for_agent_kinds_uses_provider_order() {
1131 let agent_kinds = [AgentKind::Codex, AgentKind::Antigravity];
1133
1134 let selectable_models = selectable_models_for_agent_kinds(&agent_kinds);
1136
1137 assert_eq!(
1139 selectable_models,
1140 vec![
1141 AgentModel::Gpt56Sol,
1142 AgentModel::Gpt56Terra,
1143 AgentModel::Gpt56Luna,
1144 AgentModel::Gpt55,
1145 AgentModel::Gpt53CodexSpark,
1146 AgentModel::Gemini31ProPreview,
1147 AgentModel::Gemini36Flash,
1148 AgentModel::Gemini35FlashLite,
1149 AgentModel::Gemini3FlashPreview,
1150 ]
1151 );
1152 }
1153
1154 #[test]
1155 fn test_selectable_models_for_agent_kinds_deduplicates_shared_models() {
1158 let agent_kinds = [AgentKind::Gemini, AgentKind::Antigravity];
1160
1161 let selectable_models = selectable_models_for_agent_kinds(&agent_kinds);
1163
1164 assert_eq!(
1166 selectable_models,
1167 vec![
1168 AgentModel::Gemini31ProPreview,
1169 AgentModel::Gemini36Flash,
1170 AgentModel::Gemini35FlashLite,
1171 AgentModel::Gemini3FlashPreview,
1172 ]
1173 );
1174 }
1175
1176 #[test]
1177 fn test_resolve_model_for_available_agent_kinds_prefers_available_fallback() {
1180 let unavailable_model = AgentModel::ClaudeOpus48;
1182 let available_agent_kinds = [AgentKind::Codex, AgentKind::Antigravity];
1183 let fallback_model = AgentModel::Gpt55;
1184
1185 let resolved_model = resolve_model_for_available_agent_kinds(
1187 unavailable_model,
1188 &available_agent_kinds,
1189 fallback_model,
1190 );
1191
1192 assert_eq!(resolved_model, AgentModel::Gpt55);
1194 }
1195
1196 #[test]
1197 fn test_resolve_model_for_available_agent_kinds_uses_first_available_default() {
1200 let unavailable_model = AgentModel::ClaudeOpus48;
1202 let available_agent_kinds = [AgentKind::Codex, AgentKind::Antigravity];
1203 let unavailable_fallback_model = AgentModel::ClaudeSonnet5;
1204
1205 let resolved_model = resolve_model_for_available_agent_kinds(
1207 unavailable_model,
1208 &available_agent_kinds,
1209 unavailable_fallback_model,
1210 );
1211
1212 assert_eq!(resolved_model, AgentKind::Codex.default_model());
1214 }
1215
1216 #[test]
1217 fn test_resolve_agent_selection_for_model_preserves_preferred_shared_provider() {
1220 let model = AgentModel::Gemini36Flash;
1222 let available_agent_kinds = [AgentKind::Gemini, AgentKind::Antigravity];
1223
1224 let resolved_selection = resolve_agent_selection_for_model(
1226 model,
1227 AgentKind::Antigravity,
1228 &available_agent_kinds,
1229 );
1230
1231 assert_eq!(
1233 resolved_selection,
1234 AgentSelection::new(AgentKind::Antigravity, model)
1235 );
1236 }
1237
1238 #[test]
1239 fn test_resolve_agent_selection_for_model_uses_available_provider_order() {
1242 let model = AgentModel::Gemini36Flash;
1244 let available_agent_kinds = [AgentKind::Gemini, AgentKind::Antigravity];
1245
1246 let resolved_selection =
1248 resolve_agent_selection_for_model(model, AgentKind::Codex, &available_agent_kinds);
1249
1250 assert_eq!(
1252 resolved_selection,
1253 AgentSelection::new(AgentKind::Gemini, model)
1254 );
1255 }
1256
1257 #[test]
1258 fn test_resolve_prompt_model_agent_kind_prefers_current_agent() {
1261 let available_agent_kinds = [AgentKind::Antigravity, AgentKind::Codex];
1263
1264 let resolved_agent_kind =
1266 resolve_prompt_model_agent_kind(AgentKind::Codex, &available_agent_kinds);
1267
1268 assert_eq!(resolved_agent_kind, Some(AgentKind::Codex));
1270 }
1271
1272 #[test]
1273 fn test_resolve_prompt_model_agent_kind_uses_first_available_agent() {
1276 let available_agent_kinds = [AgentKind::Antigravity, AgentKind::Codex];
1278
1279 let resolved_agent_kind =
1281 resolve_prompt_model_agent_kind(AgentKind::Claude, &available_agent_kinds);
1282
1283 assert_eq!(resolved_agent_kind, Some(AgentKind::Antigravity));
1285 }
1286}