use crate::error::{Error, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct ModelPrice {
pub input_per_mtok: f64,
pub output_per_mtok: f64,
}
impl ModelPrice {
#[must_use]
pub fn cost(&self, input: u64, output: u64) -> f64 {
(input as f64 / 1e6) * self.input_per_mtok + (output as f64 / 1e6) * self.output_per_mtok
}
}
#[derive(Debug, Clone, Default)]
pub struct PriceTable {
prices: HashMap<String, ModelPrice>,
}
impl PriceTable {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn defaults() -> Self {
let mut prices = HashMap::new();
let mut put = |k: &str, i: f64, o: f64| {
prices.insert(
k.to_owned(),
ModelPrice {
input_per_mtok: i,
output_per_mtok: o,
},
);
};
put("anthropic/claude-haiku-4-5", 1.0, 5.0);
put("anthropic/claude-sonnet-5", 3.0, 15.0);
put("anthropic/claude-opus-4-8", 15.0, 75.0);
put("openai/gpt-4.1-mini", 0.4, 1.6);
put("openai/gpt-5.5", 5.0, 15.0);
put("google/gemini-3.1-flash", 0.35, 1.05);
put("google/gemini-3.1-pro", 3.5, 10.5);
Self { prices }
}
#[must_use]
pub fn with_override(mut self, model: impl Into<String>, price: ModelPrice) -> Self {
self.prices.insert(model.into(), price);
self
}
#[must_use]
pub fn get(&self, model: &str) -> Option<ModelPrice> {
self.prices.get(model).copied()
}
pub fn cost_usd(&self, model: &str, input: u64, output: u64) -> Result<f64> {
self.get(model)
.map(|p| p.cost(input, output))
.ok_or_else(|| Error::UnknownModel(model.to_owned()))
}
pub fn baseline_usd(&self, top_model: &str, input: u64, output: u64) -> Result<f64> {
self.cost_usd(top_model, input, output)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cost_math_is_correct() {
let p = ModelPrice {
input_per_mtok: 3.0,
output_per_mtok: 15.0,
};
assert!((p.cost(1000, 500) - 0.0105).abs() < 1e-12);
assert_eq!(p.cost(0, 0), 0.0);
}
#[test]
fn table_lookup_and_unknown_model() {
let t = PriceTable::defaults();
assert!(t.cost_usd("anthropic/claude-haiku-4-5", 1000, 1000).is_ok());
match t.cost_usd("acme/nope", 1, 1) {
Err(Error::UnknownModel(m)) => assert_eq!(m, "acme/nope"),
other => panic!("expected UnknownModel, got {other:?}"),
}
}
#[test]
fn baseline_exceeds_cheap_rung_for_same_tokens() {
let t = PriceTable::defaults();
let (i, o) = (2000, 800);
let cheap = t.cost_usd("anthropic/claude-haiku-4-5", i, o).unwrap();
let baseline = t.baseline_usd("anthropic/claude-opus-4-8", i, o).unwrap();
assert!(baseline > cheap);
assert!(baseline - cheap > 0.0); }
#[test]
fn overrides_win() {
let t = PriceTable::new().with_override(
"x/y",
ModelPrice {
input_per_mtok: 2.0,
output_per_mtok: 2.0,
},
);
assert_eq!(t.cost_usd("x/y", 1_000_000, 0).unwrap(), 2.0);
}
}