use serde::{Deserialize, Serialize};
use crate::catalog::{CatalogOffering, CatalogSource};
use crate::models_dev::ModelsDevCost;
use crate::route::PricingSku;
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Currency {
#[default]
Usd,
Cny,
Other(String),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "source", rename_all = "snake_case")]
pub enum PricingProvenance {
ModelsDevBundled,
ProviderLive,
ProviderDocs,
UserOverride,
Unknown,
}
impl PricingProvenance {
#[must_use]
pub fn label(&self) -> &'static str {
match self {
Self::ModelsDevBundled => "models_dev_bundled",
Self::ProviderLive => "provider_live",
Self::ProviderDocs => "provider_docs",
Self::UserOverride => "user_override",
Self::Unknown => "unknown",
}
}
#[must_use]
pub fn is_authoritative_without_freshness_check(&self) -> bool {
matches!(
self,
Self::ModelsDevBundled | Self::ProviderDocs | Self::UserOverride
)
}
}
pub const LIVE_PRICING_MAX_AGE_SECS: u64 = 24 * 60 * 60;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "defect", rename_all = "snake_case")]
pub enum LivePricingDefect {
Stale { age_secs: u64, max_age_secs: u64 },
EndpointMismatch {
row_fingerprint: String,
route_fingerprint: String,
},
MissingEndpointFingerprint,
MissingTimestamp,
UnknownRouteEndpoint,
}
impl LivePricingDefect {
#[must_use]
pub fn label(&self) -> &'static str {
match self {
Self::Stale { .. } => "live_pricing_stale",
Self::EndpointMismatch { .. } => "live_pricing_endpoint_mismatch",
Self::MissingEndpointFingerprint => "live_pricing_missing_endpoint_fingerprint",
Self::MissingTimestamp => "live_pricing_missing_timestamp",
Self::UnknownRouteEndpoint => "live_pricing_unknown_route_endpoint",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct TokenUsage {
pub input: u64,
pub output: u64,
pub cache_read: u64,
pub cache_write: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TokenClass {
Input,
Output,
CacheRead,
CacheWrite,
}
impl TokenClass {
pub const ALL: [Self; 4] = [Self::Input, Self::Output, Self::CacheRead, Self::CacheWrite];
#[must_use]
pub fn label(self) -> &'static str {
match self {
Self::Input => "input",
Self::Output => "output",
Self::CacheRead => "cache_read",
Self::CacheWrite => "cache_write",
}
}
#[must_use]
pub fn tokens(self, usage: &TokenUsage) -> u64 {
match self {
Self::Input => usage.input,
Self::Output => usage.output,
Self::CacheRead => usage.cache_read,
Self::CacheWrite => usage.cache_write,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct OfferingPricing {
pub provider: String,
pub wire_model_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub canonical_model: Option<String>,
pub currency: Currency,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub input_per_million: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub output_per_million: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cache_read_per_million: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cache_write_per_million: Option<f64>,
pub provenance: PricingProvenance,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub effective_at: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub endpoint_fingerprint: Option<String>,
}
impl OfferingPricing {
#[must_use]
pub fn from_catalog_offering(offering: &CatalogOffering) -> Option<Self> {
let cost = offering.cost.as_ref()?;
if !catalog_cost_is_valid(cost) {
return None;
}
if cost.input.is_none()
&& cost.output.is_none()
&& cost.cache_read.is_none()
&& cost.cache_write.is_none()
{
return None;
}
Some(Self {
provider: offering.provider.clone(),
wire_model_id: offering.wire_model_id.clone(),
canonical_model: offering.canonical_model.clone(),
currency: Currency::Usd,
input_per_million: cost.input,
output_per_million: cost.output,
cache_read_per_million: cost.cache_read,
cache_write_per_million: cost.cache_write,
provenance: provenance_from_source(&offering.source),
effective_at: effective_at_from_source(&offering.source),
endpoint_fingerprint: endpoint_fingerprint_from_source(&offering.source),
})
}
#[must_use]
pub fn has_any_price(&self) -> bool {
self.input_per_million.is_some()
|| self.output_per_million.is_some()
|| self.cache_read_per_million.is_some()
|| self.cache_write_per_million.is_some()
}
#[must_use]
pub fn is_stale(&self, now_unix: u64, max_age_secs: u64) -> bool {
match self.effective_at {
Some(t) => now_unix.saturating_sub(t) >= max_age_secs,
None => false,
}
}
#[must_use]
pub fn live_pricing_defect(
&self,
route_endpoint_fingerprint: Option<&str>,
now_unix: Option<u64>,
max_age_secs: u64,
) -> Option<LivePricingDefect> {
if self.provenance != PricingProvenance::ProviderLive {
return None;
}
let Some(row_fingerprint) = self.endpoint_fingerprint.as_deref() else {
return Some(LivePricingDefect::MissingEndpointFingerprint);
};
let Some(route_fingerprint) = route_endpoint_fingerprint else {
return Some(LivePricingDefect::UnknownRouteEndpoint);
};
if row_fingerprint != route_fingerprint {
return Some(LivePricingDefect::EndpointMismatch {
row_fingerprint: row_fingerprint.to_string(),
route_fingerprint: route_fingerprint.to_string(),
});
}
let Some(effective_at) = self.effective_at else {
return Some(LivePricingDefect::MissingTimestamp);
};
let Some(now_unix) = now_unix else {
return Some(LivePricingDefect::MissingTimestamp);
};
let age_secs = now_unix.saturating_sub(effective_at);
if age_secs >= max_age_secs {
return Some(LivePricingDefect::Stale {
age_secs,
max_age_secs,
});
}
None
}
#[must_use]
pub fn price_per_million(&self, class: TokenClass) -> Option<f64> {
match class {
TokenClass::Input => self.input_per_million,
TokenClass::Output => self.output_per_million,
TokenClass::CacheRead => self.cache_read_per_million,
TokenClass::CacheWrite => self.cache_write_per_million,
}
}
#[must_use]
pub fn unpriced_used_classes(&self, usage: &TokenUsage) -> Vec<TokenClass> {
TokenClass::ALL
.into_iter()
.filter(|class| class.tokens(usage) > 0 && self.price_per_million(*class).is_none())
.collect()
}
#[must_use]
pub fn estimate_cost(&self, usage: &TokenUsage) -> Option<f64> {
let mut total = 0.0_f64;
for class in TokenClass::ALL {
let tokens = class.tokens(usage);
if tokens > 0 {
let price = self.price_per_million(class)?;
let component = (tokens as f64 / 1_000_000.0) * price;
if !component.is_finite() || component < 0.0 {
return None;
}
total += component;
if !total.is_finite() || total < 0.0 {
return None;
}
}
}
Some(total)
}
#[must_use]
pub fn to_route_sku(&self) -> PricingSku {
if self.input_per_million.is_none() && self.output_per_million.is_none() {
return PricingSku::UnknownOrStale;
}
PricingSku::Token {
input_per_mtok: self.input_per_million,
output_per_mtok: self.output_per_million,
}
}
}
#[must_use]
pub fn route_pricing_sku(offering: &CatalogOffering) -> PricingSku {
OfferingPricing::from_catalog_offering(offering)
.map_or(PricingSku::UnknownOrStale, |pricing| pricing.to_route_sku())
}
#[must_use]
pub(crate) fn route_pricing_sku_from_cost(cost: Option<&ModelsDevCost>) -> PricingSku {
let Some(cost) = cost else {
return PricingSku::UnknownOrStale;
};
if !catalog_cost_is_valid(cost) {
return PricingSku::UnknownOrStale;
}
if cost.input.is_none() && cost.output.is_none() {
return PricingSku::UnknownOrStale;
}
PricingSku::Token {
input_per_mtok: cost.input,
output_per_mtok: cost.output,
}
}
pub const MAX_PLAUSIBLE_PRICE_PER_MILLION: f64 = 100_000.0;
#[must_use]
pub fn catalog_cost_is_valid(cost: &ModelsDevCost) -> bool {
[cost.input, cost.output, cost.cache_read, cost.cache_write]
.into_iter()
.flatten()
.all(|price| price.is_finite() && (0.0..=MAX_PLAUSIBLE_PRICE_PER_MILLION).contains(&price))
}
fn provenance_from_source(source: &CatalogSource) -> PricingProvenance {
match source {
CatalogSource::Bundled => PricingProvenance::ModelsDevBundled,
CatalogSource::Live { .. } => PricingProvenance::ProviderLive,
CatalogSource::UserOverride => PricingProvenance::UserOverride,
}
}
fn effective_at_from_source(source: &CatalogSource) -> Option<u64> {
match source {
CatalogSource::Live { fetched_at, .. } => Some(*fetched_at),
CatalogSource::Bundled | CatalogSource::UserOverride => None,
}
}
fn endpoint_fingerprint_from_source(source: &CatalogSource) -> Option<String> {
match source {
CatalogSource::Live {
base_url_fingerprint,
..
} => Some(base_url_fingerprint.clone()),
CatalogSource::Bundled | CatalogSource::UserOverride => None,
}
}
#[cfg(test)]
mod tests;