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 ClaudeOpus48,
107 ClaudeSonnet5,
109 ClaudeFable5,
111 ClaudeHaiku4520251001,
113}
114
115#[derive(Debug, Clone, Copy, PartialEq, Eq)]
117pub struct AgentSelection {
118 kind: AgentKind,
119 model: AgentModel,
120}
121
122#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
124pub enum ReasoningLevel {
125 Low,
127 Medium,
129 #[default]
131 High,
132 XHigh,
134 Max,
136}
137
138impl AgentSelection {
139 #[must_use]
144 pub fn new(kind: AgentKind, model: AgentModel) -> Self {
145 let model = if kind.supports_model(model) {
146 model
147 } else {
148 kind.default_model()
149 };
150
151 Self { kind, model }
152 }
153
154 #[must_use]
156 pub fn kind(self) -> AgentKind {
157 self.kind
158 }
159
160 #[must_use]
162 pub fn model(self) -> AgentModel {
163 self.model
164 }
165}
166
167pub trait AgentSelectionMetadata {
169 fn name(&self) -> &'static str;
171
172 fn description(&self) -> &'static str;
174}
175
176impl AgentModel {
177 pub fn as_str(self) -> &'static str {
180 match self {
181 Self::Gpt56Sol => "gpt-5.6-sol",
182 Self::Gpt56Terra => "gpt-5.6-terra",
183 Self::Gpt56Luna => "gpt-5.6-luna",
184 Self::Gpt55 => "gpt-5.5",
185 Self::Gemini3FlashPreview => "gemini-3-flash-preview",
186 Self::Gemini36Flash => "gemini-3.6-flash",
187 Self::Gemini35FlashLite => "gemini-3.5-flash-lite",
188 Self::Gemini31ProPreview => "gemini-3.1-pro-preview",
189 Self::Gpt53CodexSpark => "gpt-5.3-codex-spark",
190 Self::ClaudeOpus48 => "claude-opus-4-8",
191 Self::ClaudeSonnet5 => "claude-sonnet-5",
192 Self::ClaudeFable5 => "claude-fable-5",
193 Self::ClaudeHaiku4520251001 => "claude-haiku-4-5-20251001",
194 }
195 }
196
197 pub fn provider_model_str(self) -> &'static str {
202 self.as_str()
203 }
204
205 pub fn parse_persisted(value: &str) -> Result<Self, String> {
217 match value {
218 "gemini-3.5-flash" => Ok(Self::Gemini36Flash),
219 "gemini-3.1-flash-lite-preview" => Ok(Self::Gemini35FlashLite),
220 "claude-opus-4-6" | "claude-opus-4-7" => Ok(Self::ClaudeOpus48),
221 "claude-sonnet-4-6" => Ok(Self::ClaudeSonnet5),
222 "gpt-5.4-mini" => Ok(Self::Gpt56Luna),
223 "gpt-5.4" => Ok(Self::Gpt55),
224 _ => value.parse(),
225 }
226 }
227}
228
229pub fn parse_persisted_session_agent_model(
238 agent_value: Option<&str>,
239 model_value: &str,
240) -> AgentSelection {
241 let parsed_agent = agent_value
242 .filter(|value| !value.trim().is_empty())
243 .and_then(|value| value.parse::<AgentKind>().ok());
244
245 if let Some(agent_kind) = parsed_agent {
246 return parse_model_for_persisted_agent(agent_kind, model_value);
247 }
248
249 let model = AgentModel::parse_persisted(model_value)
250 .unwrap_or_else(|_| AgentKind::Antigravity.default_model());
251 let agent_kind = legacy_agent_kind_for_model_value(model_value, model);
252
253 AgentSelection::new(agent_kind, model)
254}
255
256fn parse_model_for_persisted_agent(agent_kind: AgentKind, model_value: &str) -> AgentSelection {
259 if let Some(model) = agent_kind.parse_model(model_value) {
260 return AgentSelection::new(agent_kind, model);
261 }
262
263 if let Ok(model) = AgentModel::parse_persisted(model_value)
264 && agent_kind.supports_model(model)
265 {
266 return AgentSelection::new(agent_kind, model);
267 }
268
269 AgentSelection::new(agent_kind, agent_kind.default_model())
270}
271
272fn legacy_agent_kind_for_model_value(model_value: &str, model: AgentModel) -> AgentKind {
275 if model_value.starts_with("claude-") {
276 return AgentKind::Claude;
277 }
278
279 if model_value.starts_with("gpt-") {
280 return AgentKind::Codex;
281 }
282
283 if model_value.starts_with("gemini-") {
284 return AgentKind::Antigravity;
285 }
286
287 AgentKind::ALL
288 .iter()
289 .copied()
290 .find(|agent_kind| agent_kind.supports_model(model))
291 .unwrap_or(AgentKind::Antigravity)
292}
293
294#[must_use]
297pub fn selectable_models_for_agent_kinds(agent_kinds: &[AgentKind]) -> Vec<AgentModel> {
298 let mut models = Vec::new();
299 for model in agent_kinds
300 .iter()
301 .flat_map(|agent_kind| agent_kind.models())
302 .copied()
303 {
304 if !models.contains(&model) {
305 models.push(model);
306 }
307 }
308
309 models
310}
311
312#[must_use]
319pub fn resolve_model_for_available_agent_kinds(
320 model: AgentModel,
321 agent_kinds: &[AgentKind],
322 fallback_model: AgentModel,
323) -> AgentModel {
324 if agent_kinds
325 .iter()
326 .any(|agent_kind| agent_kind.supports_model(model))
327 {
328 return model;
329 }
330
331 if agent_kinds
332 .iter()
333 .any(|agent_kind| agent_kind.supports_model(fallback_model))
334 {
335 return fallback_model;
336 }
337
338 agent_kinds
339 .first()
340 .copied()
341 .map_or(fallback_model, AgentKind::default_model)
342}
343
344#[must_use]
351pub fn resolve_agent_kind_for_model(
352 model: AgentModel,
353 agent_kinds: &[AgentKind],
354 fallback_agent_kind: AgentKind,
355) -> AgentKind {
356 agent_kinds
357 .iter()
358 .copied()
359 .find(|agent_kind| agent_kind.supports_model(model))
360 .unwrap_or(fallback_agent_kind)
361}
362
363#[must_use]
370pub fn resolve_agent_selection_for_model(
371 model: AgentModel,
372 preferred_agent_kind: AgentKind,
373 agent_kinds: &[AgentKind],
374) -> AgentSelection {
375 let agent_kind = if preferred_agent_kind.supports_model(model) {
376 preferred_agent_kind
377 } else {
378 resolve_agent_kind_for_model(model, agent_kinds, preferred_agent_kind)
379 };
380
381 AgentSelection::new(agent_kind, model)
382}
383
384#[must_use]
390pub fn resolve_prompt_model_agent_kind(
391 session_agent_kind: AgentKind,
392 agent_kinds: &[AgentKind],
393) -> Option<AgentKind> {
394 if agent_kinds.contains(&session_agent_kind) {
395 return Some(session_agent_kind);
396 }
397
398 agent_kinds.first().copied()
399}
400
401impl ReasoningLevel {
402 pub const ALL: [Self; 5] = [Self::Low, Self::Medium, Self::High, Self::XHigh, Self::Max];
404
405 pub fn as_str(self) -> &'static str {
410 match self {
411 Self::Low => "low",
412 Self::Medium => "medium",
413 Self::High => "high",
414 Self::XHigh => "xhigh",
415 Self::Max => "max",
416 }
417 }
418
419 pub fn codex(self) -> &'static str {
421 match self {
422 Self::Low => "low",
423 Self::Medium => "medium",
424 Self::High => "high",
425 Self::XHigh => "xhigh",
426 Self::Max => "max",
427 }
428 }
429
430 pub fn claude(self) -> &'static str {
436 match self {
437 Self::Low => "low",
438 Self::Medium => "medium",
439 Self::High => "high",
440 Self::XHigh | Self::Max => "max",
441 }
442 }
443
444 pub fn description(self) -> &'static str {
446 match self {
447 Self::Low => "Fastest responses with lighter reasoning.",
448 Self::Medium => "Balanced speed and reasoning depth.",
449 Self::High => "Deeper reasoning for tougher tasks.",
450 Self::XHigh => "Extra-high reasoning for complex tasks.",
451 Self::Max => "Maximum reasoning effort for the hardest tasks.",
452 }
453 }
454}
455
456impl FromStr for ReasoningLevel {
457 type Err = String;
458
459 fn from_str(value: &str) -> Result<Self, Self::Err> {
460 match value {
461 "low" => Ok(Self::Low),
462 "medium" => Ok(Self::Medium),
463 "high" => Ok(Self::High),
464 "xhigh" => Ok(Self::XHigh),
465 "max" => Ok(Self::Max),
466 other => Err(format!("unknown reasoning level: {other}")),
467 }
468 }
469}
470
471impl FromStr for AgentModel {
472 type Err = String;
473
474 fn from_str(value: &str) -> Result<Self, Self::Err> {
475 match value {
476 "gemini-3-flash-preview" => Ok(Self::Gemini3FlashPreview),
477 "gemini-3.6-flash" => Ok(Self::Gemini36Flash),
478 "gemini-3.5-flash-lite" => Ok(Self::Gemini35FlashLite),
479 "gemini-3.1-pro-preview" => Ok(Self::Gemini31ProPreview),
480 "gpt-5.6-sol" => Ok(Self::Gpt56Sol),
481 "gpt-5.6-terra" => Ok(Self::Gpt56Terra),
482 "gpt-5.6-luna" => Ok(Self::Gpt56Luna),
483 "gpt-5.5" => Ok(Self::Gpt55),
484 "gpt-5.3-codex-spark" => Ok(Self::Gpt53CodexSpark),
485 "claude-opus-4-8" => Ok(Self::ClaudeOpus48),
486 "claude-sonnet-5" => Ok(Self::ClaudeSonnet5),
487 "claude-fable-5" => Ok(Self::ClaudeFable5),
488 "claude-haiku-4-5-20251001" => Ok(Self::ClaudeHaiku4520251001),
489 other => Err(format!("unknown model: {other}")),
490 }
491 }
492}
493
494impl AgentSelectionMetadata for AgentModel {
495 fn name(&self) -> &'static str {
496 (*self).provider_model_str()
497 }
498
499 fn description(&self) -> &'static str {
500 match self {
501 Self::Gemini31ProPreview => "Higher-quality Gemini model for deeper reasoning.",
502 Self::Gemini36Flash => "Fast Gemini model for agentic and multimodal tasks.",
503 Self::Gemini35FlashLite => {
504 "Lightweight Gemini model for fast, cost-conscious workloads."
505 }
506 Self::Gemini3FlashPreview => "Fast Gemini model for quick iterations.",
507 Self::Gpt56Sol => "Newest Codex model for the strongest coding performance.",
508 Self::Gpt56Terra => "Current Codex model for balanced coding performance.",
509 Self::Gpt56Luna => "Current Codex model for lighter coding iterations.",
510 Self::Gpt55 => "Newer Codex model with stronger coding performance when available.",
511 Self::Gpt53CodexSpark => "Codex spark model for quick coding iterations.",
512 Self::ClaudeOpus48 => "Latest Claude Opus model for complex tasks.",
513 Self::ClaudeSonnet5 => "Balanced Claude model for quality and latency.",
514 Self::ClaudeFable5 => "Claude Fable model for creative, narrative-heavy tasks.",
515 Self::ClaudeHaiku4520251001 => "Fast Claude model for lighter tasks.",
516 }
517 }
518}
519
520impl AgentKind {
521 pub const ALL: &[AgentKind] = &[
523 AgentKind::Gemini,
524 AgentKind::Antigravity,
525 AgentKind::Claude,
526 AgentKind::Codex,
527 ];
528
529 pub fn executable_name(self) -> &'static str {
531 match self {
532 Self::Antigravity => "agy",
533 Self::Gemini => "gemini",
534 Self::Claude => "claude",
535 Self::Codex => "codex",
536 }
537 }
538
539 pub fn default_model(self) -> AgentModel {
541 match self {
542 Self::Antigravity | Self::Gemini => AgentModel::Gemini31ProPreview,
543 Self::Claude => AgentModel::ClaudeFable5,
544 Self::Codex => AgentModel::Gpt56Sol,
545 }
546 }
547
548 pub fn model_str(self, model: AgentModel) -> Option<&'static str> {
550 if !self.supports_model(model) {
551 return None;
552 }
553
554 Some(model.as_str())
555 }
556
557 pub fn models(self) -> &'static [AgentModel] {
559 const ANTIGRAVITY_MODELS: &[AgentModel] = &[
560 AgentModel::Gemini31ProPreview,
561 AgentModel::Gemini36Flash,
562 AgentModel::Gemini35FlashLite,
563 AgentModel::Gemini3FlashPreview,
564 ];
565 const GEMINI_MODELS: &[AgentModel] = &[
566 AgentModel::Gemini31ProPreview,
567 AgentModel::Gemini36Flash,
568 AgentModel::Gemini35FlashLite,
569 AgentModel::Gemini3FlashPreview,
570 ];
571 const CLAUDE_MODELS: &[AgentModel] = &[
572 AgentModel::ClaudeFable5,
573 AgentModel::ClaudeOpus48,
574 AgentModel::ClaudeSonnet5,
575 AgentModel::ClaudeHaiku4520251001,
576 ];
577 const CODEX_MODELS: &[AgentModel] = &[
578 AgentModel::Gpt56Sol,
579 AgentModel::Gpt56Terra,
580 AgentModel::Gpt56Luna,
581 AgentModel::Gpt55,
582 AgentModel::Gpt53CodexSpark,
583 ];
584
585 match self {
586 Self::Antigravity => ANTIGRAVITY_MODELS,
587 Self::Gemini => GEMINI_MODELS,
588 Self::Claude => CLAUDE_MODELS,
589 Self::Codex => CODEX_MODELS,
590 }
591 }
592
593 pub fn parse_model(self, value: &str) -> Option<AgentModel> {
595 let model = value.parse::<AgentModel>().ok()?;
596 if !self.supports_model(model) {
597 return None;
598 }
599
600 Some(model)
601 }
602
603 pub fn supports_model(self, model: AgentModel) -> bool {
605 self.models().contains(&model)
606 }
607}
608
609impl AgentSelectionMetadata for AgentKind {
610 fn name(&self) -> &'static str {
611 match self {
612 Self::Antigravity => "antigravity",
613 Self::Gemini => "gemini",
614 Self::Claude => "claude",
615 Self::Codex => "codex",
616 }
617 }
618
619 fn description(&self) -> &'static str {
620 match self {
621 Self::Antigravity => "Google Antigravity CLI agent.",
622 Self::Gemini => "Google Gemini CLI agent.",
623 Self::Claude => "Anthropic Claude Code agent.",
624 Self::Codex => "OpenAI Codex CLI agent.",
625 }
626 }
627}
628
629impl AgentSelectionMetadata for ReasoningLevel {
630 fn name(&self) -> &'static str {
631 (*self).as_str()
632 }
633
634 fn description(&self) -> &'static str {
635 (*self).description()
636 }
637}
638
639impl fmt::Display for AgentKind {
640 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
641 write!(f, "{}", self.name())
642 }
643}
644
645impl FromStr for AgentKind {
646 type Err = String;
647
648 fn from_str(s: &str) -> Result<Self, Self::Err> {
649 match s.to_lowercase().as_str() {
650 "antigravity" | "agy" => Ok(Self::Antigravity),
651 "gemini" => Ok(Self::Gemini),
652 "claude" => Ok(Self::Claude),
653 "codex" => Ok(Self::Codex),
654 other => Err(format!("unknown agent kind: {other}")),
655 }
656 }
657}
658
659#[cfg(test)]
660mod tests {
661 use super::*;
662
663 #[test]
664 fn test_parse_model_returns_none_for_models_from_other_providers() {
666 let claude_kind = AgentKind::Claude;
668 let antigravity_model = AgentModel::Gemini3FlashPreview.as_str();
669
670 let parsed_model = claude_kind.parse_model(antigravity_model);
672
673 assert_eq!(parsed_model, None);
675 }
676
677 #[test]
678 fn test_parse_model_parses_current_codex_models() {
680 let codex_kind = AgentKind::Codex;
682
683 let parsed_sol = codex_kind.parse_model("gpt-5.6-sol");
685 let parsed_terra = codex_kind.parse_model("gpt-5.6-terra");
686 let parsed_luna = codex_kind.parse_model("gpt-5.6-luna");
687 let parsed_55 = codex_kind.parse_model("gpt-5.5");
688 let parsed_spark = codex_kind.parse_model("gpt-5.3-codex-spark");
689
690 assert_eq!(parsed_sol, Some(AgentModel::Gpt56Sol));
692 assert_eq!(parsed_terra, Some(AgentModel::Gpt56Terra));
693 assert_eq!(parsed_luna, Some(AgentModel::Gpt56Luna));
694 assert_eq!(parsed_55, Some(AgentModel::Gpt55));
695 assert_eq!(parsed_spark, Some(AgentModel::Gpt53CodexSpark));
696 }
697
698 #[test]
699 fn test_parse_model_parses_antigravity() {
702 let antigravity_kind = AgentKind::Antigravity;
704
705 let parsed_model = antigravity_kind.parse_model("gemini-3.6-flash");
707
708 assert_eq!(parsed_model, Some(AgentModel::Gemini36Flash));
710 }
711
712 #[test]
713 fn test_parse_model_parses_direct_gemini() {
716 let gemini_kind = AgentKind::Gemini;
718
719 let parsed_model = gemini_kind.parse_model("gemini-3.6-flash");
721
722 assert_eq!(parsed_model, Some(AgentModel::Gemini36Flash));
724 }
725
726 #[test]
727 fn test_current_gemini_models_have_current_descriptions() {
729 let models = [AgentModel::Gemini36Flash, AgentModel::Gemini35FlashLite];
731
732 let descriptions = models.map(|model| model.description());
734
735 assert_eq!(
737 descriptions,
738 [
739 "Fast Gemini model for agentic and multimodal tasks.",
740 "Lightweight Gemini model for fast, cost-conscious workloads.",
741 ]
742 );
743 }
744
745 #[test]
746 fn test_parse_model_rejects_retired_gpt_models() {
748 let codex_kind = AgentKind::Codex;
750
751 let parsed_gpt_54 = codex_kind.parse_model("gpt-5.4");
753 let parsed_gpt_54_mini = codex_kind.parse_model("gpt-5.4-mini");
754
755 assert_eq!(parsed_gpt_54, None);
757 assert_eq!(parsed_gpt_54_mini, None);
758 }
759
760 #[test]
761 fn test_parse_model_rejects_retired_claude_aliases() {
764 let claude_kind = AgentKind::Claude;
766
767 let parsed_opus_46 = claude_kind.parse_model("claude-opus-4-6");
769 let parsed_opus_47 = claude_kind.parse_model("claude-opus-4-7");
770 let parsed_sonnet_46 = claude_kind.parse_model("claude-sonnet-4-6");
771
772 assert_eq!(parsed_opus_46, None);
774 assert_eq!(parsed_opus_47, None);
775 assert_eq!(parsed_sonnet_46, None);
776 }
777
778 #[test]
779 fn test_parse_model_parses_current_claude_models() {
781 let claude_kind = AgentKind::Claude;
783
784 let parsed_sonnet_5 = claude_kind.parse_model("claude-sonnet-5");
786 let parsed_fable_5 = claude_kind.parse_model("claude-fable-5");
787 let parsed_haiku_45 = claude_kind.parse_model("claude-haiku-4-5-20251001");
788
789 assert_eq!(parsed_sonnet_5, Some(AgentModel::ClaudeSonnet5));
791 assert_eq!(parsed_fable_5, Some(AgentModel::ClaudeFable5));
792 assert_eq!(parsed_haiku_45, Some(AgentModel::ClaudeHaiku4520251001));
793 }
794
795 #[test]
796 fn test_parse_persisted_handles_supported_and_retired_models() {
799 let parsed_opus_46 = AgentModel::parse_persisted("claude-opus-4-6");
803 let parsed_opus_47 = AgentModel::parse_persisted("claude-opus-4-7");
804 let parsed_sonnet_46 = AgentModel::parse_persisted("claude-sonnet-4-6");
805 let parsed_sonnet_5 = AgentModel::parse_persisted("claude-sonnet-5");
806 let parsed_gpt_54_mini = AgentModel::parse_persisted("gpt-5.4-mini");
807 let parsed_gpt_54 = AgentModel::parse_persisted("gpt-5.4");
808 let parsed_gemini_36_flash = AgentModel::parse_persisted("gemini-3.6-flash");
809 let parsed_gemini_35_flash = AgentModel::parse_persisted("gemini-3.5-flash");
810 let parsed_gemini_31_flash_lite_preview =
811 AgentModel::parse_persisted("gemini-3.1-flash-lite-preview");
812
813 assert_eq!(parsed_opus_46, Ok(AgentModel::ClaudeOpus48));
815 assert_eq!(parsed_opus_47, Ok(AgentModel::ClaudeOpus48));
816 assert_eq!(parsed_sonnet_46, Ok(AgentModel::ClaudeSonnet5));
817 assert_eq!(parsed_sonnet_5, Ok(AgentModel::ClaudeSonnet5));
818 assert_eq!(parsed_gpt_54_mini, Ok(AgentModel::Gpt56Luna));
819 assert_eq!(parsed_gpt_54, Ok(AgentModel::Gpt55));
820 assert_eq!(parsed_gemini_36_flash, Ok(AgentModel::Gemini36Flash));
821 assert_eq!(parsed_gemini_35_flash, Ok(AgentModel::Gemini36Flash));
822 assert_eq!(
823 parsed_gemini_31_flash_lite_preview,
824 Ok(AgentModel::Gemini35FlashLite)
825 );
826 }
827
828 #[test]
829 fn test_parse_persisted_session_agent_model_prefers_saved_agent() {
832 let selection = parse_persisted_session_agent_model(Some("codex"), "gemini-3.6-flash");
836
837 assert_eq!(selection.kind(), AgentKind::Codex);
839 assert_eq!(selection.model(), AgentKind::Codex.default_model());
840 }
841
842 #[test]
843 fn test_parse_persisted_session_agent_model_preserves_antigravity_models() {
846 let selection =
850 parse_persisted_session_agent_model(Some("antigravity"), "gemini-3.6-flash");
851
852 assert_eq!(selection.kind(), AgentKind::Antigravity);
854 assert_eq!(selection.model(), AgentModel::Gemini36Flash);
855 }
856
857 #[test]
858 fn test_parse_persisted_session_agent_model_migrates_retired_saved_google_models() {
861 let persisted_selections = [
863 ("gemini", "gemini-3.5-flash"),
864 ("gemini", "gemini-3.1-flash-lite-preview"),
865 ("antigravity", "gemini-3.5-flash"),
866 ("antigravity", "gemini-3.1-flash-lite-preview"),
867 ];
868
869 let migrated_selections = persisted_selections.map(|(agent_value, model_value)| {
871 parse_persisted_session_agent_model(Some(agent_value), model_value)
872 });
873
874 assert_eq!(
876 migrated_selections,
877 [
878 AgentSelection::new(AgentKind::Gemini, AgentModel::Gemini36Flash),
879 AgentSelection::new(AgentKind::Gemini, AgentModel::Gemini35FlashLite),
880 AgentSelection::new(AgentKind::Antigravity, AgentModel::Gemini36Flash),
881 AgentSelection::new(AgentKind::Antigravity, AgentModel::Gemini35FlashLite),
882 ]
883 );
884 }
885
886 #[test]
887 fn test_parse_persisted_session_agent_model_supports_legacy_rows() {
890 let selection = parse_persisted_session_agent_model(None, "claude-opus-4-6");
894
895 assert_eq!(selection.kind(), AgentKind::Claude);
897 assert_eq!(selection.model(), AgentModel::ClaudeOpus48);
898 }
899
900 #[test]
901 fn test_parse_persisted_session_agent_model_defaults_legacy_gemini_to_antigravity() {
904 let selection = parse_persisted_session_agent_model(None, "gemini-3.6-flash");
908
909 assert_eq!(selection.kind(), AgentKind::Antigravity);
911 assert_eq!(selection.model(), AgentModel::Gemini36Flash);
912 }
913
914 #[test]
915 fn test_parse_persisted_session_agent_model_migrates_retired_legacy_gemini_rows() {
918 let migrated_flash = parse_persisted_session_agent_model(None, "gemini-3.5-flash");
922 let migrated_flash_lite =
923 parse_persisted_session_agent_model(None, "gemini-3.1-flash-lite-preview");
924
925 assert_eq!(
927 migrated_flash,
928 AgentSelection::new(AgentKind::Antigravity, AgentModel::Gemini36Flash)
929 );
930 assert_eq!(
931 migrated_flash_lite,
932 AgentSelection::new(AgentKind::Antigravity, AgentModel::Gemini35FlashLite)
933 );
934 }
935
936 #[test]
937 fn test_reasoning_level_from_str_parses_supported_values() {
939 let low_level = "low".parse::<ReasoningLevel>();
943 let medium_level = "medium".parse::<ReasoningLevel>();
944 let high_level = "high".parse::<ReasoningLevel>();
945 let xhigh_level = "xhigh".parse::<ReasoningLevel>();
946 let max_level = "max".parse::<ReasoningLevel>();
947
948 assert_eq!(low_level, Ok(ReasoningLevel::Low));
950 assert_eq!(medium_level, Ok(ReasoningLevel::Medium));
951 assert_eq!(high_level, Ok(ReasoningLevel::High));
952 assert_eq!(xhigh_level, Ok(ReasoningLevel::XHigh));
953 assert_eq!(max_level, Ok(ReasoningLevel::Max));
954 }
955
956 #[test]
957 fn test_reasoning_level_from_str_rejects_unknown_values() {
959 let parse_result = "minimal".parse::<ReasoningLevel>();
963
964 assert!(parse_result.is_err());
966 }
967
968 #[test]
969 fn test_codex_models_are_supported_by_codex() {
971 let models = [
973 AgentModel::Gpt56Sol,
974 AgentModel::Gpt56Terra,
975 AgentModel::Gpt56Luna,
976 AgentModel::Gpt55,
977 AgentModel::Gpt53CodexSpark,
978 ];
979
980 let supported = models.map(|model| AgentKind::Codex.supports_model(model));
982 let unsupported = models.map(|model| AgentKind::Claude.supports_model(model));
983
984 assert_eq!(supported, [true; 5]);
986 assert_eq!(unsupported, [false; 5]);
987 }
988
989 #[test]
990 fn test_claude_models_are_supported_by_claude() {
992 let models = [
994 AgentModel::ClaudeOpus48,
995 AgentModel::ClaudeSonnet5,
996 AgentModel::ClaudeFable5,
997 AgentModel::ClaudeHaiku4520251001,
998 ];
999
1000 let supported = models.map(|model| AgentKind::Claude.supports_model(model));
1002 let unsupported = models.map(|model| AgentKind::Codex.supports_model(model));
1003
1004 assert_eq!(supported, [true; 4]);
1006 assert_eq!(unsupported, [false; 4]);
1007 }
1008
1009 #[test]
1010 fn test_gemini_models_are_supported_by_gemini_and_antigravity() {
1012 let models = [
1014 AgentModel::Gemini31ProPreview,
1015 AgentModel::Gemini36Flash,
1016 AgentModel::Gemini35FlashLite,
1017 AgentModel::Gemini3FlashPreview,
1018 ];
1019
1020 let antigravity_supported =
1022 models.map(|model| AgentKind::Antigravity.supports_model(model));
1023 let gemini_supported = models.map(|model| AgentKind::Gemini.supports_model(model));
1024 let codex_supported = models.map(|model| AgentKind::Codex.supports_model(model));
1025
1026 assert_eq!(antigravity_supported, [true; 4]);
1028 assert_eq!(gemini_supported, [true; 4]);
1029 assert_eq!(codex_supported, [false; 4]);
1030 }
1031
1032 #[test]
1033 fn test_antigravity_provider_model_str_returns_raw_gemini_model() {
1036 let model = AgentModel::Gemini35FlashLite;
1038
1039 let persisted_model = model.as_str();
1041 let provider_model = model.provider_model_str();
1042
1043 assert_eq!(persisted_model, "gemini-3.5-flash-lite");
1045 assert_eq!(provider_model, "gemini-3.5-flash-lite");
1046 }
1047
1048 #[test]
1049 fn test_reasoning_level_claude_maps_all_levels() {
1052 assert_eq!(ReasoningLevel::Low.claude(), "low");
1054 assert_eq!(ReasoningLevel::Medium.claude(), "medium");
1055 assert_eq!(ReasoningLevel::High.claude(), "high");
1056 assert_eq!(ReasoningLevel::XHigh.claude(), "max");
1057 assert_eq!(ReasoningLevel::Max.claude(), "max");
1058 }
1059
1060 #[test]
1061 fn test_reasoning_level_codex_maps_all_levels() {
1063 assert_eq!(ReasoningLevel::Low.codex(), "low");
1065 assert_eq!(ReasoningLevel::Medium.codex(), "medium");
1066 assert_eq!(ReasoningLevel::High.codex(), "high");
1067 assert_eq!(ReasoningLevel::XHigh.codex(), "xhigh");
1068 assert_eq!(ReasoningLevel::Max.codex(), "max");
1069 }
1070
1071 #[test]
1072 fn test_reasoning_level_as_str_returns_stable_persisted_values() {
1075 assert_eq!(ReasoningLevel::Low.as_str(), "low");
1077 assert_eq!(ReasoningLevel::Medium.as_str(), "medium");
1078 assert_eq!(ReasoningLevel::High.as_str(), "high");
1079 assert_eq!(ReasoningLevel::XHigh.as_str(), "xhigh");
1080 assert_eq!(ReasoningLevel::Max.as_str(), "max");
1081 }
1082
1083 #[test]
1084 fn test_selectable_models_for_agent_kinds_uses_provider_order() {
1086 let agent_kinds = [AgentKind::Codex, AgentKind::Antigravity];
1088
1089 let selectable_models = selectable_models_for_agent_kinds(&agent_kinds);
1091
1092 assert_eq!(
1094 selectable_models,
1095 vec![
1096 AgentModel::Gpt56Sol,
1097 AgentModel::Gpt56Terra,
1098 AgentModel::Gpt56Luna,
1099 AgentModel::Gpt55,
1100 AgentModel::Gpt53CodexSpark,
1101 AgentModel::Gemini31ProPreview,
1102 AgentModel::Gemini36Flash,
1103 AgentModel::Gemini35FlashLite,
1104 AgentModel::Gemini3FlashPreview,
1105 ]
1106 );
1107 }
1108
1109 #[test]
1110 fn test_selectable_models_for_agent_kinds_deduplicates_shared_models() {
1113 let agent_kinds = [AgentKind::Gemini, AgentKind::Antigravity];
1115
1116 let selectable_models = selectable_models_for_agent_kinds(&agent_kinds);
1118
1119 assert_eq!(
1121 selectable_models,
1122 vec![
1123 AgentModel::Gemini31ProPreview,
1124 AgentModel::Gemini36Flash,
1125 AgentModel::Gemini35FlashLite,
1126 AgentModel::Gemini3FlashPreview,
1127 ]
1128 );
1129 }
1130
1131 #[test]
1132 fn test_resolve_model_for_available_agent_kinds_prefers_available_fallback() {
1135 let unavailable_model = AgentModel::ClaudeOpus48;
1137 let available_agent_kinds = [AgentKind::Codex, AgentKind::Antigravity];
1138 let fallback_model = AgentModel::Gpt55;
1139
1140 let resolved_model = resolve_model_for_available_agent_kinds(
1142 unavailable_model,
1143 &available_agent_kinds,
1144 fallback_model,
1145 );
1146
1147 assert_eq!(resolved_model, AgentModel::Gpt55);
1149 }
1150
1151 #[test]
1152 fn test_resolve_model_for_available_agent_kinds_uses_first_available_default() {
1155 let unavailable_model = AgentModel::ClaudeOpus48;
1157 let available_agent_kinds = [AgentKind::Codex, AgentKind::Antigravity];
1158 let unavailable_fallback_model = AgentModel::ClaudeSonnet5;
1159
1160 let resolved_model = resolve_model_for_available_agent_kinds(
1162 unavailable_model,
1163 &available_agent_kinds,
1164 unavailable_fallback_model,
1165 );
1166
1167 assert_eq!(resolved_model, AgentKind::Codex.default_model());
1169 }
1170
1171 #[test]
1172 fn test_resolve_agent_selection_for_model_preserves_preferred_shared_provider() {
1175 let model = AgentModel::Gemini36Flash;
1177 let available_agent_kinds = [AgentKind::Gemini, AgentKind::Antigravity];
1178
1179 let resolved_selection = resolve_agent_selection_for_model(
1181 model,
1182 AgentKind::Antigravity,
1183 &available_agent_kinds,
1184 );
1185
1186 assert_eq!(
1188 resolved_selection,
1189 AgentSelection::new(AgentKind::Antigravity, model)
1190 );
1191 }
1192
1193 #[test]
1194 fn test_resolve_agent_selection_for_model_uses_available_provider_order() {
1197 let model = AgentModel::Gemini36Flash;
1199 let available_agent_kinds = [AgentKind::Gemini, AgentKind::Antigravity];
1200
1201 let resolved_selection =
1203 resolve_agent_selection_for_model(model, AgentKind::Codex, &available_agent_kinds);
1204
1205 assert_eq!(
1207 resolved_selection,
1208 AgentSelection::new(AgentKind::Gemini, model)
1209 );
1210 }
1211
1212 #[test]
1213 fn test_resolve_prompt_model_agent_kind_prefers_current_agent() {
1216 let available_agent_kinds = [AgentKind::Antigravity, AgentKind::Codex];
1218
1219 let resolved_agent_kind =
1221 resolve_prompt_model_agent_kind(AgentKind::Codex, &available_agent_kinds);
1222
1223 assert_eq!(resolved_agent_kind, Some(AgentKind::Codex));
1225 }
1226
1227 #[test]
1228 fn test_resolve_prompt_model_agent_kind_uses_first_available_agent() {
1231 let available_agent_kinds = [AgentKind::Antigravity, AgentKind::Codex];
1233
1234 let resolved_agent_kind =
1236 resolve_prompt_model_agent_kind(AgentKind::Claude, &available_agent_kinds);
1237
1238 assert_eq!(resolved_agent_kind, Some(AgentKind::Antigravity));
1240 }
1241}