use std::collections::BTreeMap;
use crate::provider::Usage;
use crate::state::ProviderCall;
pub const MICROS_PER_UNIT: u64 = 1_000_000;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct Price {
pub input: u64,
pub output: u64,
pub cache_read: u64,
pub cache_write: u64,
pub per_server_tool_request: u64,
}
impl Price {
pub const ZERO: Self = Self {
input: 0,
output: 0,
cache_read: 0,
cache_write: 0,
per_server_tool_request: 0,
};
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PriceTier {
pub min_prompt_tokens: u64,
pub price: Price,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct PriceTable {
as_of: String,
prices: BTreeMap<String, Price>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
tiers: BTreeMap<String, Vec<PriceTier>>,
}
impl PriceTable {
pub fn new(as_of: impl Into<String>) -> Self {
Self {
as_of: as_of.into(),
prices: BTreeMap::new(),
tiers: BTreeMap::new(),
}
}
#[must_use]
pub fn with(mut self, model: impl Into<String>, price: Price) -> Self {
self.prices.insert(model.into(), price);
self
}
#[must_use]
pub fn with_tiers(mut self, model: impl Into<String>, mut tiers: Vec<PriceTier>) -> Self {
let model = model.into();
if tiers.is_empty() {
self.tiers.remove(&model);
return self;
}
tiers.sort_by_key(|t| t.min_prompt_tokens);
self.tiers.insert(model, tiers);
self
}
pub fn as_of(&self) -> &str {
&self.as_of
}
pub fn price(&self, model: &str) -> Option<Price> {
self.prices.get(model).copied()
}
pub fn tiers(&self, model: &str) -> &[PriceTier] {
self.tiers.get(model).map_or(&[], |t| t.as_slice())
}
fn rate(&self, model: &str, prompt_tokens: u64) -> Option<Price> {
let base = self.price(model)?;
Some(
self.tiers(model)
.iter()
.rfind(|t| prompt_tokens >= t.min_prompt_tokens)
.map_or(base, |t| t.price),
)
}
pub fn cost_micros(&self, model: &str, usage: &Usage) -> Option<u64> {
let p = self.rate(model, usage.prompt_tokens)?;
let fresh_input = usage
.prompt_tokens
.saturating_sub(usage.cache_read_tokens)
.saturating_sub(usage.cache_write_tokens);
let per_million = |tokens: u64, price: u64| tokens as u128 * price as u128;
let mtok = per_million(fresh_input, p.input)
+ per_million(usage.completion_tokens, p.output)
+ per_million(usage.cache_read_tokens, p.cache_read)
+ per_million(usage.cache_write_tokens, p.cache_write);
let requests = usage.server_tool_requests as u128 * p.per_server_tool_request as u128;
let micros = (mtok + 500_000) / 1_000_000 + requests;
Some(micros.min(u64::MAX as u128) as u64)
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Spend {
pub key: String,
pub calls: u64,
pub usage: Usage,
pub cost_micros: u64,
pub unpriced_calls: u64,
}
pub(crate) fn group(key: impl Into<String>, calls: &[&ProviderCall], prices: &PriceTable) -> Spend {
let mut spend = Spend {
key: key.into(),
calls: calls.len() as u64,
..Default::default()
};
for call in calls {
let Some(usage) = call.usage else {
spend.unpriced_calls += 1;
continue;
};
spend.usage.prompt_tokens += usage.prompt_tokens;
spend.usage.completion_tokens += usage.completion_tokens;
spend.usage.total_tokens += usage.total_tokens;
spend.usage.cache_read_tokens += usage.cache_read_tokens;
spend.usage.cache_write_tokens += usage.cache_write_tokens;
spend.usage.reasoning_tokens += usage.reasoning_tokens;
spend.usage.server_tool_requests += usage.server_tool_requests;
match call
.model
.as_deref()
.and_then(|m| prices.cost_micros(m, &usage))
{
Some(micros) => spend.cost_micros += micros,
None => spend.unpriced_calls += 1,
}
}
spend
}
#[cfg(test)]
mod tests {
use super::*;
fn table() -> PriceTable {
PriceTable::new("2026-07-29").with(
"m",
Price {
input: 3_000_000,
output: 15_000_000,
cache_read: 300_000,
cache_write: 3_750_000,
per_server_tool_request: 10_000,
},
)
}
#[test]
fn a_hand_computed_million_token_figure_comes_out_exact() {
let usage = Usage {
prompt_tokens: 1_000_000,
completion_tokens: 1_000_000,
total_tokens: 2_000_000,
cache_read_tokens: 500_000,
cache_write_tokens: 100_000,
reasoning_tokens: 200_000,
server_tool_requests: 3,
};
assert_eq!(
table().cost_micros("m", &usage),
Some(1_200_000 + 15_000_000 + 150_000 + 375_000 + 30_000)
);
}
#[test]
fn an_unpriced_model_is_unknown_rather_than_free() {
let usage = Usage {
prompt_tokens: 10,
completion_tokens: 10,
total_tokens: 20,
..Default::default()
};
assert_eq!(table().cost_micros("not-in-the-table", &usage), None);
assert_eq!(table().cost_micros("m", &usage), Some(180));
}
#[test]
fn cache_figures_larger_than_the_prompt_saturate_rather_than_underflow() {
let usage = Usage {
prompt_tokens: 10,
cache_read_tokens: 900,
total_tokens: 10,
..Default::default()
};
assert_eq!(table().cost_micros("m", &usage), Some(270));
}
fn tiered() -> PriceTable {
PriceTable::new("2026-08-01")
.with(
"long",
Price {
input: 1_250_000,
..Price::ZERO
},
)
.with_tiers(
"long",
vec![PriceTier {
min_prompt_tokens: 200_000,
price: Price {
input: 2_500_000,
..Price::ZERO
},
}],
)
}
fn prompt(tokens: u64) -> Usage {
Usage {
prompt_tokens: tokens,
total_tokens: tokens,
..Default::default()
}
}
#[test]
fn the_tier_boundary_is_exact_on_both_sides() {
let t = tiered();
assert_eq!(t.cost_micros("long", &prompt(199_999)), Some(249_999));
assert_eq!(t.cost_micros("long", &prompt(200_000)), Some(500_000));
assert_eq!(t.cost_micros("long", &prompt(400_000)), Some(1_000_000));
}
#[test]
fn a_prompt_below_every_floor_gets_the_base_row_not_the_lowest_tier() {
assert_eq!(tiered().cost_micros("long", &prompt(1_000)), Some(1_250));
}
#[test]
fn the_highest_reached_floor_wins_whatever_order_the_tiers_were_written_in() {
let t = PriceTable::new("x")
.with(
"two",
Price {
input: 1_000_000,
..Price::ZERO
},
)
.with_tiers(
"two",
vec![
PriceTier {
min_prompt_tokens: 128_000,
price: Price {
input: 3_000_000,
..Price::ZERO
},
},
PriceTier {
min_prompt_tokens: 32_000,
price: Price {
input: 2_000_000,
..Price::ZERO
},
},
],
);
assert_eq!(t.cost_micros("two", &prompt(1_000_000)), Some(3_000_000));
assert_eq!(t.cost_micros("two", &prompt(64_000)), Some(128_000));
assert_eq!(t.cost_micros("two", &prompt(1_000)), Some(1_000));
assert_eq!(
t.tiers("two")
.iter()
.map(|x| x.min_prompt_tokens)
.collect::<Vec<_>>(),
vec![32_000, 128_000],
"stored lowest-first however they arrived"
);
}
#[test]
fn the_same_usage_costs_strictly_less_once_the_tiers_are_removed() {
let long = prompt(400_000);
let with = tiered().cost_micros("long", &long).unwrap();
let without = tiered()
.with_tiers("long", Vec::new())
.cost_micros("long", &long)
.unwrap();
assert!(
without < with,
"removing the tier must make the same request cheaper: {without} vs {with}"
);
assert_eq!(without, 500_000);
}
#[test]
fn a_table_with_no_tiers_prices_exactly_as_it_did_before_they_existed() {
let usage = Usage {
prompt_tokens: 1_000_000,
completion_tokens: 1_000_000,
total_tokens: 2_000_000,
cache_read_tokens: 500_000,
cache_write_tokens: 100_000,
reasoning_tokens: 200_000,
server_tool_requests: 3,
};
assert!(table().tiers("m").is_empty());
assert_eq!(
table().cost_micros("m", &usage),
Some(1_200_000 + 15_000_000 + 150_000 + 375_000 + 30_000)
);
}
#[test]
fn a_tier_on_a_model_with_no_base_price_is_still_unpriced() {
let t = PriceTable::new("x").with_tiers(
"orphan",
vec![PriceTier {
min_prompt_tokens: 1,
price: Price {
input: 9_000_000,
..Price::ZERO
},
}],
);
assert_eq!(t.cost_micros("orphan", &prompt(1_000)), None);
}
#[test]
fn rounding_is_once_at_the_end_and_half_up() {
let half = PriceTable::new("x").with(
"m",
Price {
input: 1,
..Price::ZERO
},
);
let usage = Usage {
prompt_tokens: 500_000,
total_tokens: 500_000,
..Default::default()
};
assert_eq!(half.cost_micros("m", &usage), Some(1));
let under = Usage {
prompt_tokens: 499_999,
total_tokens: 499_999,
..Default::default()
};
assert_eq!(half.cost_micros("m", &under), Some(0));
}
}