use serde_json::{Map, Value};
use crate::budgets::BudgetSpend;
pub const MICRO_PER_USD: u64 = 1_000_000;
pub const MICRO_PER_CENT: u64 = 10_000;
const TOKENS_PER_QUOTE: u64 = 1_000_000;
const MICRO_PER_USD_F: f64 = 1_000_000.0;
const RATES: [&str; 4] = ["input", "output", "cache_read", "cache_write"];
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct Price {
pub input: u64,
pub output: u64,
pub cache_read: u64,
pub cache_write: u64,
}
impl Price {
pub fn cost(&self, spend: BudgetSpend) -> u64 {
rate(spend.uncached_prompt_tokens(), self.input)
.saturating_add(rate(spend.output_tokens, self.output))
.saturating_add(rate(spend.cache_read_tokens, self.cache_read))
.saturating_add(rate(spend.cache_write_tokens, self.cache_write))
}
pub(super) fn from_json(value: &Value) -> Option<Self> {
let row = value.as_object()?;
let micros = |key: &str| quoted(row.get(key)).unwrap_or(0);
Some(Self {
input: micros("input"),
output: micros("output"),
cache_read: micros("cache_read"),
cache_write: micros("cache_write"),
})
}
pub fn json_fields(&self) -> Map<String, Value> {
RATES
.iter()
.zip([self.input, self.output, self.cache_read, self.cache_write])
.map(|(key, micro)| ((*key).to_owned(), decimal(micro)))
.collect()
}
}
fn rate(tokens: u64, per_quote: u64) -> u64 {
tokens.saturating_mul(per_quote) / TOKENS_PER_QUOTE
}
pub(crate) fn quoted(value: Option<&Value>) -> Option<u64> {
let usd = value.and_then(Value::as_f64).filter(|usd| *usd >= 0.0)?;
Some((usd * MICRO_PER_USD_F).round() as u64)
}
pub(crate) fn parse_usd(word: &str) -> Option<u64> {
quoted(Some(&Value::from(word.parse::<f64>().ok()?)))
}
pub(crate) fn decimal(micro: u64) -> Value {
let (whole, frac) = (micro / MICRO_PER_USD, micro % MICRO_PER_USD);
let text = if frac == 0 {
whole.to_string()
} else {
format!("{whole}.{frac:06}")
.trim_end_matches('0')
.to_owned()
};
serde_json::from_str(&text).unwrap_or_default()
}