#[inline]
fn model_prices(model: &str) -> (f64, f64) {
match model {
m if m.contains("opus") => (5.0, 25.0),
m if m.contains("sonnet") => (3.0, 15.0),
m if m.contains("haiku") => (1.0, 5.0),
_ => (3.0, 15.0), }
}
pub fn calculate_cost(
model: &str,
input_tokens: u64,
output_tokens: u64,
cache_read: u64,
cache_creation: u64,
) -> f64 {
let (input_price, output_price) = model_prices(model);
(input_tokens as f64 / 1_000_000.0) * input_price
+ (cache_read as f64 / 1_000_000.0) * input_price * 0.1
+ (cache_creation as f64 / 1_000_000.0) * input_price * 1.25
+ (output_tokens as f64 / 1_000_000.0) * output_price
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn opus_pricing() {
let cost = calculate_cost("claude-opus-4-5", 1_000_000, 1_000_000, 0, 0);
assert!((cost - 30.0).abs() < 1e-9, "expected $30, got ${cost}");
}
#[test]
fn sonnet_pricing() {
let cost = calculate_cost("claude-sonnet-4-5", 1_000_000, 1_000_000, 0, 0);
assert!((cost - 18.0).abs() < 1e-9, "expected $18, got ${cost}");
}
#[test]
fn haiku_pricing() {
let cost = calculate_cost("claude-haiku-4-5", 1_000_000, 1_000_000, 0, 0);
assert!((cost - 6.0).abs() < 1e-9, "expected $6, got ${cost}");
}
#[test]
fn cache_read_bills_at_tenth_input_rate() {
let cost = calculate_cost("claude-sonnet-4-5", 0, 0, 1_000_000, 0);
assert!((cost - 0.30).abs() < 1e-9, "expected $0.30, got ${cost}");
}
#[test]
fn cache_creation_bills_at_125_percent_input_rate() {
let cost = calculate_cost("claude-sonnet-4-5", 0, 0, 0, 1_000_000);
assert!((cost - 3.75).abs() < 1e-9, "expected $3.75, got ${cost}");
}
#[test]
fn unknown_model_falls_back_to_sonnet() {
let cost_unknown = calculate_cost("gpt-99-turbo", 1_000_000, 0, 0, 0);
let cost_sonnet = calculate_cost("claude-sonnet-4-5", 1_000_000, 0, 0, 0);
assert!((cost_unknown - cost_sonnet).abs() < 1e-9);
}
#[test]
fn zero_usage_is_zero_cost() {
assert_eq!(calculate_cost("claude-opus-4-5", 0, 0, 0, 0), 0.0);
}
}