use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
#[derive(Default)]
pub enum Model {
#[serde(rename = "claude-opus-4-6")]
Opus46,
#[serde(rename = "claude-opus-4-5-20251101")]
Opus45,
#[serde(rename = "claude-sonnet-4-5-20250929")]
Sonnet45,
#[serde(rename = "claude-sonnet-4-20250514")]
Sonnet4,
#[serde(rename = "claude-haiku-4-5-20251001")]
Haiku45,
#[serde(rename = "claude-3-5-haiku-20241022")]
Haiku35,
#[serde(rename = "claude-3-5-sonnet-20241022")]
Sonnet35V2,
#[serde(other)]
#[default]
Unknown,
}
impl Model {
pub fn display_name(&self) -> &'static str {
match self {
Self::Opus46 => "Opus 4.6",
Self::Opus45 => "Opus 4.5",
Self::Sonnet45 => "Sonnet 4.5",
Self::Sonnet4 => "Sonnet 4",
Self::Haiku45 => "Haiku 4.5",
Self::Haiku35 => "Haiku 3.5",
Self::Sonnet35V2 => "Sonnet 3.5 v2",
Self::Unknown => "Unknown",
}
}
pub fn context_window_size(&self) -> u32 {
match self {
Self::Opus46 => 200_000,
Self::Opus45 => 200_000,
Self::Sonnet45 => 200_000,
Self::Sonnet4 => 200_000,
Self::Haiku45 => 200_000,
Self::Haiku35 => 200_000,
Self::Sonnet35V2 => 200_000,
Self::Unknown => 200_000, }
}
pub fn input_cost_per_million(&self) -> f64 {
match self {
Self::Opus46 => 5.00,
Self::Opus45 => 15.00,
Self::Sonnet45 => 3.00,
Self::Sonnet4 => 3.00,
Self::Haiku45 => 1.00,
Self::Haiku35 => 0.80,
Self::Sonnet35V2 => 3.00,
Self::Unknown => 3.00, }
}
pub fn output_cost_per_million(&self) -> f64 {
match self {
Self::Opus46 => 25.00,
Self::Opus45 => 75.00,
Self::Sonnet45 => 15.00,
Self::Sonnet4 => 15.00,
Self::Haiku45 => 5.00,
Self::Haiku35 => 4.00,
Self::Sonnet35V2 => 15.00,
Self::Unknown => 15.00, }
}
pub fn from_id(id: &str) -> Self {
if id.starts_with("claude-opus-4-6") {
Self::Opus46
} else if id.starts_with("claude-opus-4-5") {
Self::Opus45
} else if id.starts_with("claude-sonnet-4-5") {
Self::Sonnet45
} else if id.starts_with("claude-sonnet-4") {
Self::Sonnet4
} else if id.starts_with("claude-haiku-4-5") {
Self::Haiku45
} else if id.starts_with("claude-3-5-haiku") {
Self::Haiku35
} else if id.starts_with("claude-3-5-sonnet") {
Self::Sonnet35V2
} else {
Self::Unknown
}
}
pub fn is_unknown(&self) -> bool {
matches!(self, Self::Unknown)
}
}
pub fn derive_display_name(id: &str) -> String {
if id.len() > 9 {
let potential_date = &id[id.len() - 8..];
if potential_date.chars().all(|c| c.is_ascii_digit()) {
if let Some(base) = id[..id.len() - 8].strip_suffix('-') {
return base.to_string();
}
}
}
id.to_string()
}
impl fmt::Display for Model {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.display_name())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_model_parsing_opus45() {
let model: Model = serde_json::from_str("\"claude-opus-4-5-20251101\"").unwrap();
assert_eq!(model, Model::Opus45);
assert_eq!(model.display_name(), "Opus 4.5");
}
#[test]
fn test_model_parsing_opus46() {
assert_eq!(Model::from_id("claude-opus-4-6"), Model::Opus46);
assert_eq!(Model::Opus46.display_name(), "Opus 4.6");
}
#[test]
fn test_model_parsing_sonnet45() {
assert_eq!(
Model::from_id("claude-sonnet-4-5-20250929"),
Model::Sonnet45
);
assert_eq!(Model::Sonnet45.display_name(), "Sonnet 4.5");
}
#[test]
fn test_model_parsing_haiku45() {
assert_eq!(Model::from_id("claude-haiku-4-5-20251001"), Model::Haiku45);
assert_eq!(Model::Haiku45.display_name(), "Haiku 4.5");
}
#[test]
fn test_model_unknown_serde() {
let model: Model = serde_json::from_str("\"gpt-4o\"").unwrap();
assert_eq!(model, Model::Unknown);
}
#[test]
fn test_from_id_prefix_exact() {
assert_eq!(Model::from_id("claude-opus-4-5-20251101"), Model::Opus45);
assert_eq!(Model::from_id("claude-sonnet-4-20250514"), Model::Sonnet4);
}
#[test]
fn test_from_id_prefix_with_different_date() {
assert_eq!(Model::from_id("claude-opus-4-6-20260301"), Model::Opus46);
assert_eq!(
Model::from_id("claude-sonnet-4-5-20261201"),
Model::Sonnet45
);
assert_eq!(Model::from_id("claude-haiku-4-5-20260601"), Model::Haiku45);
assert_eq!(Model::from_id("claude-opus-4-5-20260101"), Model::Opus45);
}
#[test]
fn test_from_id_prefix_no_date() {
assert_eq!(Model::from_id("claude-opus-4-6"), Model::Opus46);
assert_eq!(Model::from_id("claude-sonnet-4-5"), Model::Sonnet45);
}
#[test]
fn test_from_id_sonnet4_not_confused_with_sonnet45() {
assert_eq!(
Model::from_id("claude-sonnet-4-5-20250929"),
Model::Sonnet45
);
assert_eq!(Model::from_id("claude-sonnet-4-20250514"), Model::Sonnet4);
}
#[test]
fn test_from_id_unknown() {
assert_eq!(Model::from_id("gpt-4o"), Model::Unknown);
assert_eq!(Model::from_id("gemini-1.5-pro"), Model::Unknown);
assert_eq!(Model::from_id("llama-3-70b"), Model::Unknown);
assert_eq!(Model::from_id("unknown-model"), Model::Unknown);
}
#[test]
fn test_is_unknown() {
assert!(Model::Unknown.is_unknown());
assert!(!Model::Opus46.is_unknown());
}
#[test]
fn test_derive_display_name_strips_date() {
assert_eq!(
derive_display_name("claude-opus-4-7-20260501"),
"claude-opus-4-7"
);
assert_eq!(
derive_display_name("claude-sonnet-5-20270101"),
"claude-sonnet-5"
);
}
#[test]
fn test_derive_display_name_no_date() {
assert_eq!(derive_display_name("gpt-4o"), "gpt-4o");
assert_eq!(derive_display_name("gemini-1.5-pro"), "gemini-1.5-pro");
}
#[test]
fn test_derive_display_name_short_ids() {
assert_eq!(derive_display_name("gpt-4"), "gpt-4");
assert_eq!(derive_display_name("o1"), "o1");
}
}