use serde::Deserialize;
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, Deserialize)]
pub struct ModelPrice {
pub input: f64,
pub output: f64,
}
#[derive(Debug, Clone, Default)]
pub struct PricingTable {
prices: HashMap<String, ModelPrice>,
}
impl PricingTable {
pub fn from_toml_str(s: &str) -> anyhow::Result<Self> {
let prices: HashMap<String, ModelPrice> = toml::from_str(s)?;
Ok(Self { prices })
}
pub fn cost_usd(
&self,
provider: &str,
model: &str,
input_tokens: u64,
output_tokens: u64,
) -> f64 {
self.prices
.get(&format!("{provider}:{model}"))
.map(|p| {
(input_tokens as f64 * p.input + output_tokens as f64 * p.output) / 1_000_000.0
})
.unwrap_or(0.0)
}
pub fn embedding_cost_usd(
&self,
provider: &str,
model: &str,
input_tokens: u64,
default_input_per_mtok: f64,
) -> f64 {
let per_mtok = self
.prices
.get(&format!("{provider}:{model}"))
.map(|p| p.input)
.unwrap_or(default_input_per_mtok);
input_tokens as f64 * per_mtok / 1_000_000.0
}
}
#[cfg(test)]
mod tests {
use super::*;
const SAMPLE: &str = r#"
["vertex:gemini-3-pro"]
input = 1.25
output = 5.0
"#;
#[test]
fn computes_cost_from_tokens() {
let t = PricingTable::from_toml_str(SAMPLE).unwrap();
let c = t.cost_usd("vertex", "gemini-3-pro", 1_000_000, 1_000_000);
assert!((c - 6.25).abs() < 1e-9, "got {c}");
}
#[test]
fn unknown_model_is_free_not_error() {
let t = PricingTable::from_toml_str(SAMPLE).unwrap();
assert_eq!(t.cost_usd("oai_compat", "qwen-local", 1000, 1000), 0.0);
}
#[test]
fn embedding_uses_key_when_present_else_default() {
let t = PricingTable::from_toml_str(
"[\"vertex:text-embedding-004\"]\ninput = 0.025\noutput = 0.0\n",
)
.unwrap();
assert!(
(t.embedding_cost_usd("vertex", "text-embedding-004", 1_000_000, 0.10) - 0.025).abs()
< 1e-9
);
assert!(
(t.embedding_cost_usd("openai", "text-embedding-3-large", 1_000_000, 0.10) - 0.10)
.abs()
< 1e-9
);
}
}