use crate::model::Usage;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ModelPricing {
pub input: f64,
pub output: f64,
pub cache_read: f64,
pub cache_write_5m: f64,
pub cache_write_1h: f64,
}
pub const SNAPSHOT: &[(&str, ModelPricing)] = &[
(
"claude-fable-5",
ModelPricing {
input: 10.0,
output: 50.0,
cache_read: 1.0,
cache_write_5m: 12.50,
cache_write_1h: 20.0,
},
),
(
"claude-mythos-5",
ModelPricing {
input: 10.0,
output: 50.0,
cache_read: 1.0,
cache_write_5m: 12.50,
cache_write_1h: 20.0,
},
),
(
"claude-sonnet-4-6",
ModelPricing {
input: 3.0,
output: 15.0,
cache_read: 0.30,
cache_write_5m: 3.75,
cache_write_1h: 6.0,
},
),
(
"claude-sonnet-4-5",
ModelPricing {
input: 3.0,
output: 15.0,
cache_read: 0.30,
cache_write_5m: 3.75,
cache_write_1h: 6.0,
},
),
(
"claude-sonnet-4",
ModelPricing {
input: 3.0,
output: 15.0,
cache_read: 0.30,
cache_write_5m: 3.75,
cache_write_1h: 6.0,
},
),
(
"claude-3-7-sonnet",
ModelPricing {
input: 3.0,
output: 15.0,
cache_read: 0.30,
cache_write_5m: 3.75,
cache_write_1h: 6.0,
},
),
(
"claude-opus-4-8",
ModelPricing {
input: 5.0,
output: 25.0,
cache_read: 0.50,
cache_write_5m: 6.25,
cache_write_1h: 10.0,
},
),
(
"claude-opus-4-7",
ModelPricing {
input: 5.0,
output: 25.0,
cache_read: 0.50,
cache_write_5m: 6.25,
cache_write_1h: 10.0,
},
),
(
"claude-opus-4-6",
ModelPricing {
input: 5.0,
output: 25.0,
cache_read: 0.50,
cache_write_5m: 6.25,
cache_write_1h: 10.0,
},
),
(
"claude-opus-4-5",
ModelPricing {
input: 5.0,
output: 25.0,
cache_read: 0.50,
cache_write_5m: 6.25,
cache_write_1h: 10.0,
},
),
(
"claude-opus-4-1",
ModelPricing {
input: 15.0,
output: 75.0,
cache_read: 1.50,
cache_write_5m: 18.75,
cache_write_1h: 30.0,
},
),
(
"claude-opus-4",
ModelPricing {
input: 15.0,
output: 75.0,
cache_read: 1.50,
cache_write_5m: 18.75,
cache_write_1h: 30.0,
},
),
(
"claude-haiku-4-5",
ModelPricing {
input: 1.0,
output: 5.0,
cache_read: 0.10,
cache_write_5m: 1.25,
cache_write_1h: 2.0,
},
),
(
"claude-3-5-haiku",
ModelPricing {
input: 0.80,
output: 4.0,
cache_read: 0.08,
cache_write_5m: 1.0,
cache_write_1h: 1.6,
},
),
];
pub fn lookup(model: &str) -> Option<&'static ModelPricing> {
let m = normalize_model(model);
SNAPSHOT
.iter()
.filter(|(key, _)| key_matches(&m, key))
.max_by_key(|(key, _)| key.len()) .map(|(_, p)| p)
}
fn normalize_model(model: &str) -> String {
let mut m = model.trim().to_ascii_lowercase();
for prefix in ["anthropic/", "us.anthropic.", "anthropic."] {
if let Some(rest) = m.strip_prefix(prefix) {
m = rest.to_string();
break;
}
}
m
}
fn key_matches(model: &str, key: &str) -> bool {
match model.strip_prefix(key) {
None => false,
Some("") => true,
Some(rest) => {
(rest.starts_with('-') || rest.starts_with('@'))
&& rest.len() >= 9
&& rest[1..].chars().all(|c| c.is_ascii_digit())
}
}
}
pub fn cost_usd(model: Option<&str>, usage: &Usage) -> Option<f64> {
Some(price_usage(lookup(model?)?, usage))
}
fn price_usage(p: &ModelPricing, usage: &Usage) -> f64 {
let per_m = |tokens: Option<u64>, rate: f64| tokens.unwrap_or(0) as f64 * rate / 1e6;
let mut cost = per_m(usage.input, p.input)
+ per_m(usage.output, p.output)
+ per_m(usage.thinking, p.output)
+ per_m(usage.cache_read, p.cache_read);
cost += match (usage.cache_creation_5m, usage.cache_creation_1h) {
(None, None) => per_m(usage.cache_creation, p.cache_write_5m),
(m5, h1) => per_m(m5, p.cache_write_5m) + per_m(h1, p.cache_write_1h),
};
cost
}
#[derive(Debug, Clone, Default)]
pub struct PricingTable {
overrides: Vec<(String, ModelPricing)>,
}
impl PricingTable {
pub fn embedded() -> Self {
Self::default()
}
pub fn with_overrides(overrides: Vec<(String, ModelPricing)>) -> Self {
let overrides = overrides
.into_iter()
.map(|(k, p)| (k.trim().to_ascii_lowercase(), p))
.collect();
Self { overrides }
}
pub fn override_count(&self) -> usize {
self.overrides.len()
}
pub fn lookup(&self, model: &str) -> Option<&ModelPricing> {
if !self.overrides.is_empty() {
let m = normalize_model(model);
if let Some((_, p)) = self
.overrides
.iter()
.filter(|(key, _)| key_matches(&m, key))
.max_by_key(|(key, _)| key.len())
{
return Some(p);
}
}
lookup(model)
}
pub fn cost_usd(&self, model: Option<&str>, usage: &Usage) -> Option<f64> {
Some(price_usage(self.lookup(model?)?, usage))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn dated_release_suffixes_match() {
assert!(lookup("claude-sonnet-4-5-20250929").is_some());
assert!(lookup("claude-haiku-4-5-20251001").is_some());
assert!(lookup("anthropic/claude-sonnet-4-5").is_some());
assert_eq!(lookup("claude-sonnet-4-5").map(|p| p.input), Some(3.0));
}
#[test]
fn unknown_models_are_never_guessed() {
assert!(lookup("claude-opus-4-9").is_none()); assert!(lookup("claude-opus-5-0").is_none());
assert!(lookup("claude-fable-6").is_none());
assert!(lookup("<synthetic>").is_none());
assert!(lookup("gpt-yolo").is_none());
}
#[test]
fn current_models_from_official_snapshot_are_priced() {
assert_eq!(lookup("claude-opus-4-8").map(|p| p.input), Some(5.0));
assert_eq!(lookup("claude-opus-4-7").map(|p| p.input), Some(5.0));
assert_eq!(lookup("claude-opus-4-6").map(|p| p.input), Some(5.0));
assert_eq!(lookup("claude-sonnet-4-6").map(|p| p.output), Some(15.0));
assert_eq!(lookup("claude-fable-5").map(|p| p.output), Some(50.0));
assert_eq!(lookup("claude-mythos-5").map(|p| p.input), Some(10.0));
let fable = lookup("claude-fable-5").unwrap();
assert_eq!(fable.cache_read, 1.0);
assert_eq!(fable.cache_write_5m, 12.50);
assert_eq!(fable.cache_write_1h, 20.0);
}
#[test]
fn cache_ttls_price_differently() {
let split = Usage {
cache_creation: Some(1_000_000),
cache_creation_5m: Some(0),
cache_creation_1h: Some(1_000_000),
..Usage::default()
};
let cost = cost_usd(Some("claude-sonnet-4-5"), &split).unwrap();
assert!((cost - 6.0).abs() < 1e-9, "got {cost}");
let unsplit = Usage {
cache_creation: Some(1_000_000),
..Usage::default()
};
let cost = cost_usd(Some("claude-sonnet-4-5"), &unsplit).unwrap();
assert!(
(cost - 3.75).abs() < 1e-9,
"unsplit assumes 5m rate, got {cost}"
);
}
#[test]
fn cost_traces_to_unit_prices() {
let usage = Usage {
input: Some(1_000_000),
output: Some(1_000_000),
cache_read: Some(1_000_000),
..Usage::default()
};
let cost = cost_usd(Some("claude-haiku-4-5"), &usage).unwrap();
assert!((cost - (1.0 + 5.0 + 0.10)).abs() < 1e-9);
assert_eq!(cost_usd(None, &usage), None);
}
}