use std::collections::HashMap;
use std::sync::LazyLock;
use serde::Deserialize;
const PRICING_JSON: &str = include_str!("../schemas/pricing.json");
static PRICING: LazyLock<std::result::Result<PricingRegistry, String>> =
LazyLock::new(|| serde_json::from_str(PRICING_JSON).map_err(|e| e.to_string()));
fn pricing() -> Option<&'static PricingRegistry> {
PRICING.as_ref().ok()
}
#[derive(Debug, Deserialize)]
struct PricingRegistry {
models: HashMap<String, ModelPricing>,
}
#[derive(Debug, Clone, Default, Deserialize)]
#[cfg_attr(alef, alef(skip))]
pub struct ModelPricing {
pub input_cost_per_token: f64,
pub output_cost_per_token: f64,
#[serde(default)]
pub cache_read_input_token_cost: Option<f64>,
#[serde(default)]
pub cache_creation_input_token_cost: Option<f64>,
}
#[must_use]
pub fn completion_cost(model: &str, prompt_tokens: u64, completion_tokens: u64) -> Option<f64> {
completion_cost_with_cache(model, prompt_tokens, 0, completion_tokens)
}
#[must_use]
pub fn completion_cost_with_cache(
model: &str,
prompt_tokens: u64,
cached_tokens: u64,
completion_tokens: u64,
) -> Option<f64> {
let pricing = model_pricing(model)?;
let cached = cached_tokens.min(prompt_tokens);
let uncached = prompt_tokens - cached;
let cache_rate = pricing
.cache_read_input_token_cost
.unwrap_or(pricing.input_cost_per_token);
Some(
(uncached as f64) * pricing.input_cost_per_token
+ (cached as f64) * cache_rate
+ (completion_tokens as f64) * pricing.output_cost_per_token,
)
}
#[cfg_attr(alef, alef(skip))]
#[must_use]
pub fn model_pricing(model: &str) -> Option<&'static ModelPricing> {
let models = &pricing()?.models;
if let Some(p) = models.get(model) {
return Some(p);
}
let mut candidate = model;
while let Some(pos) = candidate.rfind(['-', '.']) {
candidate = &candidate[..pos];
if let Some(p) = models.get(candidate) {
return Some(p);
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn completion_cost_known_model_returns_expected_value() {
let cost = completion_cost("gpt-4", 100, 50).expect("gpt-4 must be in registry");
let expected = 100.0 * 0.00003 + 50.0 * 0.00006;
assert!((cost - expected).abs() < 1e-12, "expected {expected}, got {cost}");
}
#[test]
fn completion_cost_unknown_model_returns_none() {
assert!(
completion_cost("unknown-model-xyz", 100, 50).is_none(),
"unknown model should return None"
);
}
#[test]
fn completion_cost_gpt4o_matches_published_pricing() {
let cost = completion_cost("gpt-4o", 1_000, 500).expect("gpt-4o must be in registry");
let expected = 1_000.0 * 0.0000025 + 500.0 * 0.00001;
assert!((cost - expected).abs() < 1e-12, "expected {expected}, got {cost}");
}
#[test]
fn completion_cost_embedding_model_has_zero_output_cost() {
let cost =
completion_cost("text-embedding-3-small", 100, 0).expect("text-embedding-3-small must be in registry");
assert!(cost > 0.0, "input tokens must have a positive cost");
let pricing =
model_pricing("text-embedding-3-small").expect("text-embedding-3-small must be in pricing registry");
assert_eq!(pricing.output_cost_per_token, 0.0, "embedding output cost must be zero");
}
#[test]
fn model_pricing_returns_none_for_unknown_model() {
assert!(model_pricing("does-not-exist").is_none());
}
#[test]
fn model_pricing_prefix_fallback_matches_shorter_name() {
let exact = model_pricing("gpt-4").expect("gpt-4 must be in registry");
let prefix = model_pricing("gpt-4-0613").expect("gpt-4-0613 should match gpt-4 via prefix");
assert!(
(exact.input_cost_per_token - prefix.input_cost_per_token).abs() < 1e-15,
"prefix match should return the same pricing as exact match"
);
}
#[test]
fn completion_cost_prefix_fallback() {
let cost = completion_cost("gpt-4-0613", 100, 50);
assert!(cost.is_some(), "gpt-4-0613 should resolve via prefix fallback to gpt-4");
}
#[test]
fn model_pricing_returns_correct_fields_for_known_model() {
let p = model_pricing("gpt-4o-mini").expect("gpt-4o-mini must be in registry");
assert!(
(p.input_cost_per_token - 0.00000015).abs() < 1e-12,
"unexpected input_cost_per_token: {}",
p.input_cost_per_token
);
assert!(
(p.output_cost_per_token - 0.0000006).abs() < 1e-12,
"unexpected output_cost_per_token: {}",
p.output_cost_per_token
);
}
#[test]
fn completion_cost_with_cache_applies_discount_when_pricing_available() {
let pricing = ModelPricing {
input_cost_per_token: 1e-5,
output_cost_per_token: 2e-5,
cache_read_input_token_cost: Some(1e-6),
cache_creation_input_token_cost: None,
};
let expected = 800.0 * 1e-5 + 200.0 * 1e-6 + 50.0 * 2e-5;
let uncached = 1000 - 200;
let actual = (uncached as f64) * pricing.input_cost_per_token
+ 200.0
* pricing
.cache_read_input_token_cost
.expect("cache_read_input_token_cost should be set")
+ 50.0 * pricing.output_cost_per_token;
assert!((actual - expected).abs() < 1e-12);
}
#[test]
fn completion_cost_with_cache_falls_back_to_input_rate_without_cache_pricing() {
let with_cache = completion_cost_with_cache("gpt-4", 1_000, 200, 50).expect("gpt-4 must be in registry");
let without = completion_cost("gpt-4", 1_000, 50).expect("gpt-4 must be in registry");
assert!((with_cache - without).abs() < 1e-12);
}
#[test]
fn completion_cost_with_cache_clamps_cached_tokens_to_prompt_tokens() {
let cost = completion_cost_with_cache("gpt-4", 100, 500, 0).expect("gpt-4 must be in registry");
let clamped = completion_cost_with_cache("gpt-4", 100, 100, 0).expect("gpt-4 must be in registry");
assert!((cost - clamped).abs() < 1e-12);
}
#[test]
fn completion_cost_with_cache_uses_registry_cache_pricing_when_available() {
let pricing = model_pricing("claude-sonnet-4-5").expect("claude-sonnet-4-5 must be in registry");
let cache_rate = pricing
.cache_read_input_token_cost
.expect("claude-sonnet-4-5 must have cache_read_input_token_cost");
assert!(
cache_rate < pricing.input_cost_per_token,
"cache rate ({cache_rate}) must be cheaper than input rate ({})",
pricing.input_cost_per_token
);
let expected = 800.0 * pricing.input_cost_per_token + 200.0 * cache_rate + 50.0 * pricing.output_cost_per_token;
let actual = completion_cost_with_cache("claude-sonnet-4-5", 1_000, 200, 50)
.expect("claude-sonnet-4-5 must be priceable");
assert!((actual - expected).abs() < 1e-12, "expected {expected}, got {actual}");
let no_cache = completion_cost("claude-sonnet-4-5", 1_000, 50).expect("claude-sonnet-4-5 must be priceable");
assert!(
actual < no_cache,
"cached cost ({actual}) must be < uncached ({no_cache})"
);
}
#[test]
fn completion_cost_with_cache_unknown_model_returns_none() {
assert!(completion_cost_with_cache("unknown-model-xyz", 100, 10, 50).is_none());
}
#[test]
fn pricing_registry_embedded_json_is_valid() {
assert!(
PRICING.as_ref().is_ok(),
"embedded schemas/pricing.json failed to parse: {:?}",
PRICING.as_ref().err()
);
}
}