use crate::llm::types::TokenUsage;
pub fn estimate_cost(model: &str, usage: &TokenUsage) -> Option<f64> {
let (input_per_m, output_per_m) = model_pricing(model)?;
let input_cost = (usage.input_tokens as f64 / 1_000_000.0) * input_per_m;
let output_cost = (usage.output_tokens as f64 / 1_000_000.0) * output_per_m;
let cache_read_cost = (usage.cache_read_input_tokens as f64 / 1_000_000.0) * input_per_m * 0.1;
let cache_write_cost =
(usage.cache_creation_input_tokens as f64 / 1_000_000.0) * input_per_m * 1.25;
let reasoning_cost = (usage.reasoning_tokens as f64 / 1_000_000.0) * output_per_m;
Some(input_cost + output_cost + cache_read_cost + cache_write_cost + reasoning_cost)
}
fn model_pricing(model: &str) -> Option<(f64, f64)> {
match model {
"claude-sonnet-4-6-20250610" => Some((3.0, 15.0)),
"claude-opus-4-6-20250610" => Some((5.0, 25.0)),
"claude-sonnet-4-5-20250514" => Some((3.0, 15.0)),
"claude-opus-4-5-20250514" => Some((5.0, 25.0)),
"claude-sonnet-4-20250514" => Some((3.0, 15.0)),
"claude-opus-4-20250514" | "claude-opus-4-1-20250414" => Some((15.0, 75.0)),
"claude-haiku-4-5-20251001" => Some((1.0, 5.0)),
"claude-3-5-sonnet-20241022" | "claude-3-5-sonnet-20240620" => Some((3.0, 15.0)),
"claude-3-5-haiku-20241022" => Some((0.80, 4.0)),
"claude-3-opus-20240229" => Some((15.0, 75.0)),
"claude-3-sonnet-20240229" => Some((3.0, 15.0)),
"claude-3-haiku-20240307" => Some((0.25, 1.25)),
"anthropic/claude-sonnet-4.6" => Some((3.0, 15.0)),
"anthropic/claude-opus-4.6" => Some((5.0, 25.0)),
"anthropic/claude-sonnet-4.5" => Some((3.0, 15.0)),
"anthropic/claude-opus-4.5" => Some((5.0, 25.0)),
"anthropic/claude-sonnet-4" => Some((3.0, 15.0)),
"anthropic/claude-opus-4" => Some((15.0, 75.0)),
"anthropic/claude-haiku-4" => Some((1.0, 5.0)),
"anthropic/claude-3.5-sonnet" | "anthropic/claude-3.5-sonnet:beta" => Some((3.0, 15.0)),
"anthropic/claude-3.5-haiku" | "anthropic/claude-3.5-haiku:beta" => Some((0.80, 4.0)),
"anthropic/claude-3-opus" | "anthropic/claude-3-opus:beta" => Some((15.0, 75.0)),
"anthropic/claude-3-haiku" | "anthropic/claude-3-haiku:beta" => Some((0.25, 1.25)),
"qwen/qwen3.5-plus-02-15" => Some((0.40, 2.40)),
"deepseek/deepseek-v3.2" => Some((0.26, 0.38)),
"google/gemini-3-flash-preview" => Some((0.50, 3.00)),
"nvidia/nemotron-3-nano-30b-a3b" => Some((0.05, 0.20)),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sonnet_pricing() {
let usage = TokenUsage {
input_tokens: 1_000_000,
output_tokens: 1_000_000,
..Default::default()
};
let cost = estimate_cost("claude-sonnet-4-20250514", &usage).unwrap();
assert!((cost - 18.0).abs() < 0.001, "cost: {cost}");
}
#[test]
fn opus_pricing() {
let usage = TokenUsage {
input_tokens: 1_000_000,
output_tokens: 1_000_000,
..Default::default()
};
let cost = estimate_cost("claude-opus-4-20250514", &usage).unwrap();
assert!((cost - 90.0).abs() < 0.001, "cost: {cost}");
}
#[test]
fn haiku_pricing() {
let usage = TokenUsage {
input_tokens: 1_000_000,
output_tokens: 1_000_000,
..Default::default()
};
let cost = estimate_cost("claude-haiku-4-5-20251001", &usage).unwrap();
assert!((cost - 6.0).abs() < 0.001, "cost: {cost}");
}
#[test]
fn unknown_model_returns_none() {
let usage = TokenUsage {
input_tokens: 100,
output_tokens: 50,
..Default::default()
};
assert!(estimate_cost("gpt-4o", &usage).is_none());
assert!(estimate_cost("unknown-model", &usage).is_none());
}
#[test]
fn zero_usage_returns_zero() {
let usage = TokenUsage::default();
let cost = estimate_cost("claude-sonnet-4-20250514", &usage).unwrap();
assert!((cost - 0.0).abs() < f64::EPSILON, "cost: {cost}");
}
#[test]
fn cache_read_tokens_priced_at_10_percent() {
let usage = TokenUsage {
input_tokens: 0,
output_tokens: 0,
cache_read_input_tokens: 1_000_000,
cache_creation_input_tokens: 0,
reasoning_tokens: 0,
};
let cost = estimate_cost("claude-sonnet-4-20250514", &usage).unwrap();
assert!((cost - 0.30).abs() < 0.001, "cost: {cost}");
}
#[test]
fn cache_write_tokens_priced_at_125_percent() {
let usage = TokenUsage {
input_tokens: 0,
output_tokens: 0,
cache_read_input_tokens: 0,
cache_creation_input_tokens: 1_000_000,
reasoning_tokens: 0,
};
let cost = estimate_cost("claude-sonnet-4-20250514", &usage).unwrap();
assert!((cost - 3.75).abs() < 0.001, "cost: {cost}");
}
#[test]
fn mixed_usage_accumulates_correctly() {
let usage = TokenUsage {
input_tokens: 500_000,
output_tokens: 100_000,
cache_read_input_tokens: 200_000,
cache_creation_input_tokens: 50_000,
reasoning_tokens: 0,
};
let cost = estimate_cost("claude-sonnet-4-20250514", &usage).unwrap();
let expected = 1.50 + 1.50 + 0.06 + 0.1875;
assert!(
(cost - expected).abs() < 0.001,
"cost: {cost}, expected: {expected}"
);
}
#[test]
fn openrouter_model_aliases() {
let usage = TokenUsage {
input_tokens: 1_000_000,
output_tokens: 0,
..Default::default()
};
let cost_native = estimate_cost("claude-sonnet-4-20250514", &usage).unwrap();
let cost_or = estimate_cost("anthropic/claude-sonnet-4", &usage).unwrap();
assert!((cost_native - cost_or).abs() < f64::EPSILON);
}
#[test]
fn claude_3_5_sonnet_pricing() {
let usage = TokenUsage {
input_tokens: 1_000_000,
output_tokens: 1_000_000,
..Default::default()
};
let cost = estimate_cost("claude-3-5-sonnet-20241022", &usage).unwrap();
assert!((cost - 18.0).abs() < 0.001, "cost: {cost}");
}
#[test]
fn claude_3_haiku_pricing() {
let usage = TokenUsage {
input_tokens: 1_000_000,
output_tokens: 1_000_000,
..Default::default()
};
let cost = estimate_cost("claude-3-haiku-20240307", &usage).unwrap();
assert!((cost - 1.50).abs() < 0.001, "cost: {cost}");
}
#[test]
fn opus_4_5_pricing() {
let usage = TokenUsage {
input_tokens: 1_000_000,
output_tokens: 1_000_000,
..Default::default()
};
let cost = estimate_cost("claude-opus-4-5-20250514", &usage).unwrap();
assert!((cost - 30.0).abs() < 0.001, "cost: {cost}");
}
#[test]
fn opus_4_6_pricing() {
let usage = TokenUsage {
input_tokens: 1_000_000,
output_tokens: 1_000_000,
..Default::default()
};
let cost = estimate_cost("claude-opus-4-6-20250610", &usage).unwrap();
assert!((cost - 30.0).abs() < 0.001, "cost: {cost}");
}
#[test]
fn qwen_pricing() {
let usage = TokenUsage {
input_tokens: 1_000_000,
output_tokens: 1_000_000,
..Default::default()
};
let cost = estimate_cost("qwen/qwen3.5-plus-02-15", &usage).unwrap();
assert!((cost - 2.80).abs() < 0.001, "cost: {cost}");
}
#[test]
fn reasoning_tokens_priced_at_output_rate() {
let usage = TokenUsage {
input_tokens: 0,
output_tokens: 0,
cache_read_input_tokens: 0,
cache_creation_input_tokens: 0,
reasoning_tokens: 1_000_000,
};
let cost = estimate_cost("claude-sonnet-4-20250514", &usage).unwrap();
assert!((cost - 15.0).abs() < 0.001, "cost: {cost}");
}
#[test]
fn mixed_usage_with_reasoning_accumulates_correctly() {
let usage = TokenUsage {
input_tokens: 500_000,
output_tokens: 100_000,
cache_read_input_tokens: 0,
cache_creation_input_tokens: 0,
reasoning_tokens: 200_000,
};
let cost = estimate_cost("claude-sonnet-4-20250514", &usage).unwrap();
let expected = 1.50 + 1.50 + 3.00;
assert!(
(cost - expected).abs() < 0.001,
"cost: {cost}, expected: {expected}"
);
}
#[test]
fn openrouter_claude_3_5_aliases() {
let usage = TokenUsage {
input_tokens: 1_000_000,
output_tokens: 0,
..Default::default()
};
let native = estimate_cost("claude-3-5-sonnet-20241022", &usage).unwrap();
let or = estimate_cost("anthropic/claude-3.5-sonnet", &usage).unwrap();
assert!((native - or).abs() < f64::EPSILON);
let or_beta = estimate_cost("anthropic/claude-3.5-sonnet:beta", &usage).unwrap();
assert!((native - or_beta).abs() < f64::EPSILON);
}
#[test]
fn deepseek_pricing() {
let usage = TokenUsage {
input_tokens: 1_000_000,
output_tokens: 1_000_000,
..Default::default()
};
let cost = estimate_cost("deepseek/deepseek-v3.2", &usage).unwrap();
assert!((cost - 0.64).abs() < 0.001, "cost: {cost}");
}
}