use crate::types::{Model, Usage};
#[derive(Debug, Clone, Copy)]
pub struct ModelPricing {
pub input: f64,
pub cache_write_5m: f64,
pub cache_write_1h: f64,
pub cache_read: f64,
pub output: f64,
}
impl ModelPricing {
pub const FABLE_5: Self = Self {
input: 10.0,
cache_write_5m: 12.5,
cache_write_1h: 20.0,
cache_read: 1.0,
output: 50.0,
};
pub const MYTHOS_5: Self = Self {
input: 10.0,
cache_write_5m: 12.5,
cache_write_1h: 20.0,
cache_read: 1.0,
output: 50.0,
};
pub const OPUS_5: Self = Self {
input: 5.0,
cache_write_5m: 6.25,
cache_write_1h: 10.0,
cache_read: 0.50,
output: 25.0,
};
pub const SONNET_5: Self = Self {
input: 2.0,
cache_write_5m: 2.5,
cache_write_1h: 4.0,
cache_read: 0.20,
output: 10.0,
};
pub const OPUS_5_FAST: Self = Self {
input: 10.0,
cache_write_5m: 12.5,
cache_write_1h: 20.0,
cache_read: 1.0,
output: 50.0,
};
pub const OPUS_48: Self = Self {
input: 5.0,
cache_write_5m: 6.25,
cache_write_1h: 10.0,
cache_read: 0.50,
output: 25.0,
};
pub const OPUS_47: Self = Self {
input: 5.0,
cache_write_5m: 6.25,
cache_write_1h: 10.0,
cache_read: 0.50,
output: 25.0,
};
pub const OPUS_46: Self = Self {
input: 5.0,
cache_write_5m: 6.25,
cache_write_1h: 10.0,
cache_read: 0.50,
output: 25.0,
};
pub const OPUS_45: Self = Self {
input: 5.0,
cache_write_5m: 6.25,
cache_write_1h: 10.0,
cache_read: 0.50,
output: 25.0,
};
pub const OPUS_41: Self = Self {
input: 15.0,
cache_write_5m: 18.75,
cache_write_1h: 30.0,
cache_read: 1.50,
output: 75.0,
};
pub const OPUS_4: Self = Self {
input: 15.0,
cache_write_5m: 18.75,
cache_write_1h: 30.0,
cache_read: 1.50,
output: 75.0,
};
pub const SONNET_46: Self = Self {
input: 3.0,
cache_write_5m: 3.75,
cache_write_1h: 6.0,
cache_read: 0.30,
output: 15.0,
};
pub const SONNET_45: Self = Self {
input: 3.0,
cache_write_5m: 3.75,
cache_write_1h: 6.0,
cache_read: 0.30,
output: 15.0,
};
pub const SONNET_4: Self = Self {
input: 3.0,
cache_write_5m: 3.75,
cache_write_1h: 6.0,
cache_read: 0.30,
output: 15.0,
};
pub const HAIKU_45: Self = Self {
input: 1.0,
cache_write_5m: 1.25,
cache_write_1h: 2.0,
cache_read: 0.10,
output: 5.0,
};
pub const HAIKU_35: Self = Self {
input: 0.80,
cache_write_5m: 1.0,
cache_write_1h: 1.60,
cache_read: 0.08,
output: 4.0,
};
pub fn for_model(model: &Model) -> Option<Self> {
Self::for_model_id(&model.to_string())
}
pub fn for_model_id(model_id: &str) -> Option<Self> {
let pricing = match model_id {
id if id.starts_with("claude-fable-5") => Self::FABLE_5,
id if id.starts_with("claude-mythos-5") => Self::MYTHOS_5,
id if id.starts_with("claude-opus-5") => Self::OPUS_5,
id if id.starts_with("claude-sonnet-5") => Self::SONNET_5,
id if id.starts_with("claude-opus-4-8") => Self::OPUS_48,
id if id.starts_with("claude-opus-4-7") => Self::OPUS_47,
id if id.starts_with("claude-opus-4-6") => Self::OPUS_46,
id if id.starts_with("claude-opus-4-5") => Self::OPUS_45,
id if id.starts_with("claude-opus-4-1") => Self::OPUS_41,
id if id.starts_with("claude-opus-4") => Self::OPUS_4,
id if id.starts_with("claude-sonnet-4-6") => Self::SONNET_46,
id if id.starts_with("claude-sonnet-4-5") => Self::SONNET_45,
id if id.starts_with("claude-sonnet-4") => Self::SONNET_4,
id if id.starts_with("claude-haiku-4-5") => Self::HAIKU_45,
id if id.starts_with("claude-haiku-3-5") || id.starts_with("claude-3-5-haiku") => {
Self::HAIKU_35
}
_ => return None,
};
Some(pricing)
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct CostBreakdown {
pub input_cost: f64,
pub cache_write_cost: f64,
pub cache_read_cost: f64,
pub output_cost: f64,
}
impl CostBreakdown {
pub fn total(&self) -> f64 {
self.input_cost + self.cache_write_cost + self.cache_read_cost + self.output_cost
}
}
impl std::fmt::Display for CostBreakdown {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"${:.6} (in=${:.6} cache_w=${:.6} cache_r=${:.6} out=${:.6})",
self.total(),
self.input_cost,
self.cache_write_cost,
self.cache_read_cost,
self.output_cost
)
}
}
pub fn estimate_cost(pricing: ModelPricing, usage: &Usage) -> CostBreakdown {
let mtok = 1_000_000.0;
CostBreakdown {
input_cost: usage.input_tokens as f64 / mtok * pricing.input,
cache_write_cost: usage.cache_creation_input_tokens.unwrap_or(0) as f64 / mtok
* pricing.cache_write_5m,
cache_read_cost: usage.cache_read_input_tokens.unwrap_or(0) as f64 / mtok
* pricing.cache_read,
output_cost: usage.output_tokens as f64 / mtok * pricing.output,
}
}
pub fn estimate_cost_1h(pricing: ModelPricing, usage: &Usage) -> CostBreakdown {
let mtok = 1_000_000.0;
CostBreakdown {
input_cost: usage.input_tokens as f64 / mtok * pricing.input,
cache_write_cost: usage.cache_creation_input_tokens.unwrap_or(0) as f64 / mtok
* pricing.cache_write_1h,
cache_read_cost: usage.cache_read_input_tokens.unwrap_or(0) as f64 / mtok
* pricing.cache_read,
output_cost: usage.output_tokens as f64 / mtok * pricing.output,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sonnet_46_basic_cost() {
let usage = Usage {
input_tokens: 1000,
output_tokens: 500,
cache_creation_input_tokens: None,
cache_read_input_tokens: None,
cache_creation_input_tokens_1h: None,
server_tool_use: None,
};
let cost = estimate_cost(ModelPricing::SONNET_46, &usage);
assert!((cost.input_cost - 0.003).abs() < 1e-9);
assert!((cost.output_cost - 0.0075).abs() < 1e-9);
assert!((cost.total() - 0.0105).abs() < 1e-9);
}
#[test]
fn sonnet_5_basic_cost() {
let usage = Usage {
input_tokens: 1000,
output_tokens: 500,
cache_creation_input_tokens: None,
cache_read_input_tokens: None,
cache_creation_input_tokens_1h: None,
server_tool_use: None,
};
let cost = estimate_cost(ModelPricing::SONNET_5, &usage);
assert!((cost.input_cost - 0.002).abs() < 1e-9);
assert!((cost.output_cost - 0.005).abs() < 1e-9);
}
#[test]
fn sonnet_46_with_caching() {
let usage = Usage {
input_tokens: 3,
output_tokens: 256,
cache_creation_input_tokens: Some(274),
cache_read_input_tokens: Some(2048),
cache_creation_input_tokens_1h: None,
server_tool_use: None,
};
let cost = estimate_cost(ModelPricing::SONNET_46, &usage);
assert!(cost.cache_read_cost > 0.0);
assert!(cost.cache_write_cost > 0.0);
assert!(cost.total() > 0.0);
}
#[test]
fn published_rates_match_vendor_page() {
for (id, input, output, cache_read) in [
("claude-fable-5", 10.0, 50.0, 1.0),
("claude-mythos-5", 10.0, 50.0, 1.0),
("claude-opus-5", 5.0, 25.0, 0.50),
("claude-opus-4-8", 5.0, 25.0, 0.50),
("claude-opus-4-7", 5.0, 25.0, 0.50),
("claude-opus-4-6", 5.0, 25.0, 0.50),
("claude-opus-4-5", 5.0, 25.0, 0.50),
("claude-opus-4-1", 15.0, 75.0, 1.50),
("claude-sonnet-5", 2.0, 10.0, 0.20),
("claude-sonnet-4-6", 3.0, 15.0, 0.30),
("claude-sonnet-4-5", 3.0, 15.0, 0.30),
("claude-haiku-4-5", 1.0, 5.0, 0.10),
("claude-haiku-3-5", 0.80, 4.0, 0.08),
] {
let p = ModelPricing::for_model_id(id).unwrap_or_else(|| panic!("{id} missing"));
assert!((p.input - input).abs() < 1e-9, "{id} input {} != {input}", p.input);
assert!((p.output - output).abs() < 1e-9, "{id} output {} != {output}", p.output);
assert!(
(p.cache_read - cache_read).abs() < 1e-9,
"{id} cache_read {} != {cache_read}",
p.cache_read
);
}
}
#[test]
fn factory_models_all_resolve_to_pricing() {
for model in [
Model::claude_sonnet_5(),
Model::claude_opus_5(),
Model::claude_fable_5(),
Model::claude_mythos_5(),
] {
assert!(ModelPricing::for_model(&model).is_some(), "{model} has no pricing entry");
}
}
#[test]
fn dated_aliases_resolve() {
let dated = ModelPricing::for_model_id("claude-sonnet-4-5-20250929").unwrap();
assert!((dated.input - ModelPricing::SONNET_45.input).abs() < 1e-9);
let dated = ModelPricing::for_model_id("claude-opus-4-5-20251101").unwrap();
assert!((dated.input - ModelPricing::OPUS_45.input).abs() < 1e-9);
assert!(ModelPricing::for_model_id("claude-99-turbo").is_none());
}
#[test]
fn cache_multipliers_follow_documented_ratios() {
for p in [
ModelPricing::FABLE_5,
ModelPricing::MYTHOS_5,
ModelPricing::OPUS_5,
ModelPricing::OPUS_48,
ModelPricing::SONNET_5,
ModelPricing::SONNET_46,
ModelPricing::HAIKU_45,
ModelPricing::HAIKU_35,
] {
assert!((p.cache_write_5m - p.input * 1.25).abs() < 1e-9);
assert!((p.cache_write_1h - p.input * 2.0).abs() < 1e-9);
assert!((p.cache_read - p.input * 0.1).abs() < 1e-9);
}
}
#[test]
fn fast_mode_doubles_standard_rates() {
assert!((ModelPricing::OPUS_5_FAST.input - ModelPricing::OPUS_5.input * 2.0).abs() < 1e-9);
assert!(
(ModelPricing::OPUS_5_FAST.output - ModelPricing::OPUS_5.output * 2.0).abs() < 1e-9
);
assert!((ModelPricing::OPUS_5_FAST.input - ModelPricing::OPUS_48.input * 2.0).abs() < 1e-9);
}
#[test]
fn display_format() {
let cost = CostBreakdown {
input_cost: 0.003,
cache_write_cost: 0.001,
cache_read_cost: 0.0005,
output_cost: 0.0075,
};
let s = cost.to_string();
assert!(s.starts_with('$'));
assert!(s.contains("in="));
assert!(s.contains("out="));
}
}