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 Gpt55,
89 Gemini3FlashPreview,
91 Gemini35Flash,
93 Gemini31FlashLitePreview,
96 Gemini31ProPreview,
98 Gpt54Mini,
100 Gpt53CodexSpark,
102 ClaudeOpus48,
104 ClaudeSonnet5,
106 ClaudeFable5,
108 ClaudeHaiku4520251001,
110}
111
112#[derive(Debug, Clone, Copy, PartialEq, Eq)]
114pub struct AgentSelection {
115 kind: AgentKind,
116 model: AgentModel,
117}
118
119#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
121pub enum ReasoningLevel {
122 Low,
124 Medium,
126 #[default]
128 High,
129 XHigh,
131}
132
133impl AgentSelection {
134 #[must_use]
139 pub fn new(kind: AgentKind, model: AgentModel) -> Self {
140 let model = if kind.supports_model(model) {
141 model
142 } else {
143 kind.default_model()
144 };
145
146 Self { kind, model }
147 }
148
149 #[must_use]
151 pub fn kind(self) -> AgentKind {
152 self.kind
153 }
154
155 #[must_use]
157 pub fn model(self) -> AgentModel {
158 self.model
159 }
160}
161
162pub trait AgentSelectionMetadata {
164 fn name(&self) -> &'static str;
166
167 fn description(&self) -> &'static str;
169}
170
171impl AgentModel {
172 pub fn as_str(self) -> &'static str {
175 match self {
176 Self::Gpt55 => "gpt-5.5",
177 Self::Gemini3FlashPreview => "gemini-3-flash-preview",
178 Self::Gemini35Flash => "gemini-3.5-flash",
179 Self::Gemini31FlashLitePreview => "gemini-3.1-flash-lite-preview",
180 Self::Gemini31ProPreview => "gemini-3.1-pro-preview",
181 Self::Gpt54Mini => "gpt-5.4-mini",
182 Self::Gpt53CodexSpark => "gpt-5.3-codex-spark",
183 Self::ClaudeOpus48 => "claude-opus-4-8",
184 Self::ClaudeSonnet5 => "claude-sonnet-5",
185 Self::ClaudeFable5 => "claude-fable-5",
186 Self::ClaudeHaiku4520251001 => "claude-haiku-4-5-20251001",
187 }
188 }
189
190 pub fn provider_model_str(self) -> &'static str {
195 self.as_str()
196 }
197
198 pub fn parse_persisted(value: &str) -> Result<Self, String> {
210 match value {
211 "claude-opus-4-6" | "claude-opus-4-7" => Ok(Self::ClaudeOpus48),
212 "claude-sonnet-4-6" => Ok(Self::ClaudeSonnet5),
213 "gpt-5.4" => Ok(Self::Gpt55),
214 _ => value.parse(),
215 }
216 }
217}
218
219pub fn parse_persisted_session_agent_model(
228 agent_value: Option<&str>,
229 model_value: &str,
230) -> AgentSelection {
231 let parsed_agent = agent_value
232 .filter(|value| !value.trim().is_empty())
233 .and_then(|value| value.parse::<AgentKind>().ok());
234
235 if let Some(agent_kind) = parsed_agent {
236 return parse_model_for_persisted_agent(agent_kind, model_value);
237 }
238
239 let model = AgentModel::parse_persisted(model_value)
240 .unwrap_or_else(|_| AgentKind::Antigravity.default_model());
241 let agent_kind = legacy_agent_kind_for_model_value(model_value, model);
242
243 AgentSelection::new(agent_kind, model)
244}
245
246fn parse_model_for_persisted_agent(agent_kind: AgentKind, model_value: &str) -> AgentSelection {
249 if let Some(model) = agent_kind.parse_model(model_value) {
250 return AgentSelection::new(agent_kind, model);
251 }
252
253 if let Ok(model) = AgentModel::parse_persisted(model_value)
254 && agent_kind.supports_model(model)
255 {
256 return AgentSelection::new(agent_kind, model);
257 }
258
259 AgentSelection::new(agent_kind, agent_kind.default_model())
260}
261
262fn legacy_agent_kind_for_model_value(model_value: &str, model: AgentModel) -> AgentKind {
265 if model_value.starts_with("claude-") {
266 return AgentKind::Claude;
267 }
268
269 if model_value.starts_with("gpt-") {
270 return AgentKind::Codex;
271 }
272
273 if model_value.starts_with("gemini-") {
274 return AgentKind::Antigravity;
275 }
276
277 AgentKind::ALL
278 .iter()
279 .copied()
280 .find(|agent_kind| agent_kind.supports_model(model))
281 .unwrap_or(AgentKind::Antigravity)
282}
283
284#[must_use]
287pub fn selectable_models_for_agent_kinds(agent_kinds: &[AgentKind]) -> Vec<AgentModel> {
288 let mut models = Vec::new();
289 for model in agent_kinds
290 .iter()
291 .flat_map(|agent_kind| agent_kind.models())
292 .copied()
293 {
294 if !models.contains(&model) {
295 models.push(model);
296 }
297 }
298
299 models
300}
301
302#[must_use]
309pub fn resolve_model_for_available_agent_kinds(
310 model: AgentModel,
311 agent_kinds: &[AgentKind],
312 fallback_model: AgentModel,
313) -> AgentModel {
314 if agent_kinds
315 .iter()
316 .any(|agent_kind| agent_kind.supports_model(model))
317 {
318 return model;
319 }
320
321 if agent_kinds
322 .iter()
323 .any(|agent_kind| agent_kind.supports_model(fallback_model))
324 {
325 return fallback_model;
326 }
327
328 agent_kinds
329 .first()
330 .copied()
331 .map_or(fallback_model, AgentKind::default_model)
332}
333
334#[must_use]
341pub fn resolve_agent_kind_for_model(
342 model: AgentModel,
343 agent_kinds: &[AgentKind],
344 fallback_agent_kind: AgentKind,
345) -> AgentKind {
346 agent_kinds
347 .iter()
348 .copied()
349 .find(|agent_kind| agent_kind.supports_model(model))
350 .unwrap_or(fallback_agent_kind)
351}
352
353#[must_use]
360pub fn resolve_agent_selection_for_model(
361 model: AgentModel,
362 preferred_agent_kind: AgentKind,
363 agent_kinds: &[AgentKind],
364) -> AgentSelection {
365 let agent_kind = if preferred_agent_kind.supports_model(model) {
366 preferred_agent_kind
367 } else {
368 resolve_agent_kind_for_model(model, agent_kinds, preferred_agent_kind)
369 };
370
371 AgentSelection::new(agent_kind, model)
372}
373
374#[must_use]
380pub fn resolve_prompt_model_agent_kind(
381 session_agent_kind: AgentKind,
382 agent_kinds: &[AgentKind],
383) -> Option<AgentKind> {
384 if agent_kinds.contains(&session_agent_kind) {
385 return Some(session_agent_kind);
386 }
387
388 agent_kinds.first().copied()
389}
390
391impl ReasoningLevel {
392 pub const ALL: [Self; 4] = [Self::Low, Self::Medium, Self::High, Self::XHigh];
394
395 pub fn as_str(self) -> &'static str {
400 match self {
401 Self::Low => "low",
402 Self::Medium => "medium",
403 Self::High => "high",
404 Self::XHigh => "xhigh",
405 }
406 }
407
408 pub fn codex(self) -> &'static str {
410 match self {
411 Self::Low => "low",
412 Self::Medium => "medium",
413 Self::High => "high",
414 Self::XHigh => "xhigh",
415 }
416 }
417
418 pub fn claude(self) -> &'static str {
424 match self {
425 Self::Low => "low",
426 Self::Medium => "medium",
427 Self::High => "high",
428 Self::XHigh => "max",
429 }
430 }
431
432 pub fn description(self) -> &'static str {
434 match self {
435 Self::Low => "Fastest responses with lighter reasoning.",
436 Self::Medium => "Balanced speed and reasoning depth.",
437 Self::High => "Deeper reasoning for tougher tasks.",
438 Self::XHigh => "Maximum reasoning effort for the hardest tasks.",
439 }
440 }
441}
442
443impl FromStr for ReasoningLevel {
444 type Err = String;
445
446 fn from_str(value: &str) -> Result<Self, Self::Err> {
447 match value {
448 "low" => Ok(Self::Low),
449 "medium" => Ok(Self::Medium),
450 "high" => Ok(Self::High),
451 "xhigh" => Ok(Self::XHigh),
452 other => Err(format!("unknown reasoning level: {other}")),
453 }
454 }
455}
456
457impl FromStr for AgentModel {
458 type Err = String;
459
460 fn from_str(value: &str) -> Result<Self, Self::Err> {
461 match value {
462 "gemini-3-flash-preview" => Ok(Self::Gemini3FlashPreview),
463 "gemini-3.5-flash" => Ok(Self::Gemini35Flash),
464 "gemini-3.1-flash-lite-preview" => Ok(Self::Gemini31FlashLitePreview),
465 "gemini-3.1-pro-preview" => Ok(Self::Gemini31ProPreview),
466 "gpt-5.5" => Ok(Self::Gpt55),
467 "gpt-5.4-mini" => Ok(Self::Gpt54Mini),
468 "gpt-5.3-codex-spark" => Ok(Self::Gpt53CodexSpark),
469 "claude-opus-4-8" => Ok(Self::ClaudeOpus48),
470 "claude-sonnet-5" => Ok(Self::ClaudeSonnet5),
471 "claude-fable-5" => Ok(Self::ClaudeFable5),
472 "claude-haiku-4-5-20251001" => Ok(Self::ClaudeHaiku4520251001),
473 other => Err(format!("unknown model: {other}")),
474 }
475 }
476}
477
478impl AgentSelectionMetadata for AgentModel {
479 fn name(&self) -> &'static str {
480 (*self).provider_model_str()
481 }
482
483 fn description(&self) -> &'static str {
484 match self {
485 Self::Gemini31ProPreview => "Higher-quality Gemini model for deeper reasoning.",
486 Self::Gemini35Flash => "Fast Gemini model for current Flash workloads.",
487 Self::Gemini31FlashLitePreview => {
488 "Lightweight Gemini model for fast, cost-conscious iterations."
489 }
490 Self::Gemini3FlashPreview => "Fast Gemini model for quick iterations.",
491 Self::Gpt55 => "Newer Codex model with stronger coding performance when available.",
492 Self::Gpt54Mini => "Small, fast Codex model for simpler coding tasks.",
493 Self::Gpt53CodexSpark => "Codex spark model for quick coding iterations.",
494 Self::ClaudeOpus48 => "Latest Claude Opus model for complex tasks.",
495 Self::ClaudeSonnet5 => "Balanced Claude model for quality and latency.",
496 Self::ClaudeFable5 => "Claude Fable model for creative, narrative-heavy tasks.",
497 Self::ClaudeHaiku4520251001 => "Fast Claude model for lighter tasks.",
498 }
499 }
500}
501
502impl AgentKind {
503 pub const ALL: &[AgentKind] = &[
505 AgentKind::Gemini,
506 AgentKind::Antigravity,
507 AgentKind::Claude,
508 AgentKind::Codex,
509 ];
510
511 pub fn executable_name(self) -> &'static str {
513 match self {
514 Self::Antigravity => "agy",
515 Self::Gemini => "gemini",
516 Self::Claude => "claude",
517 Self::Codex => "codex",
518 }
519 }
520
521 pub fn default_model(self) -> AgentModel {
523 match self {
524 Self::Antigravity | Self::Gemini => AgentModel::Gemini31ProPreview,
525 Self::Claude => AgentModel::ClaudeFable5,
526 Self::Codex => AgentModel::Gpt55,
527 }
528 }
529
530 pub fn model_str(self, model: AgentModel) -> Option<&'static str> {
532 if !self.supports_model(model) {
533 return None;
534 }
535
536 Some(model.as_str())
537 }
538
539 pub fn models(self) -> &'static [AgentModel] {
541 const ANTIGRAVITY_MODELS: &[AgentModel] = &[
542 AgentModel::Gemini31ProPreview,
543 AgentModel::Gemini35Flash,
544 AgentModel::Gemini31FlashLitePreview,
545 AgentModel::Gemini3FlashPreview,
546 ];
547 const GEMINI_MODELS: &[AgentModel] = &[
548 AgentModel::Gemini31ProPreview,
549 AgentModel::Gemini35Flash,
550 AgentModel::Gemini31FlashLitePreview,
551 AgentModel::Gemini3FlashPreview,
552 ];
553 const CLAUDE_MODELS: &[AgentModel] = &[
554 AgentModel::ClaudeFable5,
555 AgentModel::ClaudeOpus48,
556 AgentModel::ClaudeSonnet5,
557 AgentModel::ClaudeHaiku4520251001,
558 ];
559 const CODEX_MODELS: &[AgentModel] = &[
560 AgentModel::Gpt55,
561 AgentModel::Gpt54Mini,
562 AgentModel::Gpt53CodexSpark,
563 ];
564
565 match self {
566 Self::Antigravity => ANTIGRAVITY_MODELS,
567 Self::Gemini => GEMINI_MODELS,
568 Self::Claude => CLAUDE_MODELS,
569 Self::Codex => CODEX_MODELS,
570 }
571 }
572
573 pub fn parse_model(self, value: &str) -> Option<AgentModel> {
575 let model = value.parse::<AgentModel>().ok()?;
576 if !self.supports_model(model) {
577 return None;
578 }
579
580 Some(model)
581 }
582
583 pub fn supports_model(self, model: AgentModel) -> bool {
585 self.models().contains(&model)
586 }
587}
588
589impl AgentSelectionMetadata for AgentKind {
590 fn name(&self) -> &'static str {
591 match self {
592 Self::Antigravity => "antigravity",
593 Self::Gemini => "gemini",
594 Self::Claude => "claude",
595 Self::Codex => "codex",
596 }
597 }
598
599 fn description(&self) -> &'static str {
600 match self {
601 Self::Antigravity => "Google Antigravity CLI agent.",
602 Self::Gemini => "Google Gemini CLI agent.",
603 Self::Claude => "Anthropic Claude Code agent.",
604 Self::Codex => "OpenAI Codex CLI agent.",
605 }
606 }
607}
608
609impl AgentSelectionMetadata for ReasoningLevel {
610 fn name(&self) -> &'static str {
611 (*self).as_str()
612 }
613
614 fn description(&self) -> &'static str {
615 (*self).description()
616 }
617}
618
619impl fmt::Display for AgentKind {
620 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
621 write!(f, "{}", self.name())
622 }
623}
624
625impl FromStr for AgentKind {
626 type Err = String;
627
628 fn from_str(s: &str) -> Result<Self, Self::Err> {
629 match s.to_lowercase().as_str() {
630 "antigravity" | "agy" => Ok(Self::Antigravity),
631 "gemini" => Ok(Self::Gemini),
632 "claude" => Ok(Self::Claude),
633 "codex" => Ok(Self::Codex),
634 other => Err(format!("unknown agent kind: {other}")),
635 }
636 }
637}
638
639#[cfg(test)]
640mod tests {
641 use super::*;
642
643 #[test]
644 fn test_parse_model_returns_none_for_models_from_other_providers() {
646 let claude_kind = AgentKind::Claude;
648 let antigravity_model = AgentModel::Gemini3FlashPreview.as_str();
649
650 let parsed_model = claude_kind.parse_model(antigravity_model);
652
653 assert_eq!(parsed_model, None);
655 }
656
657 #[test]
658 fn test_parse_model_parses_gpt_55() {
660 let codex_kind = AgentKind::Codex;
662
663 let parsed_model = codex_kind.parse_model("gpt-5.5");
665
666 assert_eq!(parsed_model, Some(AgentModel::Gpt55));
668 }
669
670 #[test]
671 fn test_parse_model_parses_antigravity() {
674 let antigravity_kind = AgentKind::Antigravity;
676
677 let parsed_model = antigravity_kind.parse_model("gemini-3.5-flash");
679
680 assert_eq!(parsed_model, Some(AgentModel::Gemini35Flash));
682 }
683
684 #[test]
685 fn test_parse_model_parses_direct_gemini() {
688 let gemini_kind = AgentKind::Gemini;
690
691 let parsed_model = gemini_kind.parse_model("gemini-3.5-flash");
693
694 assert_eq!(parsed_model, Some(AgentModel::Gemini35Flash));
696 }
697
698 #[test]
699 fn test_parse_model_rejects_retired_gpt_54() {
701 let codex_kind = AgentKind::Codex;
703
704 let parsed_gpt_54 = codex_kind.parse_model("gpt-5.4");
706
707 assert_eq!(parsed_gpt_54, None);
709 }
710
711 #[test]
712 fn test_parse_model_parses_gpt_54_mini() {
714 let codex_kind = AgentKind::Codex;
716
717 let parsed_model = codex_kind.parse_model("gpt-5.4-mini");
719
720 assert_eq!(parsed_model, Some(AgentModel::Gpt54Mini));
722 }
723
724 #[test]
725 fn test_parse_model_rejects_retired_claude_aliases() {
728 let claude_kind = AgentKind::Claude;
730
731 let parsed_opus_46 = claude_kind.parse_model("claude-opus-4-6");
733 let parsed_opus_47 = claude_kind.parse_model("claude-opus-4-7");
734 let parsed_sonnet_46 = claude_kind.parse_model("claude-sonnet-4-6");
735
736 assert_eq!(parsed_opus_46, None);
738 assert_eq!(parsed_opus_47, None);
739 assert_eq!(parsed_sonnet_46, None);
740 }
741
742 #[test]
743 fn test_parse_model_parses_current_claude_models() {
745 let claude_kind = AgentKind::Claude;
747
748 let parsed_sonnet_5 = claude_kind.parse_model("claude-sonnet-5");
750 let parsed_fable_5 = claude_kind.parse_model("claude-fable-5");
751 let parsed_haiku_45 = claude_kind.parse_model("claude-haiku-4-5-20251001");
752
753 assert_eq!(parsed_sonnet_5, Some(AgentModel::ClaudeSonnet5));
755 assert_eq!(parsed_fable_5, Some(AgentModel::ClaudeFable5));
756 assert_eq!(parsed_haiku_45, Some(AgentModel::ClaudeHaiku4520251001));
757 }
758
759 #[test]
760 fn test_parse_persisted_handles_supported_and_retired_models() {
763 let parsed_opus_46 = AgentModel::parse_persisted("claude-opus-4-6");
767 let parsed_opus_47 = AgentModel::parse_persisted("claude-opus-4-7");
768 let parsed_sonnet_46 = AgentModel::parse_persisted("claude-sonnet-4-6");
769 let parsed_sonnet_5 = AgentModel::parse_persisted("claude-sonnet-5");
770 let parsed_gpt_54 = AgentModel::parse_persisted("gpt-5.4");
771 let parsed_gemini_35_flash = AgentModel::parse_persisted("gemini-3.5-flash");
772
773 assert_eq!(parsed_opus_46, Ok(AgentModel::ClaudeOpus48));
775 assert_eq!(parsed_opus_47, Ok(AgentModel::ClaudeOpus48));
776 assert_eq!(parsed_sonnet_46, Ok(AgentModel::ClaudeSonnet5));
777 assert_eq!(parsed_sonnet_5, Ok(AgentModel::ClaudeSonnet5));
778 assert_eq!(parsed_gpt_54, Ok(AgentModel::Gpt55));
779 assert_eq!(parsed_gemini_35_flash, Ok(AgentModel::Gemini35Flash));
780 }
781
782 #[test]
783 fn test_parse_persisted_session_agent_model_prefers_saved_agent() {
786 let selection = parse_persisted_session_agent_model(Some("codex"), "gemini-3.5-flash");
790
791 assert_eq!(selection.kind(), AgentKind::Codex);
793 assert_eq!(selection.model(), AgentKind::Codex.default_model());
794 }
795
796 #[test]
797 fn test_parse_persisted_session_agent_model_preserves_antigravity_models() {
800 let selection =
804 parse_persisted_session_agent_model(Some("antigravity"), "gemini-3.5-flash");
805
806 assert_eq!(selection.kind(), AgentKind::Antigravity);
808 assert_eq!(selection.model(), AgentModel::Gemini35Flash);
809 }
810
811 #[test]
812 fn test_parse_persisted_session_agent_model_supports_legacy_rows() {
815 let selection = parse_persisted_session_agent_model(None, "claude-opus-4-6");
819
820 assert_eq!(selection.kind(), AgentKind::Claude);
822 assert_eq!(selection.model(), AgentModel::ClaudeOpus48);
823 }
824
825 #[test]
826 fn test_parse_persisted_session_agent_model_defaults_legacy_gemini_to_antigravity() {
829 let selection = parse_persisted_session_agent_model(None, "gemini-3.5-flash");
833
834 assert_eq!(selection.kind(), AgentKind::Antigravity);
836 assert_eq!(selection.model(), AgentModel::Gemini35Flash);
837 }
838
839 #[test]
840 fn test_reasoning_level_from_str_parses_supported_values() {
842 let low_level = "low".parse::<ReasoningLevel>();
846 let medium_level = "medium".parse::<ReasoningLevel>();
847 let high_level = "high".parse::<ReasoningLevel>();
848 let xhigh_level = "xhigh".parse::<ReasoningLevel>();
849
850 assert_eq!(low_level, Ok(ReasoningLevel::Low));
852 assert_eq!(medium_level, Ok(ReasoningLevel::Medium));
853 assert_eq!(high_level, Ok(ReasoningLevel::High));
854 assert_eq!(xhigh_level, Ok(ReasoningLevel::XHigh));
855 }
856
857 #[test]
858 fn test_reasoning_level_from_str_rejects_unknown_values() {
860 let parse_result = "minimal".parse::<ReasoningLevel>();
864
865 assert!(parse_result.is_err());
867 }
868
869 #[test]
870 fn test_codex_models_are_supported_by_codex() {
872 let models = [
874 AgentModel::Gpt55,
875 AgentModel::Gpt54Mini,
876 AgentModel::Gpt53CodexSpark,
877 ];
878
879 let supported = models.map(|model| AgentKind::Codex.supports_model(model));
881 let unsupported = models.map(|model| AgentKind::Claude.supports_model(model));
882
883 assert_eq!(supported, [true; 3]);
885 assert_eq!(unsupported, [false; 3]);
886 }
887
888 #[test]
889 fn test_claude_models_are_supported_by_claude() {
891 let models = [
893 AgentModel::ClaudeOpus48,
894 AgentModel::ClaudeSonnet5,
895 AgentModel::ClaudeFable5,
896 AgentModel::ClaudeHaiku4520251001,
897 ];
898
899 let supported = models.map(|model| AgentKind::Claude.supports_model(model));
901 let unsupported = models.map(|model| AgentKind::Codex.supports_model(model));
902
903 assert_eq!(supported, [true; 4]);
905 assert_eq!(unsupported, [false; 4]);
906 }
907
908 #[test]
909 fn test_gemini_models_are_supported_by_gemini_and_antigravity() {
911 let models = [
913 AgentModel::Gemini31ProPreview,
914 AgentModel::Gemini35Flash,
915 AgentModel::Gemini31FlashLitePreview,
916 AgentModel::Gemini3FlashPreview,
917 ];
918
919 let antigravity_supported =
921 models.map(|model| AgentKind::Antigravity.supports_model(model));
922 let gemini_supported = models.map(|model| AgentKind::Gemini.supports_model(model));
923 let codex_supported = models.map(|model| AgentKind::Codex.supports_model(model));
924
925 assert_eq!(antigravity_supported, [true; 4]);
927 assert_eq!(gemini_supported, [true; 4]);
928 assert_eq!(codex_supported, [false; 4]);
929 }
930
931 #[test]
932 fn test_antigravity_provider_model_str_returns_raw_gemini_model() {
935 let model = AgentModel::Gemini31FlashLitePreview;
937
938 let persisted_model = model.as_str();
940 let provider_model = model.provider_model_str();
941
942 assert_eq!(persisted_model, "gemini-3.1-flash-lite-preview");
944 assert_eq!(provider_model, "gemini-3.1-flash-lite-preview");
945 }
946
947 #[test]
948 fn test_reasoning_level_claude_maps_all_levels() {
951 assert_eq!(ReasoningLevel::Low.claude(), "low");
953 assert_eq!(ReasoningLevel::Medium.claude(), "medium");
954 assert_eq!(ReasoningLevel::High.claude(), "high");
955 assert_eq!(ReasoningLevel::XHigh.claude(), "max");
956 }
957
958 #[test]
959 fn test_reasoning_level_as_str_returns_stable_persisted_values() {
962 assert_eq!(ReasoningLevel::Low.as_str(), "low");
964 assert_eq!(ReasoningLevel::Medium.as_str(), "medium");
965 assert_eq!(ReasoningLevel::High.as_str(), "high");
966 assert_eq!(ReasoningLevel::XHigh.as_str(), "xhigh");
967 }
968
969 #[test]
970 fn test_selectable_models_for_agent_kinds_uses_provider_order() {
972 let agent_kinds = [AgentKind::Codex, AgentKind::Antigravity];
974
975 let selectable_models = selectable_models_for_agent_kinds(&agent_kinds);
977
978 assert_eq!(
980 selectable_models,
981 vec![
982 AgentModel::Gpt55,
983 AgentModel::Gpt54Mini,
984 AgentModel::Gpt53CodexSpark,
985 AgentModel::Gemini31ProPreview,
986 AgentModel::Gemini35Flash,
987 AgentModel::Gemini31FlashLitePreview,
988 AgentModel::Gemini3FlashPreview,
989 ]
990 );
991 }
992
993 #[test]
994 fn test_selectable_models_for_agent_kinds_deduplicates_shared_models() {
997 let agent_kinds = [AgentKind::Gemini, AgentKind::Antigravity];
999
1000 let selectable_models = selectable_models_for_agent_kinds(&agent_kinds);
1002
1003 assert_eq!(
1005 selectable_models,
1006 vec![
1007 AgentModel::Gemini31ProPreview,
1008 AgentModel::Gemini35Flash,
1009 AgentModel::Gemini31FlashLitePreview,
1010 AgentModel::Gemini3FlashPreview,
1011 ]
1012 );
1013 }
1014
1015 #[test]
1016 fn test_resolve_model_for_available_agent_kinds_prefers_available_fallback() {
1019 let unavailable_model = AgentModel::ClaudeOpus48;
1021 let available_agent_kinds = [AgentKind::Codex, AgentKind::Antigravity];
1022 let fallback_model = AgentModel::Gpt55;
1023
1024 let resolved_model = resolve_model_for_available_agent_kinds(
1026 unavailable_model,
1027 &available_agent_kinds,
1028 fallback_model,
1029 );
1030
1031 assert_eq!(resolved_model, AgentModel::Gpt55);
1033 }
1034
1035 #[test]
1036 fn test_resolve_model_for_available_agent_kinds_uses_first_available_default() {
1039 let unavailable_model = AgentModel::ClaudeOpus48;
1041 let available_agent_kinds = [AgentKind::Codex, AgentKind::Antigravity];
1042 let unavailable_fallback_model = AgentModel::ClaudeSonnet5;
1043
1044 let resolved_model = resolve_model_for_available_agent_kinds(
1046 unavailable_model,
1047 &available_agent_kinds,
1048 unavailable_fallback_model,
1049 );
1050
1051 assert_eq!(resolved_model, AgentKind::Codex.default_model());
1053 }
1054
1055 #[test]
1056 fn test_resolve_agent_selection_for_model_preserves_preferred_shared_provider() {
1059 let model = AgentModel::Gemini35Flash;
1061 let available_agent_kinds = [AgentKind::Gemini, AgentKind::Antigravity];
1062
1063 let resolved_selection = resolve_agent_selection_for_model(
1065 model,
1066 AgentKind::Antigravity,
1067 &available_agent_kinds,
1068 );
1069
1070 assert_eq!(
1072 resolved_selection,
1073 AgentSelection::new(AgentKind::Antigravity, model)
1074 );
1075 }
1076
1077 #[test]
1078 fn test_resolve_agent_selection_for_model_uses_available_provider_order() {
1081 let model = AgentModel::Gemini35Flash;
1083 let available_agent_kinds = [AgentKind::Gemini, AgentKind::Antigravity];
1084
1085 let resolved_selection =
1087 resolve_agent_selection_for_model(model, AgentKind::Codex, &available_agent_kinds);
1088
1089 assert_eq!(
1091 resolved_selection,
1092 AgentSelection::new(AgentKind::Gemini, model)
1093 );
1094 }
1095
1096 #[test]
1097 fn test_resolve_prompt_model_agent_kind_prefers_current_agent() {
1100 let available_agent_kinds = [AgentKind::Antigravity, AgentKind::Codex];
1102
1103 let resolved_agent_kind =
1105 resolve_prompt_model_agent_kind(AgentKind::Codex, &available_agent_kinds);
1106
1107 assert_eq!(resolved_agent_kind, Some(AgentKind::Codex));
1109 }
1110
1111 #[test]
1112 fn test_resolve_prompt_model_agent_kind_uses_first_available_agent() {
1115 let available_agent_kinds = [AgentKind::Antigravity, AgentKind::Codex];
1117
1118 let resolved_agent_kind =
1120 resolve_prompt_model_agent_kind(AgentKind::Claude, &available_agent_kinds);
1121
1122 assert_eq!(resolved_agent_kind, Some(AgentKind::Antigravity));
1124 }
1125}