use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ModelError {
InvalidPrefix {
model: String,
expected_prefixes: &'static [&'static str],
},
UnknownShorthand(String),
}
impl fmt::Display for ModelError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidPrefix {
model,
expected_prefixes,
} => {
write!(
f,
"Invalid model name '{}'. Expected prefix: {}",
model,
expected_prefixes.join(" or ")
)
}
Self::UnknownShorthand(s) => write!(f, "Unknown model shorthand: {}", s),
}
}
}
impl std::error::Error for ModelError {}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum ClaudeModel {
Opus46,
Sonnet46,
Haiku45,
Opus45,
Sonnet45,
Opus41,
Opus4,
Sonnet4,
Custom(String),
}
impl Default for ClaudeModel {
fn default() -> Self {
Self::Sonnet46
}
}
impl ClaudeModel {
pub fn as_api_id(&self) -> &str {
match self {
Self::Opus46 => "claude-opus-4-6",
Self::Sonnet46 => "claude-sonnet-4-6",
Self::Haiku45 => "claude-haiku-4-5-20251001",
Self::Opus45 => "claude-opus-4-5-20251101",
Self::Sonnet45 => "claude-sonnet-4-5-20250929",
Self::Opus41 => "claude-opus-4-1-20250805",
Self::Opus4 => "claude-opus-4-20250514",
Self::Sonnet4 => "claude-sonnet-4-20250514",
Self::Custom(s) => s,
}
}
pub fn as_cli_name(&self) -> &str {
match self {
Self::Opus46 => "claude-opus-4.6",
Self::Sonnet46 => "claude-sonnet-4.6",
Self::Haiku45 => "claude-haiku-4.5",
Self::Opus45 => "claude-opus-4.5",
Self::Sonnet45 => "claude-sonnet-4.5",
Self::Opus41 => "claude-opus-4.1",
Self::Opus4 => "claude-opus-4",
Self::Sonnet4 => "claude-sonnet-4",
Self::Custom(s) => s,
}
}
fn validate_custom(s: &str) -> Result<(), ModelError> {
if s.starts_with("claude-") {
Ok(())
} else {
Err(ModelError::InvalidPrefix {
model: s.to_string(),
expected_prefixes: &["claude-"],
})
}
}
}
impl std::str::FromStr for ClaudeModel {
type Err = ModelError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"opus" | "opus-4.6" | "opus46" | "claude-opus-4.6" | "claude-opus-4-6" => {
Ok(Self::Opus46)
}
"sonnet" | "sonnet-4.6" | "sonnet46" | "claude-sonnet-4.6" | "claude-sonnet-4-6" => {
Ok(Self::Sonnet46)
}
"haiku"
| "haiku-4.5"
| "haiku45"
| "claude-haiku-4.5"
| "claude-haiku-4-5-20251001" => Ok(Self::Haiku45),
"opus-4.5" | "opus45" | "claude-opus-4.5" | "claude-opus-4-5-20251101" => {
Ok(Self::Opus45)
}
"sonnet-4.5" | "sonnet45" | "claude-sonnet-4.5" | "claude-sonnet-4-5-20250929" => {
Ok(Self::Sonnet45)
}
"opus-4.1" | "opus41" | "claude-opus-4.1" | "claude-opus-4-1-20250805" => {
Ok(Self::Opus41)
}
"opus-4" | "opus4" | "claude-opus-4" | "claude-opus-4-20250514" => Ok(Self::Opus4),
"sonnet-4" | "sonnet4" | "claude-sonnet-4" | "claude-sonnet-4-20250514" => {
Ok(Self::Sonnet4)
}
_ => {
Self::validate_custom(s)?;
Ok(Self::Custom(s.to_string()))
}
}
}
}
impl fmt::Display for ClaudeModel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_api_id())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum GeminiModel {
Pro31,
Flash3,
Pro3,
Flash25,
Pro25,
FlashLite25,
Flash20,
Custom(String),
}
impl Default for GeminiModel {
fn default() -> Self {
Self::Flash25
}
}
impl GeminiModel {
pub fn as_api_id(&self) -> &str {
match self {
Self::Pro31 => "gemini-3.1-pro-preview",
Self::Flash3 => "gemini-3-flash-preview",
Self::Pro3 => "gemini-3-pro-preview",
Self::Flash25 => "gemini-2.5-flash",
Self::Pro25 => "gemini-2.5-pro",
Self::FlashLite25 => "gemini-2.5-flash-lite",
Self::Flash20 => "gemini-2.0-flash",
Self::Custom(s) => s,
}
}
pub fn as_cli_name(&self) -> &str {
match self {
Self::Pro31 => "pro-3.1",
Self::Flash3 => "flash-3",
Self::Pro3 => "pro-3",
Self::Flash25 => "flash",
Self::Pro25 => "pro",
Self::FlashLite25 => "flash-lite",
Self::Flash20 => "flash-2.0",
Self::Custom(s) => s,
}
}
fn validate_custom(s: &str) -> Result<(), ModelError> {
if s.starts_with("gemini-") {
Ok(())
} else {
Err(ModelError::InvalidPrefix {
model: s.to_string(),
expected_prefixes: &["gemini-"],
})
}
}
}
impl std::str::FromStr for GeminiModel {
type Err = ModelError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"pro-3.1" | "pro31" | "gemini-3.1-pro-preview" => Ok(Self::Pro31),
"flash-3" | "flash3" | "gemini-3-flash-preview" | "gemini-3-flash" => Ok(Self::Flash3),
"pro-3" | "pro3" | "gemini-3-pro-preview" | "gemini-3-pro" => Ok(Self::Pro3),
"flash" | "flash-2.5" | "flash25" | "gemini-2.5-flash" => Ok(Self::Flash25),
"pro" | "pro-2.5" | "pro25" | "gemini-2.5-pro" => Ok(Self::Pro25),
"flash-lite" | "lite" | "gemini-2.5-flash-lite" => Ok(Self::FlashLite25),
"flash-2.0" | "flash20" | "flash-2" | "gemini-2.0-flash" => Ok(Self::Flash20),
_ => {
Self::validate_custom(s)?;
Ok(Self::Custom(s.to_string()))
}
}
}
}
impl fmt::Display for GeminiModel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_api_id())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum OpenAIModel {
Gpt52,
Gpt52Pro,
Gpt51,
Gpt5,
Gpt5Mini,
Gpt52Codex,
Gpt51Codex,
Gpt51CodexMini,
Gpt5Codex,
Gpt5CodexMini,
Gpt41,
Gpt41Mini,
Gpt4o,
Gpt4oMini,
O3Pro,
O3,
O3Mini,
O1,
O1Pro,
Custom(String),
}
impl Default for OpenAIModel {
fn default() -> Self {
Self::Gpt5
}
}
impl OpenAIModel {
pub fn as_api_id(&self) -> &str {
match self {
Self::Gpt52 => "gpt-5.2",
Self::Gpt52Pro => "gpt-5.2-pro",
Self::Gpt51 => "gpt-5.1",
Self::Gpt5 => "gpt-5",
Self::Gpt5Mini => "gpt-5-mini",
Self::Gpt52Codex => "gpt-5.2-codex",
Self::Gpt51Codex => "gpt-5.1-codex",
Self::Gpt51CodexMini => "gpt-5.1-codex-mini",
Self::Gpt5Codex => "gpt-5-codex",
Self::Gpt5CodexMini => "gpt-5-codex-mini",
Self::Gpt41 => "gpt-4.1",
Self::Gpt41Mini => "gpt-4.1-mini",
Self::Gpt4o => "gpt-4o",
Self::Gpt4oMini => "gpt-4o-mini",
Self::O3Pro => "o3-pro",
Self::O3 => "o3",
Self::O3Mini => "o3-mini",
Self::O1 => "o1",
Self::O1Pro => "o1-pro",
Self::Custom(s) => s,
}
}
pub fn as_cli_name(&self) -> &str {
self.as_api_id() }
fn validate_custom(s: &str) -> Result<(), ModelError> {
const VALID_PREFIXES: &[&str] = &["gpt-", "o1-", "o3-"];
if VALID_PREFIXES.iter().any(|p| s.starts_with(p)) {
Ok(())
} else {
Err(ModelError::InvalidPrefix {
model: s.to_string(),
expected_prefixes: VALID_PREFIXES,
})
}
}
}
impl std::str::FromStr for OpenAIModel {
type Err = ModelError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"5.2" | "gpt-5.2" | "gpt52" => Ok(Self::Gpt52),
"5.2-pro" | "gpt-5.2-pro" => Ok(Self::Gpt52Pro),
"5.1" | "gpt-5.1" | "gpt51" => Ok(Self::Gpt51),
"5" | "gpt-5" | "gpt5" => Ok(Self::Gpt5),
"5-mini" | "gpt-5-mini" => Ok(Self::Gpt5Mini),
"5.2-codex" | "gpt-5.2-codex" | "codex" => Ok(Self::Gpt52Codex),
"5.1-codex" | "gpt-5.1-codex" => Ok(Self::Gpt51Codex),
"5.1-codex-mini" | "gpt-5.1-codex-mini" | "codex-mini" => Ok(Self::Gpt51CodexMini),
"5-codex" | "gpt-5-codex" => Ok(Self::Gpt5Codex),
"5-codex-mini" | "gpt-5-codex-mini" => Ok(Self::Gpt5CodexMini),
"4.1" | "gpt-4.1" | "gpt41" => Ok(Self::Gpt41),
"4.1-mini" | "gpt-4.1-mini" => Ok(Self::Gpt41Mini),
"4o" | "gpt-4o" => Ok(Self::Gpt4o),
"4o-mini" | "gpt-4o-mini" => Ok(Self::Gpt4oMini),
"o3-pro" => Ok(Self::O3Pro),
"o3" => Ok(Self::O3),
"o3-mini" => Ok(Self::O3Mini),
"o1" => Ok(Self::O1),
"o1-pro" => Ok(Self::O1Pro),
_ => {
Self::validate_custom(s)?;
Ok(Self::Custom(s.to_string()))
}
}
}
}
impl fmt::Display for OpenAIModel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_api_id())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Model {
Claude(ClaudeModel),
Gemini(GeminiModel),
OpenAI(OpenAIModel),
}
impl Model {
pub fn as_api_id(&self) -> &str {
match self {
Self::Claude(m) => m.as_api_id(),
Self::Gemini(m) => m.as_api_id(),
Self::OpenAI(m) => m.as_api_id(),
}
}
pub fn as_cli_name(&self) -> &str {
match self {
Self::Claude(m) => m.as_cli_name(),
Self::Gemini(m) => m.as_cli_name(),
Self::OpenAI(m) => m.as_cli_name(),
}
}
}
impl From<ClaudeModel> for Model {
fn from(m: ClaudeModel) -> Self {
Self::Claude(m)
}
}
impl From<GeminiModel> for Model {
fn from(m: GeminiModel) -> Self {
Self::Gemini(m)
}
}
impl From<OpenAIModel> for Model {
fn from(m: OpenAIModel) -> Self {
Self::OpenAI(m)
}
}
impl fmt::Display for Model {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_api_id())
}
}
#[cfg(test)]
mod tests {
use super::*;
mod claude_model {
use super::*;
#[test]
fn test_default() {
assert_eq!(ClaudeModel::default(), ClaudeModel::Sonnet46);
}
#[test]
fn test_api_id() {
assert_eq!(ClaudeModel::Opus46.as_api_id(), "claude-opus-4-6");
assert_eq!(ClaudeModel::Sonnet46.as_api_id(), "claude-sonnet-4-6");
assert_eq!(
ClaudeModel::Haiku45.as_api_id(),
"claude-haiku-4-5-20251001"
);
assert_eq!(ClaudeModel::Opus45.as_api_id(), "claude-opus-4-5-20251101");
assert_eq!(
ClaudeModel::Sonnet45.as_api_id(),
"claude-sonnet-4-5-20250929"
);
}
#[test]
fn test_cli_name() {
assert_eq!(ClaudeModel::Opus46.as_cli_name(), "claude-opus-4.6");
assert_eq!(ClaudeModel::Sonnet46.as_cli_name(), "claude-sonnet-4.6");
assert_eq!(ClaudeModel::Haiku45.as_cli_name(), "claude-haiku-4.5");
assert_eq!(ClaudeModel::Opus45.as_cli_name(), "claude-opus-4.5");
assert_eq!(ClaudeModel::Sonnet4.as_cli_name(), "claude-sonnet-4");
}
#[test]
fn test_parse_shorthand() {
assert_eq!("opus".parse::<ClaudeModel>().unwrap(), ClaudeModel::Opus46);
assert_eq!(
"sonnet".parse::<ClaudeModel>().unwrap(),
ClaudeModel::Sonnet46
);
assert_eq!(
"haiku".parse::<ClaudeModel>().unwrap(),
ClaudeModel::Haiku45
);
}
#[test]
fn test_parse_versioned_shorthand() {
assert_eq!(
"opus-4.6".parse::<ClaudeModel>().unwrap(),
ClaudeModel::Opus46
);
assert_eq!(
"opus-4.5".parse::<ClaudeModel>().unwrap(),
ClaudeModel::Opus45
);
assert_eq!(
"sonnet-4.6".parse::<ClaudeModel>().unwrap(),
ClaudeModel::Sonnet46
);
assert_eq!(
"sonnet-4.5".parse::<ClaudeModel>().unwrap(),
ClaudeModel::Sonnet45
);
assert_eq!(
"haiku-4.5".parse::<ClaudeModel>().unwrap(),
ClaudeModel::Haiku45
);
}
#[test]
fn test_parse_full_api_id() {
assert_eq!(
"claude-opus-4-6".parse::<ClaudeModel>().unwrap(),
ClaudeModel::Opus46
);
assert_eq!(
"claude-opus-4-5-20251101".parse::<ClaudeModel>().unwrap(),
ClaudeModel::Opus45
);
assert_eq!(
"claude-sonnet-4".parse::<ClaudeModel>().unwrap(),
ClaudeModel::Sonnet4
);
}
#[test]
fn test_parse_custom_valid() {
let model: ClaudeModel = "claude-future-model-2027".parse().unwrap();
assert_eq!(
model,
ClaudeModel::Custom("claude-future-model-2027".to_string())
);
}
#[test]
fn test_parse_custom_invalid() {
let result: Result<ClaudeModel, _> = "gpt-4o".parse();
assert!(result.is_err());
}
}
mod gemini_model {
use super::*;
#[test]
fn test_default() {
assert_eq!(GeminiModel::default(), GeminiModel::Flash25);
}
#[test]
fn test_api_id() {
assert_eq!(GeminiModel::Pro31.as_api_id(), "gemini-3.1-pro-preview");
assert_eq!(GeminiModel::Flash3.as_api_id(), "gemini-3-flash-preview");
assert_eq!(GeminiModel::Pro3.as_api_id(), "gemini-3-pro-preview");
assert_eq!(
GeminiModel::FlashLite25.as_api_id(),
"gemini-2.5-flash-lite"
);
}
#[test]
fn test_parse() {
assert_eq!(
"flash".parse::<GeminiModel>().unwrap(),
GeminiModel::Flash25
);
assert_eq!("pro".parse::<GeminiModel>().unwrap(), GeminiModel::Pro25);
assert_eq!(
"flash-3".parse::<GeminiModel>().unwrap(),
GeminiModel::Flash3
);
assert_eq!(
"pro-3.1".parse::<GeminiModel>().unwrap(),
GeminiModel::Pro31
);
assert_eq!(
"flash-lite".parse::<GeminiModel>().unwrap(),
GeminiModel::FlashLite25
);
}
#[test]
fn test_parse_legacy_api_id() {
assert_eq!(
"gemini-3-flash".parse::<GeminiModel>().unwrap(),
GeminiModel::Flash3
);
assert_eq!(
"gemini-3-pro".parse::<GeminiModel>().unwrap(),
GeminiModel::Pro3
);
}
#[test]
fn test_custom_invalid() {
let result: Result<GeminiModel, _> = "claude-opus".parse();
assert!(result.is_err());
}
}
mod openai_model {
use super::*;
#[test]
fn test_default() {
assert_eq!(OpenAIModel::default(), OpenAIModel::Gpt5);
}
#[test]
fn test_api_id() {
assert_eq!(OpenAIModel::Gpt52Pro.as_api_id(), "gpt-5.2-pro");
assert_eq!(OpenAIModel::Gpt52Codex.as_api_id(), "gpt-5.2-codex");
assert_eq!(OpenAIModel::Gpt5.as_api_id(), "gpt-5");
}
#[test]
fn test_parse() {
assert_eq!("5".parse::<OpenAIModel>().unwrap(), OpenAIModel::Gpt5);
assert_eq!(
"gpt-5.2".parse::<OpenAIModel>().unwrap(),
OpenAIModel::Gpt52
);
assert_eq!(
"5.2-pro".parse::<OpenAIModel>().unwrap(),
OpenAIModel::Gpt52Pro
);
assert_eq!("o3".parse::<OpenAIModel>().unwrap(), OpenAIModel::O3);
assert_eq!(
"codex".parse::<OpenAIModel>().unwrap(),
OpenAIModel::Gpt52Codex
);
}
#[test]
fn test_parse_legacy() {
assert_eq!("4o".parse::<OpenAIModel>().unwrap(), OpenAIModel::Gpt4o);
assert_eq!(
"gpt-4.1".parse::<OpenAIModel>().unwrap(),
OpenAIModel::Gpt41
);
}
#[test]
fn test_custom_valid() {
let model: OpenAIModel = "o3-deep-research".parse().unwrap();
assert_eq!(model, OpenAIModel::Custom("o3-deep-research".to_string()));
}
#[test]
fn test_custom_invalid() {
let result: Result<OpenAIModel, _> = "gemini-pro".parse();
assert!(result.is_err());
}
}
}