#[derive(Debug, Clone, Copy)]
pub struct ModelPricing {
pub name: &'static str,
pub input_per_mtoken: f64,
pub output_per_mtoken: f64,
pub cache_read_per_mtoken: Option<f64>,
pub cache_create_per_mtoken: Option<f64>,
#[allow(dead_code)]
pub last_verified: &'static str,
}
const PRICES: &[ModelPricing] = &[
ModelPricing {
name: "claude-opus-4-6",
input_per_mtoken: 15.0,
output_per_mtoken: 75.0,
cache_read_per_mtoken: Some(1.50),
cache_create_per_mtoken: Some(18.75),
last_verified: "2026-04-15 (estimate; unverified)",
},
ModelPricing {
name: "claude-sonnet-4-6",
input_per_mtoken: 3.0,
output_per_mtoken: 15.0,
cache_read_per_mtoken: Some(0.30),
cache_create_per_mtoken: Some(3.75),
last_verified: "2026-04-15 (estimate; unverified)",
},
ModelPricing {
name: "claude-haiku-4-5",
input_per_mtoken: 1.0,
output_per_mtoken: 5.0,
cache_read_per_mtoken: Some(0.10),
cache_create_per_mtoken: Some(1.25),
last_verified: "2026-04-15 (estimate; unverified)",
},
ModelPricing {
name: "gpt-5",
input_per_mtoken: 10.0,
output_per_mtoken: 30.0,
cache_read_per_mtoken: Some(2.50),
cache_create_per_mtoken: None,
last_verified: "2026-04-15 (estimate; unverified)",
},
ModelPricing {
name: "gpt-5-mini",
input_per_mtoken: 0.5,
output_per_mtoken: 2.0,
cache_read_per_mtoken: Some(0.10),
cache_create_per_mtoken: None,
last_verified: "2026-04-15 (estimate; unverified)",
},
ModelPricing {
name: "gemini-2-5-pro",
input_per_mtoken: 2.50,
output_per_mtoken: 15.0,
cache_read_per_mtoken: Some(0.625),
cache_create_per_mtoken: None,
last_verified: "2026-04-15 (estimate; unverified)",
},
ModelPricing {
name: "gemini-2-5-flash",
input_per_mtoken: 0.30,
output_per_mtoken: 2.50,
cache_read_per_mtoken: Some(0.075),
cache_create_per_mtoken: None,
last_verified: "2026-04-15 (estimate; unverified)",
},
];
#[must_use]
pub fn lookup(model: &str) -> Option<&'static ModelPricing> {
PRICES.iter().find(|p| p.name.eq_ignore_ascii_case(model))
}
#[must_use]
pub fn cost_usd(
model: Option<&str>,
tokens_in: Option<u64>,
tokens_out: Option<u64>,
cache_read: Option<u64>,
cache_create: Option<u64>,
) -> Option<f64> {
let pricing = lookup(model?)?;
let has_any = [tokens_in, tokens_out, cache_read, cache_create]
.iter()
.any(|v| v.is_some_and(|n| n > 0));
if !has_any {
return None;
}
#[allow(clippy::cast_precision_loss)]
let t_in = tokens_in.unwrap_or(0) as f64;
#[allow(clippy::cast_precision_loss)]
let t_out = tokens_out.unwrap_or(0) as f64;
#[allow(clippy::cast_precision_loss)]
let t_cr = cache_read.unwrap_or(0) as f64;
#[allow(clippy::cast_precision_loss)]
let t_cc = cache_create.unwrap_or(0) as f64;
let cost = t_in * pricing.input_per_mtoken
+ t_out * pricing.output_per_mtoken
+ t_cr
* pricing
.cache_read_per_mtoken
.unwrap_or(pricing.input_per_mtoken)
+ t_cc
* pricing
.cache_create_per_mtoken
.unwrap_or(pricing.input_per_mtoken);
Some(cost / 1_000_000.0)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn lookup_finds_known_model_case_insensitive() {
assert!(lookup("claude-opus-4-6").is_some());
assert!(lookup("Claude-Opus-4-6").is_some());
assert!(lookup("CLAUDE-OPUS-4-6").is_some());
}
#[test]
fn lookup_returns_none_for_unknown_model() {
assert!(lookup("llama-99-ultra").is_none());
assert!(lookup("").is_none());
}
#[test]
fn cost_unknown_model_returns_none() {
assert_eq!(
cost_usd(Some("unknown"), Some(100), Some(50), None, None),
None
);
}
#[test]
fn cost_none_model_returns_none() {
assert_eq!(cost_usd(None, Some(100), Some(50), None, None), None);
}
#[test]
fn cost_zero_tokens_returns_none() {
assert_eq!(
cost_usd(Some("claude-opus-4-6"), Some(0), Some(0), Some(0), Some(0)),
None
);
assert_eq!(
cost_usd(Some("claude-opus-4-6"), None, None, None, None),
None
);
}
#[test]
fn cost_computes_input_plus_output() {
let c = cost_usd(
Some("claude-opus-4-6"),
Some(1_000_000),
Some(1_000_000),
None,
None,
)
.unwrap();
assert!((c - 90.0).abs() < 1e-6, "expected 90.0, got {c}");
}
#[test]
fn cost_cache_read_uses_discounted_rate_when_provider_sets_one() {
let c = cost_usd(Some("claude-opus-4-6"), None, None, Some(1_000_000), None).unwrap();
assert!((c - 1.50).abs() < 1e-6, "expected 1.50, got {c}");
}
#[test]
fn cost_falls_back_to_input_rate_when_cache_rate_missing() {
let c = cost_usd(Some("gpt-5"), None, None, None, Some(1_000_000)).unwrap();
assert!((c - 10.0).abs() < 1e-6, "expected 10.0, got {c}");
}
#[test]
fn every_entry_has_last_verified_date() {
for p in PRICES {
assert!(
!p.last_verified.is_empty(),
"{} missing last_verified",
p.name
);
}
}
#[test]
fn no_duplicate_model_names() {
use std::collections::HashSet;
let mut seen = HashSet::new();
for p in PRICES {
assert!(seen.insert(p.name), "duplicate pricing entry: {}", p.name);
}
}
}