use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum ClaudeModel {
Sonnet,
Opus,
Haiku,
Fable,
Best,
OpusPlan,
Sonnet1m,
Opus1m,
Fable1m,
Fable5,
Mythos5,
Opus5,
Opus48,
Opus47,
Opus46,
Opus45,
Opus41,
Opus40,
Sonnet5,
Sonnet46,
Sonnet45,
Sonnet40,
Sonnet37,
Sonnet35,
Haiku45,
Haiku35,
Custom(String),
}
impl ClaudeModel {
pub fn cli_arg(&self) -> &str {
match self {
Self::Sonnet => "sonnet",
Self::Opus => "opus",
Self::Haiku => "haiku",
Self::Fable => "fable",
Self::Best => "best",
Self::OpusPlan => "opusplan",
Self::Sonnet1m => "sonnet[1m]",
Self::Opus1m => "opus[1m]",
Self::Fable1m => "fable[1m]",
Self::Fable5 => "claude-fable-5",
Self::Mythos5 => "claude-mythos-5",
Self::Opus5 => "claude-opus-5",
Self::Opus48 => "claude-opus-4-8",
Self::Opus47 => "claude-opus-4-7",
Self::Opus46 => "claude-opus-4-6",
Self::Opus45 => "claude-opus-4-5",
Self::Opus41 => "claude-opus-4-1",
Self::Opus40 => "claude-opus-4-0",
Self::Sonnet5 => "claude-sonnet-5",
Self::Sonnet46 => "claude-sonnet-4-6",
Self::Sonnet45 => "claude-sonnet-4-5",
Self::Sonnet40 => "claude-sonnet-4-0",
Self::Sonnet37 => "claude-3-7-sonnet",
Self::Sonnet35 => "claude-3-5-sonnet",
Self::Haiku45 => "claude-haiku-4-5",
Self::Haiku35 => "claude-3-5-haiku",
Self::Custom(s) => s.as_str(),
}
}
pub fn as_str(&self) -> &str {
self.cli_arg()
}
pub fn display_name(&self) -> &str {
match self {
Self::Sonnet => "Sonnet (latest)",
Self::Opus => "Opus (latest)",
Self::Haiku => "Haiku (latest)",
Self::Fable => "Fable (latest)",
Self::Best => "Best available",
Self::OpusPlan => "Opus (plan mode) / Sonnet",
Self::Sonnet1m => "Sonnet (latest, 1M context)",
Self::Opus1m => "Opus (latest, 1M context)",
Self::Fable1m => "Fable (latest, 1M context)",
Self::Fable5 => "Fable 5",
Self::Mythos5 => "Mythos 5",
Self::Opus5 => "Opus 5",
Self::Opus48 => "Opus 4.8",
Self::Opus47 => "Opus 4.7",
Self::Opus46 => "Opus 4.6",
Self::Opus45 => "Opus 4.5",
Self::Opus41 => "Opus 4.1",
Self::Opus40 => "Opus 4",
Self::Sonnet5 => "Sonnet 5",
Self::Sonnet46 => "Sonnet 4.6",
Self::Sonnet45 => "Sonnet 4.5",
Self::Sonnet40 => "Sonnet 4",
Self::Sonnet37 => "Sonnet 3.7",
Self::Sonnet35 => "Sonnet 3.5",
Self::Haiku45 => "Haiku 4.5",
Self::Haiku35 => "Haiku 3.5",
Self::Custom(s) => s.as_str(),
}
}
pub fn is_alias(&self) -> bool {
matches!(
self,
Self::Sonnet
| Self::Opus
| Self::Haiku
| Self::Fable
| Self::Best
| Self::OpusPlan
| Self::Sonnet1m
| Self::Opus1m
| Self::Fable1m
)
}
pub fn known() -> &'static [ClaudeModel] {
&[
Self::Sonnet,
Self::Opus,
Self::Haiku,
Self::Fable,
Self::Best,
Self::OpusPlan,
Self::Sonnet1m,
Self::Opus1m,
Self::Fable1m,
Self::Fable5,
Self::Mythos5,
Self::Opus5,
Self::Opus48,
Self::Opus47,
Self::Opus46,
Self::Opus45,
Self::Opus41,
Self::Opus40,
Self::Sonnet5,
Self::Sonnet46,
Self::Sonnet45,
Self::Sonnet40,
Self::Sonnet37,
Self::Sonnet35,
Self::Haiku45,
Self::Haiku35,
]
}
}
impl fmt::Display for ClaudeModel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.cli_arg())
}
}
impl From<&str> for ClaudeModel {
fn from(s: &str) -> Self {
match s {
"sonnet" => Self::Sonnet,
"opus" => Self::Opus,
"haiku" => Self::Haiku,
"fable" => Self::Fable,
"best" => Self::Best,
"opusplan" => Self::OpusPlan,
"sonnet[1m]" => Self::Sonnet1m,
"opus[1m]" => Self::Opus1m,
"fable[1m]" => Self::Fable1m,
"claude-fable-5" => Self::Fable5,
"claude-mythos-5" => Self::Mythos5,
"claude-opus-5" => Self::Opus5,
"claude-opus-4-8" => Self::Opus48,
"claude-opus-4-7" => Self::Opus47,
"claude-opus-4-6" => Self::Opus46,
"claude-opus-4-5" => Self::Opus45,
"claude-opus-4-1" => Self::Opus41,
"claude-opus-4-0" => Self::Opus40,
"claude-sonnet-5" => Self::Sonnet5,
"claude-sonnet-4-6" => Self::Sonnet46,
"claude-sonnet-4-5" => Self::Sonnet45,
"claude-sonnet-4-0" => Self::Sonnet40,
"claude-3-7-sonnet" => Self::Sonnet37,
"claude-3-5-sonnet" => Self::Sonnet35,
"claude-haiku-4-5" => Self::Haiku45,
"claude-3-5-haiku" => Self::Haiku35,
other => Self::Custom(other.to_string()),
}
}
}
impl From<ClaudeModel> for String {
fn from(model: ClaudeModel) -> Self {
model.cli_arg().to_string()
}
}
impl Serialize for ClaudeModel {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(self.cli_arg())
}
}
impl<'de> Deserialize<'de> for ClaudeModel {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let s = String::deserialize(deserializer)?;
Ok(Self::from(s.as_str()))
}
}
#[cfg(test)]
mod tests {
use super::ClaudeModel;
#[test]
fn test_cli_arg_round_trip() {
for model in ClaudeModel::known() {
assert_eq!(&ClaudeModel::from(model.cli_arg()), model);
}
assert_eq!(
ClaudeModel::from("claude-nova-7"),
ClaudeModel::Custom("claude-nova-7".to_string())
);
assert_eq!(
ClaudeModel::from("claude-nova-7").cli_arg(),
"claude-nova-7"
);
}
#[test]
fn test_into_string_matches_cli_arg() {
let s: String = ClaudeModel::Sonnet5.into();
assert_eq!(s, "claude-sonnet-5");
let s: String = ClaudeModel::Opus1m.into();
assert_eq!(s, "opus[1m]");
}
#[test]
fn test_display_names() {
assert_eq!(ClaudeModel::Opus5.display_name(), "Opus 5");
assert_eq!(ClaudeModel::Opus5.cli_arg(), "claude-opus-5");
assert_eq!(ClaudeModel::Fable5.display_name(), "Fable 5");
assert_eq!(ClaudeModel::Opus48.display_name(), "Opus 4.8");
assert_eq!(ClaudeModel::Sonnet.display_name(), "Sonnet (latest)");
assert!(ClaudeModel::Sonnet.is_alias());
assert!(!ClaudeModel::Sonnet5.is_alias());
}
#[test]
fn test_serde_round_trip() {
let json = serde_json::to_string(&ClaudeModel::Haiku45).unwrap();
assert_eq!(json, "\"claude-haiku-4-5\"");
let back: ClaudeModel = serde_json::from_str(&json).unwrap();
assert_eq!(back, ClaudeModel::Haiku45);
}
}