use crate::cost::usage::{Usage, UsageCoverage};
use crate::model::ModelId;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
pub struct Micros(pub i64);
impl Micros {
pub fn parse(text: &str) -> std::result::Result<Micros, String> {
let text = text.trim();
let (negative, digits) = match text.strip_prefix('-') {
Some(rest) => (true, rest),
None => (false, text),
};
let (whole, fraction) = match digits.split_once('.') {
Some((w, f)) => (w, f),
None => (digits, ""),
};
if whole.is_empty() || !whole.chars().all(|c| c.is_ascii_digit()) {
return Err(format!("{text:?} is not a decimal number"));
}
if !fraction.chars().all(|c| c.is_ascii_digit()) {
return Err(format!("{text:?} is not a decimal number"));
}
if fraction.len() > 6 {
return Err(format!(
"{text:?} has more than six decimal places. A price written to seven is one \
copied from a different unit, and rounding it would look correct"
));
}
let whole: i64 = whole
.parse()
.map_err(|_| format!("{text:?} is too large to hold"))?;
let mut padded = fraction.to_string();
while padded.len() < 6 {
padded.push('0');
}
let fraction: i64 = if padded.is_empty() {
0
} else {
padded
.parse()
.map_err(|_| format!("{text:?} is not a decimal number"))?
};
let total = whole
.checked_mul(1_000_000)
.and_then(|w| w.checked_add(fraction))
.ok_or_else(|| format!("{text:?} is too large to hold"))?;
Ok(Micros(if negative { -total } else { total }))
}
pub fn exact(self) -> String {
let sign = if self.0 < 0 { "-" } else { "" };
let n = self.0.unsigned_abs();
format!("{sign}{}.{:06}", n / 1_000_000, n % 1_000_000)
}
}
impl Serialize for Micros {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&self.exact())
}
}
impl<'de> Deserialize<'de> for Micros {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
use serde::de::Error;
let text = String::deserialize(deserializer)?;
Micros::parse(&text).map_err(D::Error::custom)
}
}
impl std::fmt::Display for Micros {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.exact())
}
}
impl std::ops::Add for Micros {
type Output = Micros;
fn add(self, other: Micros) -> Micros {
Micros(self.0.saturating_add(other.0))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Rate {
pub input: Micros,
pub cache_read: Micros,
pub cache_write: Micros,
pub output: Micros,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct PriceBook {
pub id: String,
pub provider: String,
pub effective_from: String,
pub source: String,
pub verified_at: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub expires_on: Option<String>,
pub currency: String,
#[serde(default, rename = "price", with = "rows")]
pub rates: BTreeMap<String, Rate>,
}
mod rows {
use super::{Micros, Rate};
use serde::de::Error;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::collections::BTreeMap;
#[derive(Serialize, Deserialize)]
struct Row {
model: String,
input: Micros,
output: Micros,
#[serde(default)]
cache_read: Micros,
#[serde(default)]
cache_write: Micros,
}
pub fn serialize<S: Serializer>(
rates: &BTreeMap<String, Rate>,
serializer: S,
) -> Result<S::Ok, S::Error> {
rates
.iter()
.map(|(model, rate)| Row {
model: model.clone(),
input: rate.input,
output: rate.output,
cache_read: rate.cache_read,
cache_write: rate.cache_write,
})
.collect::<Vec<_>>()
.serialize(serializer)
}
pub fn deserialize<'de, D: Deserializer<'de>>(
deserializer: D,
) -> Result<BTreeMap<String, Rate>, D::Error> {
let rows = Vec::<Row>::deserialize(deserializer)?;
let mut rates = BTreeMap::new();
for row in rows {
if rates.contains_key(&row.model) {
return Err(D::Error::custom(format!(
"{} is priced twice. One row would silently overrule the other",
row.model
)));
}
rates.insert(
row.model,
Rate {
input: row.input,
output: row.output,
cache_read: row.cache_read,
cache_write: row.cache_write,
},
);
}
Ok(rates)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct Priced {
pub amount: Micros,
pub currency: String,
pub book: String,
pub coverage: UsageCoverage,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum Recheck {
Expired {
on: String,
days_ago: i64,
},
Aged {
days: i64,
},
Undatable {
field: &'static str,
},
}
impl std::fmt::Display for Recheck {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Recheck::Expired { on, days_ago } => {
write!(f, "expired on {on}, {days_ago} days ago")
}
Recheck::Aged { days } => write!(f, "last checked {days} days ago"),
Recheck::Undatable { field } => write!(f, "{field} is not a date this can read"),
}
}
}
fn day_number(text: &str) -> Option<i64> {
let mut parts = text.trim().splitn(3, '-');
let year: i64 = parts.next()?.parse().ok()?;
let month: i64 = parts.next()?.parse().ok()?;
let day: i64 = parts.next()?.parse().ok()?;
if !(1..=12).contains(&month) || !(1..=31).contains(&day) {
return None;
}
let year = if month <= 2 { year - 1 } else { year };
let era = if year >= 0 { year } else { year - 399 } / 400;
let year_of_era = year - era * 400;
let shifted = (month + 9) % 12;
let day_of_year = (153 * shifted + 2) / 5 + day - 1;
let day_of_era = year_of_era * 365 + year_of_era / 4 - year_of_era / 100 + day_of_year;
Some(era * 146_097 + day_of_era - 719_468)
}
impl PriceBook {
pub const RECHECK_AFTER_DAYS: i64 = 90;
pub fn age(&self, today: &str) -> Option<i64> {
Some(day_number(today)? - day_number(&self.verified_at)?)
}
pub fn needs_rechecking(&self, today: &str) -> Option<Recheck> {
let Some(now) = day_number(today) else {
return Some(Recheck::Undatable { field: "today" });
};
if let Some(expires) = &self.expires_on {
let Some(end) = day_number(expires) else {
return Some(Recheck::Undatable {
field: "expires_on",
});
};
if now > end {
return Some(Recheck::Expired {
on: expires.clone(),
days_ago: now - end,
});
}
}
let Some(checked) = day_number(&self.verified_at) else {
return Some(Recheck::Undatable {
field: "verified_at",
});
};
let days = now - checked;
(days >= Self::RECHECK_AFTER_DAYS).then_some(Recheck::Aged { days })
}
pub fn parse(text: &str) -> std::result::Result<PriceBook, String> {
let book: PriceBook = toml::from_str(text).map_err(|e| e.to_string())?;
for (field, value) in [
("id", &book.id),
("provider", &book.provider),
("effective_from", &book.effective_from),
("source", &book.source),
("verified_at", &book.verified_at),
("currency", &book.currency),
] {
if value.trim().is_empty() {
return Err(format!(
"{field} is blank. A price nobody can date or trace is a number nobody \
can check"
));
}
}
Ok(book)
}
pub fn rate(&self, model: &ModelId) -> Option<&Rate> {
self.rates.get(model.as_str())
}
pub fn price(&self, model: &ModelId, usage: &Usage) -> Option<Priced> {
let rate = self.rate(model)?;
if usage.coverage() == UsageCoverage::Absent {
return None;
}
let part = |tokens: Option<u64>, per_million: Micros| -> i64 {
let tokens = i64::try_from(tokens.unwrap_or(0)).unwrap_or(i64::MAX);
tokens.saturating_mul(per_million.0) / 1_000_000
};
let amount = part(usage.input_tokens, rate.input)
.saturating_add(part(usage.cache_read_tokens, rate.cache_read))
.saturating_add(part(usage.cache_write_tokens, rate.cache_write))
.saturating_add(part(usage.output_tokens, rate.output));
Some(Priced {
amount: Micros(amount),
currency: self.currency.clone(),
book: self.id.clone(),
coverage: usage.coverage(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn dated(verified_at: &str, expires_on: Option<&str>) -> PriceBook {
PriceBook {
id: "b".into(),
provider: "p".into(),
effective_from: "2026-01-01".into(),
source: "a published page".into(),
verified_at: verified_at.into(),
expires_on: expires_on.map(Into::into),
currency: "USD".into(),
rates: BTreeMap::new(),
}
}
#[test]
fn a_day_number_counts_from_the_epoch_and_gets_the_leap_years_right() {
assert_eq!(day_number("1970-01-01"), Some(0));
assert_eq!(day_number("1970-01-02"), Some(1));
assert_eq!(
day_number("2000-03-01"),
day_number("2000-02-29").map(|d| d + 1)
);
assert_eq!(
day_number("2100-03-01"),
day_number("2100-02-28").map(|d| d + 1)
);
assert_eq!(day_number("1969-12-31"), Some(-1));
}
#[test]
fn a_date_that_is_not_one_is_refused_rather_than_read_as_zero() {
assert_eq!(day_number("soon"), None);
assert_eq!(day_number("2026-13-01"), None);
assert_eq!(day_number("2026-00-10"), None);
assert_eq!(day_number("2026-08"), None);
assert_eq!(day_number(""), None);
}
#[test]
fn a_books_age_is_days_since_a_person_last_checked_it() {
let book = dated("2026-08-01", None);
assert_eq!(book.age("2026-08-31"), Some(30));
assert_eq!(book.age("2026-08-01"), Some(0));
assert_eq!(
book.age("2026-07-31"),
Some(-1),
"a table dated in the future is one somebody typed wrong, and saying so beats clamping it to zero"
);
}
#[test]
fn a_book_nobody_has_checked_in_a_quarter_asks_to_be_checked() {
let book = dated("2026-08-01", None);
assert_eq!(book.needs_rechecking("2026-09-01"), None);
assert_eq!(
book.needs_rechecking("2026-10-30"),
Some(Recheck::Aged { days: 90 }),
"the rule is >= RECHECK_AFTER_DAYS, and the boundary is part of the rule"
);
}
#[test]
fn an_expiry_the_book_announced_wins_over_its_age() {
let book = dated("2026-08-01", Some("2026-12-31"));
assert_eq!(
book.needs_rechecking("2027-01-02"),
Some(Recheck::Expired {
on: "2026-12-31".into(),
days_ago: 2
})
);
assert_eq!(
book.needs_rechecking("2026-12-31"),
Some(Recheck::Aged { days: 152 }),
"on the last good day it has not expired, though it has certainly aged"
);
}
#[test]
fn a_book_whose_date_cannot_be_read_reports_that_rather_than_ageing_forever() {
assert_eq!(
dated("recently", None).needs_rechecking("2026-08-31"),
Some(Recheck::Undatable {
field: "verified_at"
})
);
assert_eq!(
dated("2026-08-01", Some("when the contract ends")).needs_rechecking("2026-08-02"),
Some(Recheck::Undatable {
field: "expires_on"
})
);
assert_eq!(
dated("2026-08-01", None).needs_rechecking("today"),
Some(Recheck::Undatable { field: "today" })
);
}
fn book() -> PriceBook {
let mut rates = BTreeMap::new();
rates.insert(
"test-model".to_string(),
Rate {
input: Micros(3_000_000),
cache_read: Micros(300_000),
cache_write: Micros(3_750_000),
output: Micros(15_000_000),
},
);
PriceBook {
id: "test-2026-08".into(),
provider: "test".into(),
effective_from: "2026-08-01".into(),
source: "docs".into(),
verified_at: "2026-08-28".into(),
expires_on: None,
currency: "USD".into(),
rates,
}
}
#[test]
fn a_call_the_provider_did_not_measure_has_no_price() {
assert_eq!(book().price(&"test-model".into(), &Usage::absent()), None);
}
#[test]
fn a_model_this_book_does_not_list_has_no_price() {
let usage = Usage {
output_tokens: Some(1_000),
..Usage::absent()
};
assert_eq!(book().price(&"some-other-model".into(), &usage), None);
}
#[test]
fn a_priced_call_names_the_book_that_priced_it() {
let usage = Usage {
input_tokens: Some(1_000_000),
cache_read_tokens: Some(0),
cache_write_tokens: Some(0),
output_tokens: Some(1_000_000),
estimated: false,
};
let priced = book()
.price(&"test-model".into(), &usage)
.unwrap_or(Priced {
amount: Micros(0),
currency: "none".into(),
book: "none".into(),
coverage: UsageCoverage::Absent,
});
assert_eq!(priced.amount, Micros(18_000_000));
assert_eq!(priced.exact_for_test(), "18.000000");
assert_eq!(priced.book, "test-2026-08");
assert_eq!(priced.coverage, UsageCoverage::Exact);
}
#[test]
fn a_priced_call_says_what_the_amount_is_denominated_in() {
let usage = Usage {
output_tokens: Some(1_000_000),
..Usage::absent()
};
assert_eq!(
book()
.price(&"test-model".into(), &usage)
.map(|p| p.currency),
Some("USD".to_string())
);
}
#[test]
fn a_cost_from_partial_usage_says_it_is_partial() {
let usage = Usage {
output_tokens: Some(1_000_000),
..Usage::absent()
};
let priced = book().price(&"test-model".into(), &usage);
assert_eq!(
priced.map(|p| p.coverage),
Some(UsageCoverage::Partial),
"a total built from partial usage understates the bill and has to say so"
);
}
#[test]
fn a_book_with_no_source_is_refused() {
let refused = PriceBook::parse(
"id = \"x\"\nprovider = \"p\"\neffective_from = \"2026-01-01\"\n\
source = \"\"\nverified_at = \"2026-01-01\"\ncurrency = \"USD\"\n",
);
assert!(refused.is_err());
}
#[test]
fn money_is_written_to_six_places_rather_than_two() {
assert_eq!(Micros(1_234).exact(), "0.001234");
assert_eq!(Micros(-2_500_000).exact(), "-2.500000");
}
impl Priced {
fn exact_for_test(&self) -> String {
self.amount.exact()
}
}
}