use std::fmt;
use std::str::FromStr;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AgentKind {
Gemini,
Claude,
Codex,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AgentModel {
Gemini3FlashPreview,
Gemini31ProPreview,
Gpt54,
Gpt53CodexSpark,
ClaudeOpus47,
ClaudeSonnet46,
ClaudeHaiku4520251001,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ReasoningLevel {
Low,
Medium,
#[default]
High,
XHigh,
}
pub trait AgentSelectionMetadata {
fn name(&self) -> &'static str;
fn description(&self) -> &'static str;
}
impl AgentModel {
pub fn as_str(self) -> &'static str {
match self {
Self::Gemini3FlashPreview => "gemini-3-flash-preview",
Self::Gemini31ProPreview => "gemini-3.1-pro-preview",
Self::Gpt54 => "gpt-5.4",
Self::Gpt53CodexSpark => "gpt-5.3-codex-spark",
Self::ClaudeOpus47 => "claude-opus-4-7",
Self::ClaudeSonnet46 => "claude-sonnet-4-6",
Self::ClaudeHaiku4520251001 => "claude-haiku-4-5-20251001",
}
}
pub(crate) fn parse_persisted(value: &str) -> Result<Self, String> {
match value {
"claude-opus-4-6" => Ok(Self::ClaudeOpus47),
_ => value.parse(),
}
}
pub fn kind(self) -> AgentKind {
match self {
Self::Gemini3FlashPreview | Self::Gemini31ProPreview => AgentKind::Gemini,
Self::Gpt54 | Self::Gpt53CodexSpark => AgentKind::Codex,
Self::ClaudeOpus47 | Self::ClaudeSonnet46 | Self::ClaudeHaiku4520251001 => {
AgentKind::Claude
}
}
}
}
#[must_use]
pub fn selectable_models_for_agent_kinds(agent_kinds: &[AgentKind]) -> Vec<AgentModel> {
agent_kinds
.iter()
.flat_map(|agent_kind| agent_kind.models())
.copied()
.collect()
}
#[must_use]
pub fn resolve_model_for_available_agent_kinds(
model: AgentModel,
agent_kinds: &[AgentKind],
fallback_model: AgentModel,
) -> AgentModel {
if agent_kinds.contains(&model.kind()) {
return model;
}
if agent_kinds.contains(&fallback_model.kind()) {
return fallback_model;
}
agent_kinds
.first()
.copied()
.map_or(fallback_model, AgentKind::default_model)
}
#[must_use]
pub fn resolve_prompt_model_agent_kind(
session_agent_kind: AgentKind,
agent_kinds: &[AgentKind],
) -> Option<AgentKind> {
if agent_kinds.contains(&session_agent_kind) {
return Some(session_agent_kind);
}
agent_kinds.first().copied()
}
impl ReasoningLevel {
pub const ALL: [Self; 4] = [Self::Low, Self::Medium, Self::High, Self::XHigh];
pub fn as_str(self) -> &'static str {
match self {
Self::Low => "low",
Self::Medium => "medium",
Self::High => "high",
Self::XHigh => "xhigh",
}
}
pub fn codex(self) -> &'static str {
match self {
Self::Low => "low",
Self::Medium => "medium",
Self::High => "high",
Self::XHigh => "xhigh",
}
}
pub fn claude(self) -> &'static str {
match self {
Self::Low => "low",
Self::Medium => "medium",
Self::High => "high",
Self::XHigh => "max",
}
}
pub fn description(self) -> &'static str {
match self {
Self::Low => "Fastest responses with lighter reasoning.",
Self::Medium => "Balanced speed and reasoning depth.",
Self::High => "Deeper reasoning for tougher tasks.",
Self::XHigh => "Maximum reasoning effort for the hardest tasks.",
}
}
}
impl FromStr for ReasoningLevel {
type Err = String;
fn from_str(value: &str) -> Result<Self, Self::Err> {
match value {
"low" => Ok(Self::Low),
"medium" => Ok(Self::Medium),
"high" => Ok(Self::High),
"xhigh" => Ok(Self::XHigh),
other => Err(format!("unknown reasoning level: {other}")),
}
}
}
impl FromStr for AgentModel {
type Err = String;
fn from_str(value: &str) -> Result<Self, Self::Err> {
match value {
"gemini-3-flash-preview" => Ok(Self::Gemini3FlashPreview),
"gemini-3.1-pro-preview" => Ok(Self::Gemini31ProPreview),
"gpt-5.4" => Ok(Self::Gpt54),
"gpt-5.3-codex-spark" => Ok(Self::Gpt53CodexSpark),
"claude-opus-4-7" => Ok(Self::ClaudeOpus47),
"claude-sonnet-4-6" => Ok(Self::ClaudeSonnet46),
"claude-haiku-4-5-20251001" => Ok(Self::ClaudeHaiku4520251001),
other => Err(format!("unknown model: {other}")),
}
}
}
impl AgentSelectionMetadata for AgentModel {
fn name(&self) -> &'static str {
(*self).as_str()
}
fn description(&self) -> &'static str {
match self {
Self::Gemini3FlashPreview => "Fast Gemini model for quick iterations.",
Self::Gemini31ProPreview => "Higher-quality Gemini model for deeper reasoning.",
Self::Gpt54 => "Latest Codex model for coding quality.",
Self::Gpt53CodexSpark => "Codex spark model for quick coding iterations.",
Self::ClaudeOpus47 => "Latest Claude Opus model for complex tasks.",
Self::ClaudeSonnet46 => "Balanced Claude model for quality and latency.",
Self::ClaudeHaiku4520251001 => "Fast Claude model for lighter tasks.",
}
}
}
impl AgentKind {
pub const ALL: &[AgentKind] = &[AgentKind::Gemini, AgentKind::Claude, AgentKind::Codex];
pub fn default_model(self) -> AgentModel {
match self {
Self::Gemini => AgentModel::Gemini31ProPreview,
Self::Claude => AgentModel::ClaudeOpus47,
Self::Codex => AgentModel::Gpt54,
}
}
pub fn model_str(self, model: AgentModel) -> Option<&'static str> {
if model.kind() != self {
return None;
}
Some(model.as_str())
}
pub fn models(self) -> &'static [AgentModel] {
const GEMINI_MODELS: &[AgentModel] = &[
AgentModel::Gemini31ProPreview,
AgentModel::Gemini3FlashPreview,
];
const CLAUDE_MODELS: &[AgentModel] = &[
AgentModel::ClaudeOpus47,
AgentModel::ClaudeSonnet46,
AgentModel::ClaudeHaiku4520251001,
];
const CODEX_MODELS: &[AgentModel] = &[AgentModel::Gpt54, AgentModel::Gpt53CodexSpark];
match self {
Self::Gemini => GEMINI_MODELS,
Self::Claude => CLAUDE_MODELS,
Self::Codex => CODEX_MODELS,
}
}
pub fn parse_model(self, value: &str) -> Option<AgentModel> {
let model = value.parse::<AgentModel>().ok()?;
if model.kind() != self {
return None;
}
Some(model)
}
}
impl AgentSelectionMetadata for AgentKind {
fn name(&self) -> &'static str {
match self {
Self::Gemini => "gemini",
Self::Claude => "claude",
Self::Codex => "codex",
}
}
fn description(&self) -> &'static str {
match self {
Self::Gemini => "Google Gemini CLI agent.",
Self::Claude => "Anthropic Claude Code agent.",
Self::Codex => "OpenAI Codex CLI agent.",
}
}
}
impl AgentSelectionMetadata for ReasoningLevel {
fn name(&self) -> &'static str {
(*self).as_str()
}
fn description(&self) -> &'static str {
(*self).description()
}
}
impl fmt::Display for AgentKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.name())
}
}
impl FromStr for AgentKind {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"gemini" => Ok(Self::Gemini),
"claude" => Ok(Self::Claude),
"codex" => Ok(Self::Codex),
other => Err(format!("unknown agent kind: {other}")),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_model_returns_none_for_models_from_other_providers() {
let claude_kind = AgentKind::Claude;
let gemini_model = AgentModel::Gemini3FlashPreview.as_str();
let parsed_model = claude_kind.parse_model(gemini_model);
assert_eq!(parsed_model, None);
}
#[test]
fn test_parse_model_parses_gpt_54() {
let codex_kind = AgentKind::Codex;
let parsed_model = codex_kind.parse_model("gpt-5.4");
assert_eq!(parsed_model, Some(AgentModel::Gpt54));
}
#[test]
fn test_parse_model_rejects_retired_claude_opus_46() {
let claude_kind = AgentKind::Claude;
let parsed_model = claude_kind.parse_model("claude-opus-4-6");
assert_eq!(parsed_model, None);
}
#[test]
fn test_parse_persisted_maps_retired_claude_opus_46_to_claude_opus_47() {
let parsed_model = AgentModel::parse_persisted("claude-opus-4-6");
assert_eq!(parsed_model, Ok(AgentModel::ClaudeOpus47));
}
#[test]
fn test_reasoning_level_from_str_parses_supported_values() {
let low_level = "low".parse::<ReasoningLevel>();
let medium_level = "medium".parse::<ReasoningLevel>();
let high_level = "high".parse::<ReasoningLevel>();
let xhigh_level = "xhigh".parse::<ReasoningLevel>();
assert_eq!(low_level, Ok(ReasoningLevel::Low));
assert_eq!(medium_level, Ok(ReasoningLevel::Medium));
assert_eq!(high_level, Ok(ReasoningLevel::High));
assert_eq!(xhigh_level, Ok(ReasoningLevel::XHigh));
}
#[test]
fn test_reasoning_level_from_str_rejects_unknown_values() {
let parse_result = "minimal".parse::<ReasoningLevel>();
assert!(parse_result.is_err());
}
#[test]
fn test_codex_model_kind_is_codex() {
let model = AgentModel::Gpt54;
let kind = model.kind();
assert_eq!(kind, AgentKind::Codex);
}
#[test]
fn test_claude_model_kind_is_claude() {
let model = AgentModel::ClaudeSonnet46;
let kind = model.kind();
assert_eq!(kind, AgentKind::Claude);
}
#[test]
fn test_reasoning_level_claude_maps_all_levels() {
assert_eq!(ReasoningLevel::Low.claude(), "low");
assert_eq!(ReasoningLevel::Medium.claude(), "medium");
assert_eq!(ReasoningLevel::High.claude(), "high");
assert_eq!(ReasoningLevel::XHigh.claude(), "max");
}
#[test]
fn test_reasoning_level_as_str_returns_stable_persisted_values() {
assert_eq!(ReasoningLevel::Low.as_str(), "low");
assert_eq!(ReasoningLevel::Medium.as_str(), "medium");
assert_eq!(ReasoningLevel::High.as_str(), "high");
assert_eq!(ReasoningLevel::XHigh.as_str(), "xhigh");
}
#[test]
fn test_selectable_models_for_agent_kinds_uses_provider_order() {
let agent_kinds = [AgentKind::Codex, AgentKind::Gemini];
let selectable_models = selectable_models_for_agent_kinds(&agent_kinds);
assert_eq!(
selectable_models,
vec![
AgentModel::Gpt54,
AgentModel::Gpt53CodexSpark,
AgentModel::Gemini31ProPreview,
AgentModel::Gemini3FlashPreview,
]
);
}
#[test]
fn test_resolve_model_for_available_agent_kinds_prefers_available_fallback() {
let unavailable_model = AgentModel::ClaudeOpus47;
let available_agent_kinds = [AgentKind::Codex, AgentKind::Gemini];
let fallback_model = AgentModel::Gpt54;
let resolved_model = resolve_model_for_available_agent_kinds(
unavailable_model,
&available_agent_kinds,
fallback_model,
);
assert_eq!(resolved_model, AgentModel::Gpt54);
}
#[test]
fn test_resolve_model_for_available_agent_kinds_uses_first_available_default() {
let unavailable_model = AgentModel::ClaudeOpus47;
let available_agent_kinds = [AgentKind::Codex, AgentKind::Gemini];
let unavailable_fallback_model = AgentModel::ClaudeSonnet46;
let resolved_model = resolve_model_for_available_agent_kinds(
unavailable_model,
&available_agent_kinds,
unavailable_fallback_model,
);
assert_eq!(resolved_model, AgentKind::Codex.default_model());
}
#[test]
fn test_resolve_prompt_model_agent_kind_prefers_current_agent() {
let available_agent_kinds = [AgentKind::Gemini, AgentKind::Codex];
let resolved_agent_kind =
resolve_prompt_model_agent_kind(AgentKind::Codex, &available_agent_kinds);
assert_eq!(resolved_agent_kind, Some(AgentKind::Codex));
}
#[test]
fn test_resolve_prompt_model_agent_kind_uses_first_available_agent() {
let available_agent_kinds = [AgentKind::Gemini, AgentKind::Codex];
let resolved_agent_kind =
resolve_prompt_model_agent_kind(AgentKind::Claude, &available_agent_kinds);
assert_eq!(resolved_agent_kind, Some(AgentKind::Gemini));
}
}