use std::collections::{BTreeMap, HashMap};
use std::sync::LazyLock;
use serde::{Deserialize, Serialize};
const CATALOG_JSON: &str = include_str!("../schemas/catalog.json");
const PRIMARY_PROVIDERS: [&str; 3] = ["anthropic", "google", "openai"];
static PRICING: LazyLock<std::result::Result<HashMap<String, ModelPricing>, String>> =
LazyLock::new(|| registry_from_catalog_str(CATALOG_JSON));
fn registry() -> Option<&'static HashMap<String, ModelPricing>> {
PRICING.as_ref().ok()
}
#[derive(Debug, Deserialize)]
struct CatalogFile {
providers: BTreeMap<String, CatalogProviderRow>,
}
#[derive(Debug, Deserialize)]
struct CatalogProviderRow {
#[serde(default)]
models: BTreeMap<String, CatalogModelRow>,
}
#[derive(Debug, Deserialize)]
struct CatalogModelRow {
#[serde(default)]
pricing: Option<CatalogPricingRow>,
limit: CatalogLimitRow,
#[serde(default)]
mode: Option<String>,
capabilities: CatalogCapabilitiesRow,
}
#[derive(Debug, Deserialize)]
struct CatalogPricingRow {
input_cost_per_token: f64,
output_cost_per_token: f64,
#[serde(default)]
cache_read_input_token_cost: Option<f64>,
#[serde(default)]
cache_creation_input_token_cost: Option<f64>,
#[serde(default)]
input_cost_per_audio_token: Option<f64>,
#[serde(default)]
output_cost_per_audio_token: Option<f64>,
#[serde(default)]
output_cost_per_reasoning_token: Option<f64>,
#[serde(default)]
tiers: Vec<CatalogPricingTierRow>,
}
#[derive(Debug, Deserialize)]
struct CatalogPricingTierRow {
min_context_tokens: u64,
input_cost_per_token: f64,
output_cost_per_token: f64,
#[serde(default)]
cache_read_input_token_cost: Option<f64>,
#[serde(default)]
cache_creation_input_token_cost: Option<f64>,
#[serde(default)]
input_cost_per_audio_token: Option<f64>,
#[serde(default)]
output_cost_per_audio_token: Option<f64>,
#[serde(default)]
output_cost_per_reasoning_token: Option<f64>,
}
#[derive(Debug, Deserialize)]
struct CatalogLimitRow {
context: u64,
#[serde(default)]
input: Option<u64>,
output: u64,
}
#[derive(Debug, Deserialize)]
struct CatalogCapabilitiesRow {
vision: bool,
function_calling: bool,
reasoning: bool,
structured_output: bool,
audio_input: bool,
audio_output: bool,
prompt_caching: bool,
}
impl From<&CatalogPricingTierRow> for PricingTier {
fn from(row: &CatalogPricingTierRow) -> Self {
PricingTier {
min_context_tokens: row.min_context_tokens,
input_cost_per_token: row.input_cost_per_token,
output_cost_per_token: row.output_cost_per_token,
cache_read_input_token_cost: row.cache_read_input_token_cost,
cache_creation_input_token_cost: row.cache_creation_input_token_cost,
input_cost_per_audio_token: row.input_cost_per_audio_token,
output_cost_per_audio_token: row.output_cost_per_audio_token,
output_cost_per_reasoning_token: row.output_cost_per_reasoning_token,
}
}
}
fn flatten_model(model: &CatalogModelRow) -> ModelPricing {
let (
input_cost_per_token,
output_cost_per_token,
cache_read_input_token_cost,
cache_creation_input_token_cost,
input_cost_per_audio_token,
output_cost_per_audio_token,
output_cost_per_reasoning_token,
tiers,
) = match &model.pricing {
Some(pricing) => (
pricing.input_cost_per_token,
pricing.output_cost_per_token,
pricing.cache_read_input_token_cost,
pricing.cache_creation_input_token_cost,
pricing.input_cost_per_audio_token,
pricing.output_cost_per_audio_token,
pricing.output_cost_per_reasoning_token,
pricing.tiers.iter().map(PricingTier::from).collect(),
),
None => (0.0, 0.0, None, None, None, None, None, Vec::new()),
};
ModelPricing {
input_cost_per_token,
output_cost_per_token,
cache_read_input_token_cost,
cache_creation_input_token_cost,
input_cost_per_audio_token,
output_cost_per_audio_token,
output_cost_per_reasoning_token,
max_tokens: Some(model.limit.context),
max_input_tokens: Some(model.limit.input.unwrap_or(model.limit.context)),
max_output_tokens: Some(model.limit.output),
mode: model.mode.clone(),
supports_vision: Some(model.capabilities.vision),
supports_function_calling: Some(model.capabilities.function_calling),
supports_reasoning: Some(model.capabilities.reasoning),
supports_structured_output: Some(model.capabilities.structured_output),
supports_audio_input: Some(model.capabilities.audio_input),
supports_audio_output: Some(model.capabilities.audio_output),
supports_prompt_caching: Some(model.capabilities.prompt_caching),
tiers,
}
}
fn registry_from_catalog_str(catalog_json: &str) -> std::result::Result<HashMap<String, ModelPricing>, String> {
let catalog: CatalogFile = serde_json::from_str(catalog_json).map_err(|e| e.to_string())?;
let mut registry = HashMap::new();
for (provider_id, provider) in &catalog.providers {
for (model_id, model) in &provider.models {
let pricing = flatten_model(model);
if PRIMARY_PROVIDERS.contains(&provider_id.as_str()) {
registry.entry(model_id.clone()).or_insert_with(|| pricing.clone());
}
registry.insert(format!("{provider_id}/{model_id}"), pricing);
}
}
Ok(registry)
}
#[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>,
#[serde(default)]
pub input_cost_per_audio_token: Option<f64>,
#[serde(default)]
pub output_cost_per_audio_token: Option<f64>,
#[serde(default)]
pub output_cost_per_reasoning_token: Option<f64>,
#[serde(default)]
pub max_tokens: Option<u64>,
#[serde(default)]
pub max_input_tokens: Option<u64>,
#[serde(default)]
pub max_output_tokens: Option<u64>,
#[serde(default)]
pub mode: Option<String>,
#[serde(default)]
pub supports_vision: Option<bool>,
#[serde(default)]
pub supports_function_calling: Option<bool>,
#[serde(default)]
pub supports_reasoning: Option<bool>,
#[serde(default)]
pub supports_structured_output: Option<bool>,
#[serde(default)]
pub supports_audio_input: Option<bool>,
#[serde(default)]
pub supports_audio_output: Option<bool>,
#[serde(default)]
pub supports_prompt_caching: Option<bool>,
#[serde(default)]
pub tiers: Vec<PricingTier>,
}
#[derive(Debug, Clone, Default, Deserialize)]
#[cfg_attr(alef, alef(skip))]
pub struct PricingTier {
pub min_context_tokens: u64,
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>,
#[serde(default)]
pub input_cost_per_audio_token: Option<f64>,
#[serde(default)]
pub output_cost_per_audio_token: Option<f64>,
#[serde(default)]
pub output_cost_per_reasoning_token: Option<f64>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct ModelInfo {
pub input_cost_per_token: f64,
pub output_cost_per_token: f64,
pub cache_read_input_token_cost: Option<f64>,
pub cache_creation_input_token_cost: Option<f64>,
pub input_cost_per_audio_token: Option<f64>,
pub output_cost_per_audio_token: Option<f64>,
pub output_cost_per_reasoning_token: Option<f64>,
pub max_tokens: Option<u64>,
pub max_input_tokens: Option<u64>,
pub max_output_tokens: Option<u64>,
pub mode: Option<String>,
pub supports_vision: Option<bool>,
pub supports_function_calling: Option<bool>,
pub supports_reasoning: Option<bool>,
pub supports_structured_output: Option<bool>,
pub supports_audio_input: Option<bool>,
pub supports_audio_output: Option<bool>,
pub supports_prompt_caching: Option<bool>,
pub tiers: Vec<ModelTier>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct ModelTier {
pub min_context_tokens: u64,
pub input_cost_per_token: f64,
pub output_cost_per_token: f64,
pub cache_read_input_token_cost: Option<f64>,
pub cache_creation_input_token_cost: Option<f64>,
pub input_cost_per_audio_token: Option<f64>,
pub output_cost_per_audio_token: Option<f64>,
pub output_cost_per_reasoning_token: Option<f64>,
}
impl From<&PricingTier> for ModelTier {
fn from(tier: &PricingTier) -> Self {
ModelTier {
min_context_tokens: tier.min_context_tokens,
input_cost_per_token: tier.input_cost_per_token,
output_cost_per_token: tier.output_cost_per_token,
cache_read_input_token_cost: tier.cache_read_input_token_cost,
cache_creation_input_token_cost: tier.cache_creation_input_token_cost,
input_cost_per_audio_token: tier.input_cost_per_audio_token,
output_cost_per_audio_token: tier.output_cost_per_audio_token,
output_cost_per_reasoning_token: tier.output_cost_per_reasoning_token,
}
}
}
impl From<&ModelPricing> for ModelInfo {
fn from(pricing: &ModelPricing) -> Self {
ModelInfo {
input_cost_per_token: pricing.input_cost_per_token,
output_cost_per_token: pricing.output_cost_per_token,
cache_read_input_token_cost: pricing.cache_read_input_token_cost,
cache_creation_input_token_cost: pricing.cache_creation_input_token_cost,
input_cost_per_audio_token: pricing.input_cost_per_audio_token,
output_cost_per_audio_token: pricing.output_cost_per_audio_token,
output_cost_per_reasoning_token: pricing.output_cost_per_reasoning_token,
max_tokens: pricing.max_tokens,
max_input_tokens: pricing.max_input_tokens,
max_output_tokens: pricing.max_output_tokens,
mode: pricing.mode.clone(),
supports_vision: pricing.supports_vision,
supports_function_calling: pricing.supports_function_calling,
supports_reasoning: pricing.supports_reasoning,
supports_structured_output: pricing.supports_structured_output,
supports_audio_input: pricing.supports_audio_input,
supports_audio_output: pricing.supports_audio_output,
supports_prompt_caching: pricing.supports_prompt_caching,
tiers: pricing.tiers.iter().map(ModelTier::from).collect(),
}
}
}
#[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> {
with_active_registry(|reg| compute_cost_in(reg, model, prompt_tokens, cached_tokens, completion_tokens))
}
fn compute_cost_in(
reg: &HashMap<String, ModelPricing>,
model: &str,
prompt_tokens: u64,
cached_tokens: u64,
completion_tokens: u64,
) -> Option<f64> {
let pricing = lookup_in(reg, model)?;
Some(compute_cost(pricing, prompt_tokens, cached_tokens, completion_tokens))
}
fn select_tier(pricing: &ModelPricing, prompt_tokens: u64) -> Option<&PricingTier> {
pricing
.tiers
.iter()
.filter(|tier| tier.min_context_tokens <= prompt_tokens)
.max_by_key(|tier| tier.min_context_tokens)
}
fn compute_cost(pricing: &ModelPricing, prompt_tokens: u64, cached_tokens: u64, completion_tokens: u64) -> f64 {
let cached = cached_tokens.min(prompt_tokens);
let uncached = prompt_tokens - cached;
let tier = select_tier(pricing, prompt_tokens);
let input_rate = tier.map_or(pricing.input_cost_per_token, |t| t.input_cost_per_token);
let output_rate = tier.map_or(pricing.output_cost_per_token, |t| t.output_cost_per_token);
let cache_rate = tier
.and_then(|t| t.cache_read_input_token_cost)
.or(pricing.cache_read_input_token_cost)
.unwrap_or(input_rate);
(uncached as f64) * input_rate + (cached as f64) * cache_rate + (completion_tokens as f64) * output_rate
}
fn lookup(model: &str) -> Option<&'static ModelPricing> {
lookup_in(registry()?, model)
}
fn lookup_in<'a>(models: &'a HashMap<String, ModelPricing>, model: &str) -> Option<&'a ModelPricing> {
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_attr(alef, alef(skip))]
#[must_use]
pub fn model_pricing(model: &str) -> Option<&'static ModelPricing> {
lookup(model)
}
#[must_use]
pub fn model_info(model: &str) -> Option<ModelInfo> {
with_active_registry(|reg| model_info_in(reg, model))
}
fn model_info_in(reg: &HashMap<String, ModelPricing>, model: &str) -> Option<ModelInfo> {
lookup_in(reg, model).map(ModelInfo::from)
}
fn with_active_registry<T>(f: impl FnOnce(&HashMap<String, ModelPricing>) -> Option<T>) -> Option<T> {
if let Some(overlay) = refresh::overlay_registry() {
return f(&overlay);
}
f(registry()?)
}
pub mod refresh;
pub use refresh::{
CatalogRefreshConfig, CatalogRefreshError, DEFAULT_CATALOG_URL, RefreshOutcome, clear_catalog_overlay,
install_catalog_overlay_from_str, refresh_catalog,
};
#[cfg(test)]
mod refresh_tests;
#[cfg(test)]
mod tests;