#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Pricing {
pub input: f64,
pub output: f64,
pub cache_read: f64,
pub cache_write: f64,
}
impl Pricing {
pub fn estimate_usd(
&self,
input_tokens: u32,
output_tokens: u32,
cache_read_tokens: u32,
cache_write_tokens: u32,
) -> f64 {
let per_m = |tokens: u32, rate: f64| (tokens as f64 / 1_000_000.0) * rate;
per_m(input_tokens, self.input)
+ per_m(output_tokens, self.output)
+ per_m(cache_read_tokens, self.cache_read)
+ per_m(cache_write_tokens, self.cache_write)
}
}
pub fn pricing(model: &str) -> Option<Pricing> {
let m = model.to_ascii_lowercase();
if m.contains("claude-haiku") || m.contains("haiku-3") || m.contains("haiku-4") {
return Some(Pricing {
input: 0.80,
output: 4.0,
cache_read: 0.08,
cache_write: 1.0,
});
}
if m.contains("claude-opus") || m.contains("opus-4") {
return Some(Pricing {
input: 15.0,
output: 75.0,
cache_read: 1.50,
cache_write: 18.75,
});
}
if m.contains("claude-sonnet") || m.contains("sonnet-4") {
return Some(Pricing {
input: 3.0,
output: 15.0,
cache_read: 0.30,
cache_write: 3.75,
});
}
if m.contains("gpt-5.5-pro") {
return Some(Pricing {
input: 30.0,
output: 180.0,
cache_read: 0.0,
cache_write: 0.0,
});
}
if m.contains("gpt-5.5") {
return Some(Pricing {
input: 5.0,
output: 30.0,
cache_read: 0.0,
cache_write: 0.0,
});
}
if m.contains("gpt-5.4-nano") {
return Some(Pricing {
input: 0.20,
output: 1.25,
cache_read: 0.0,
cache_write: 0.0,
});
}
if m.contains("gpt-5.4-mini") {
return Some(Pricing {
input: 0.75,
output: 4.50,
cache_read: 0.0,
cache_write: 0.0,
});
}
if m.contains("gpt-5.4") {
return Some(Pricing {
input: 2.50,
output: 15.0,
cache_read: 0.0,
cache_write: 0.0,
});
}
if m.contains("gemini") {
return Some(if m.contains("flash-lite") {
Pricing {
input: 0.10,
output: 0.40,
cache_read: 0.0,
cache_write: 0.0,
}
} else if m.contains("flash") {
Pricing {
input: 0.30,
output: 2.50,
cache_read: 0.0,
cache_write: 0.0,
}
} else {
Pricing {
input: 1.25,
output: 10.0,
cache_read: 0.0,
cache_write: 0.0,
}
});
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn claude_family_rates() {
let haiku = pricing("anthropic/claude-haiku-4-5").expect("haiku priced");
assert_eq!(haiku.input, 0.80);
assert_eq!(haiku.output, 4.0);
let sonnet = pricing("bedrock/us.anthropic.claude-sonnet-4-6").expect("sonnet priced");
assert_eq!(sonnet.input, 3.0);
let opus = pricing("claude-opus-4-1").expect("opus priced");
assert_eq!(opus.output, 75.0);
}
#[test]
fn gpt5_and_gemini_rates() {
assert_eq!(pricing("openai/gpt-5.5-pro-20260423").unwrap().input, 30.0);
assert_eq!(pricing("openai/gpt-5.4-nano-20260317").unwrap().input, 0.20);
assert_eq!(pricing("openai/gpt-5.4-mini-20260317").unwrap().input, 0.75);
assert_eq!(pricing("openai/gpt-5.4-20260305").unwrap().input, 2.50);
assert_eq!(pricing("google/gemini-2.5-flash-lite").unwrap().input, 0.10);
assert_eq!(pricing("google/gemini-2.5-flash").unwrap().input, 0.30);
assert_eq!(pricing("google/gemini-2.5-pro").unwrap().input, 1.25);
}
#[test]
fn unknown_model_is_none() {
assert!(pricing("cohere/command-r-plus").is_none());
}
#[test]
fn estimate_matches_hand_calc() {
let p = Pricing {
input: 3.0,
output: 15.0,
cache_read: 0.30,
cache_write: 3.75,
};
let cost = p.estimate_usd(1_000_000, 1_000_000, 1_000_000, 1_000_000);
assert!((cost - 22.05).abs() < 1e-9, "cost was {cost}");
}
}