use chrono::{DateTime, TimeZone, Utc};
use codewhale_config::pricing::{
Currency, LIVE_PRICING_MAX_AGE_SECS, LivePricingDefect, OfferingPricing, PricingProvenance,
TokenClass, TokenUsage,
};
use crate::config::{
ApiProvider, DEEPSEEK_ALIAS_REPLACEMENT, DEEPSEEK_ALIAS_RETIREMENT_UTC,
DEFAULT_STEPFUN_BASE_URL, DEFAULT_STEPFUN_MODEL, DEFAULT_STEPFUN_PLAN_BASE_URL,
canonical_model_id_for_provider,
};
use crate::models::{Usage, has_date_snapshot_suffix};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CostCurrency {
Usd,
Cny,
}
impl CostCurrency {
pub fn from_setting(value: &str) -> Option<Self> {
match value.trim().to_ascii_lowercase().as_str() {
"usd" | "dollar" | "dollars" | "$" => Some(Self::Usd),
"cny" | "rmb" | "yuan" | "¥" => Some(Self::Cny),
_ => None,
}
}
fn symbol(self) -> &'static str {
match self {
Self::Usd => "$",
Self::Cny => "¥",
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq)]
pub struct CostEstimate {
pub usd: f64,
pub cny: f64,
}
impl CostEstimate {
#[allow(dead_code)]
pub fn usd_only(usd: f64) -> Self {
Self { usd, cny: 0.0 }
}
pub fn is_positive(self) -> bool {
self.is_finite_nonnegative() && (self.usd > 0.0 || self.cny > 0.0)
}
#[must_use]
pub fn is_finite_nonnegative(self) -> bool {
self.usd.is_finite() && self.usd >= 0.0 && self.cny.is_finite() && self.cny >= 0.0
}
#[must_use]
pub fn sanitized(self) -> Self {
Self {
usd: if self.usd.is_finite() && self.usd >= 0.0 {
self.usd
} else {
0.0
},
cny: if self.cny.is_finite() && self.cny >= 0.0 {
self.cny
} else {
0.0
},
}
}
#[must_use]
pub fn saturating_add(self, rhs: Self) -> Self {
fn component(left: f64, right: f64) -> f64 {
let sum = left + right;
if sum.is_finite() { sum } else { f64::MAX }
}
let left = self.sanitized();
let right = rhs.sanitized();
Self {
usd: component(left.usd, right.usd),
cny: component(left.cny, right.cny),
}
}
pub fn amount(self, currency: CostCurrency) -> f64 {
match currency {
CostCurrency::Usd => self.usd,
CostCurrency::Cny => self.cny,
}
}
}
#[derive(Debug, Clone, Default, serde::Deserialize)]
pub struct BalanceResponse {
#[allow(dead_code)]
pub is_available: bool,
pub balance_infos: Vec<BalanceInfo>,
}
#[derive(Debug, Clone, Default, serde::Deserialize)]
pub struct BalanceInfo {
#[allow(dead_code)]
pub currency: String,
#[serde(default)]
#[allow(dead_code)]
pub total_balance: String,
#[serde(default)]
#[allow(dead_code)]
pub topped_up_balance: String,
#[serde(default)]
#[allow(dead_code)]
pub granted_balance: String,
}
impl BalanceInfo {}
#[derive(Debug, Clone, Copy, PartialEq)]
enum CacheWritePolicy {
Rate(f64),
DocumentedAsInputRate(&'static str),
Unpublished,
}
const DEEPSEEK_CACHE_WRITE_IS_FREE: &str = "deepseek-kv-cache-no-write-charge";
impl CacheWritePolicy {
fn rate(self, input_cache_miss_per_million: f64) -> Option<f64> {
match self {
Self::Rate(rate) => Some(rate),
Self::DocumentedAsInputRate(_) => Some(input_cache_miss_per_million),
Self::Unpublished => None,
}
}
}
#[derive(Debug, Clone, Copy)]
struct CurrencyPricing {
input_cache_hit_per_million: f64,
input_cache_miss_per_million: f64,
output_per_million: f64,
cache_write: CacheWritePolicy,
}
#[derive(Debug, Clone, Copy)]
struct ModelPricing {
usd: CurrencyPricing,
cny: Option<CurrencyPricing>,
}
pub(crate) const STEPFUN_PAYG_BILLING_SURFACE: &str = "stepfun-payg";
pub(crate) const STEPFUN_PLAN_BILLING_SURFACE: &str = "stepfun-plan";
const LEGACY_STEPFUN_PLAN_BASE_URL: &str = "https://api.stepfun.com/step_plan/v1";
pub(crate) const ZAI_CODING_PLAN_BILLING_SURFACE: &str = "zai-coding-plan";
pub(crate) const ZAI_PAYG_BILLING_SURFACE: &str = "zai-payg";
pub(crate) const MOONSHOT_KIMI_CODE_BILLING_SURFACE: &str = "moonshot-kimi-code";
pub(crate) const MOONSHOT_PAYG_BILLING_SURFACE: &str = "moonshot-payg";
pub(crate) const MINIMAX_TOKEN_PLAN_BILLING_SURFACE: &str = "minimax-token-plan";
pub(crate) const MINIMAX_PAYG_BILLING_SURFACE: &str = "minimax-payg";
pub(crate) const XIAOMI_TOKEN_PLAN_BILLING_SURFACE: &str = "xiaomi-mimo-token-plan";
pub(crate) const XIAOMI_PAYG_BILLING_SURFACE: &str = "xiaomi-mimo-payg";
pub(crate) const OAUTH_SUBSCRIPTION_BILLING_SURFACE: &str = "oauth-subscription";
pub(crate) const LOCAL_BILLING_SURFACE: &str = "local-no-bill";
pub(crate) const FIRST_PARTY_PAYG_BILLING_SURFACE: &str = "first-party-payg";
pub(crate) const AGGREGATOR_BILLING_SURFACE: &str = "aggregator-payg";
pub(crate) const UNCLASSIFIED_BILLING_SURFACE: &str = "unclassified";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EndpointMetering {
Money,
ExactSubscription,
LocalNoBill,
Unknown,
}
#[must_use]
pub fn endpoint_metering_for_billing_surface(billing_surface: Option<&str>) -> EndpointMetering {
let Some(surface) = billing_surface.map(str::trim).filter(|s| !s.is_empty()) else {
return EndpointMetering::Unknown;
};
for (known, metering) in [
(STEPFUN_PAYG_BILLING_SURFACE, EndpointMetering::Money),
(ZAI_PAYG_BILLING_SURFACE, EndpointMetering::Money),
(MOONSHOT_PAYG_BILLING_SURFACE, EndpointMetering::Money),
(MINIMAX_PAYG_BILLING_SURFACE, EndpointMetering::Money),
(XIAOMI_PAYG_BILLING_SURFACE, EndpointMetering::Money),
(FIRST_PARTY_PAYG_BILLING_SURFACE, EndpointMetering::Money),
(AGGREGATOR_BILLING_SURFACE, EndpointMetering::Money),
(
STEPFUN_PLAN_BILLING_SURFACE,
EndpointMetering::ExactSubscription,
),
(
ZAI_CODING_PLAN_BILLING_SURFACE,
EndpointMetering::ExactSubscription,
),
(
MOONSHOT_KIMI_CODE_BILLING_SURFACE,
EndpointMetering::ExactSubscription,
),
(
MINIMAX_TOKEN_PLAN_BILLING_SURFACE,
EndpointMetering::ExactSubscription,
),
(
XIAOMI_TOKEN_PLAN_BILLING_SURFACE,
EndpointMetering::ExactSubscription,
),
(
OAUTH_SUBSCRIPTION_BILLING_SURFACE,
EndpointMetering::ExactSubscription,
),
(LOCAL_BILLING_SURFACE, EndpointMetering::LocalNoBill),
(UNCLASSIFIED_BILLING_SURFACE, EndpointMetering::Unknown),
] {
if surface.eq_ignore_ascii_case(known) {
return metering;
}
}
EndpointMetering::Unknown
}
struct EndpointShape {
host: String,
path: String,
}
fn endpoint_shape(base_url: &str) -> Option<EndpointShape> {
let parsed = reqwest::Url::parse(base_url.trim()).ok()?;
if parsed.scheme() != "https"
|| !parsed.username().is_empty()
|| parsed.password().is_some()
|| parsed.query().is_some()
|| parsed.fragment().is_some()
|| parsed.port_or_known_default() != Some(443)
{
return None;
}
Some(EndpointShape {
host: parsed.host_str()?.to_ascii_lowercase(),
path: parsed.path().trim_end_matches('/').to_string(),
})
}
fn host_of(url: &str) -> Option<String> {
reqwest::Url::parse(url)
.ok()?
.host_str()
.map(str::to_ascii_lowercase)
}
pub(crate) fn billing_surface_for_route(
provider: ApiProvider,
base_url: Option<&str>,
) -> Option<&'static str> {
match provider {
ApiProvider::Ollama | ApiProvider::Sglang | ApiProvider::Vllm => {
return Some(LOCAL_BILLING_SURFACE);
}
ApiProvider::OllamaCloud => return Some(UNCLASSIFIED_BILLING_SURFACE),
ApiProvider::OpenaiCodex | ApiProvider::OpencodeGo => {
return Some(OAUTH_SUBSCRIPTION_BILLING_SURFACE);
}
ApiProvider::Custom => return Some(UNCLASSIFIED_BILLING_SURFACE),
_ => {}
}
let base_url = base_url.map(str::trim).filter(|url| !url.is_empty())?;
let Some(shape) = endpoint_shape(base_url) else {
return Some(UNCLASSIFIED_BILLING_SURFACE);
};
let surface = match provider {
ApiProvider::Stepfun => stepfun_surface(&shape),
ApiProvider::Zai => zai_surface(&shape),
ApiProvider::Moonshot => moonshot_surface(&shape),
ApiProvider::Minimax | ApiProvider::MinimaxAnthropic => minimax_surface(&shape),
ApiProvider::XiaomiMimo => xiaomi_surface(&shape),
ApiProvider::Openrouter | ApiProvider::NvidiaNim | ApiProvider::OpencodeZen => {
is_official_default_endpoint(provider, &shape).then_some(AGGREGATOR_BILLING_SURFACE)
}
_ => is_official_default_endpoint(provider, &shape)
.then_some(FIRST_PARTY_PAYG_BILLING_SURFACE),
};
Some(surface.unwrap_or(UNCLASSIFIED_BILLING_SURFACE))
}
fn stepfun_surface(shape: &EndpointShape) -> Option<&'static str> {
if host_of(DEFAULT_STEPFUN_BASE_URL).is_some_and(|official| shape.host == official)
&& matches!(shape.path.as_str(), "" | "/v1")
{
return Some(STEPFUN_PAYG_BILLING_SURFACE);
}
let plan_host = [DEFAULT_STEPFUN_PLAN_BASE_URL, LEGACY_STEPFUN_PLAN_BASE_URL]
.iter()
.filter_map(|url| host_of(url))
.any(|plan| plan == shape.host);
if plan_host && matches!(shape.path.as_str(), "/step_plan" | "/step_plan/v1") {
return Some(STEPFUN_PLAN_BILLING_SURFACE);
}
None
}
fn zai_surface(shape: &EndpointShape) -> Option<&'static str> {
if shape.host == "api.z.ai" && shape.path == "/api/coding/paas/v4" {
Some(ZAI_CODING_PLAN_BILLING_SURFACE)
} else if matches!(shape.host.as_str(), "api.z.ai" | "open.bigmodel.cn")
&& matches!(
shape.path.as_str(),
"/api/paas/v4" | "/api/anthropic" | "/v1" | ""
)
{
Some(ZAI_PAYG_BILLING_SURFACE)
} else {
None
}
}
fn moonshot_surface(shape: &EndpointShape) -> Option<&'static str> {
if shape.host == "api.kimi.com" && matches!(shape.path.as_str(), "/coding" | "/coding/v1") {
Some(MOONSHOT_KIMI_CODE_BILLING_SURFACE)
} else if matches!(shape.host.as_str(), "api.moonshot.ai" | "api.moonshot.cn")
&& matches!(shape.path.as_str(), "" | "/v1" | "/anthropic")
{
Some(MOONSHOT_PAYG_BILLING_SURFACE)
} else {
None
}
}
fn minimax_surface(shape: &EndpointShape) -> Option<&'static str> {
let _is_supported_endpoint = matches!(
shape.host.as_str(),
"api.minimax.io" | "api.minimaxi.com" | "api.minimax.chat"
) && matches!(shape.path.as_str(), "" | "/v1" | "/anthropic");
None
}
fn xiaomi_surface(shape: &EndpointShape) -> Option<&'static str> {
if matches!(
shape.host.as_str(),
"token-plan-cn.xiaomimimo.com"
| "token-plan-sgp.xiaomimimo.com"
| "token-plan-ams.xiaomimimo.com"
) && shape.path == "/v1"
{
return Some(XIAOMI_TOKEN_PLAN_BILLING_SURFACE);
}
if shape.host == "api.xiaomimimo.com" && shape.path == "/v1" {
return Some(XIAOMI_PAYG_BILLING_SURFACE);
}
None
}
fn is_official_default_endpoint(provider: ApiProvider, shape: &EndpointShape) -> bool {
let Some(default) = endpoint_shape(provider.default_base_url()) else {
return false;
};
if shape.host != default.host {
return false;
}
if shape.path == default.path {
return true;
}
match provider {
ApiProvider::Deepseek | ApiProvider::DeepseekCN => {
matches!(shape.path.as_str(), "" | "/v1" | "/beta")
}
ApiProvider::DeepseekAnthropic => shape.path == "/anthropic",
ApiProvider::Openai => matches!(shape.path.as_str(), "" | "/v1"),
ApiProvider::Anthropic => matches!(shape.path.as_str(), "" | "/v1"),
_ => false,
}
}
fn pricing_for_billing_surface(
provider: ApiProvider,
model: &str,
billing_surface: Option<&str>,
) -> Option<ModelPricing> {
if provider == ApiProvider::Stepfun
&& model.trim().eq_ignore_ascii_case(DEFAULT_STEPFUN_MODEL)
&& billing_surface
.is_some_and(|surface| surface.eq_ignore_ascii_case(STEPFUN_PAYG_BILLING_SURFACE))
{
Some(usd_only_pricing(0.04, 0.20, 1.15))
} else {
None
}
}
fn route_requires_billing_surface(provider: ApiProvider, model: &str) -> bool {
provider == ApiProvider::Stepfun || model.trim().eq_ignore_ascii_case(DEFAULT_STEPFUN_MODEL)
}
fn pricing_for_model(model: &str) -> Option<ModelPricing> {
pricing_for_model_at(model, Utc::now())
}
#[must_use]
pub fn has_pricing_for_model(model: &str) -> bool {
pricing_for_model(model).is_some()
}
#[must_use]
pub fn has_pricing_for_provider(provider: ApiProvider, model: &str) -> bool {
calculate_turn_cost_estimate_for_provider(provider, model, &Usage::default()).is_some()
}
#[must_use]
pub(crate) fn has_pricing_for_billing_surface(
provider: ApiProvider,
model: &str,
billing_surface: Option<&str>,
) -> bool {
pricing_for_billing_surface(provider, model, billing_surface).is_some()
}
fn pricing_for_model_at(model: &str, now: DateTime<Utc>) -> Option<ModelPricing> {
let lower = model.to_lowercase();
if lower.starts_with("deepseek-ai/") {
return None;
}
if lower == "claude-sonnet-5" {
return Some(claude_sonnet_5_pricing(now));
}
if let Some(pricing) = known_pricing_for_model(&lower) {
return Some(pricing);
}
if lower.contains("deepseek") {
if lower.contains("v4-pro") || lower.contains("v4pro") {
Some(deepseek_v4_pro_pricing())
} else {
Some(deepseek_v4_flash_pricing())
}
} else {
None
}
}
fn known_pricing_for_model(model_lower: &str) -> Option<ModelPricing> {
let explicit = match model_lower {
"openai/gpt-5.6" | "openai/gpt-5.6-sol" | "gpt-5.6" | "gpt-5.6-sol" => {
Some(usd_only_pricing(0.50, 5.00, 30.00))
}
"openai/gpt-5.6-terra" | "gpt-5.6-terra" => Some(usd_only_pricing(0.25, 2.50, 15.00)),
"openai/gpt-5.6-luna" | "gpt-5.6-luna" => Some(usd_only_pricing(0.10, 1.00, 6.00)),
"meta/muse-spark-1.1" | "muse-spark-1.1" => Some(usd_only_pricing(0.15, 1.25, 4.25)),
"meta/muse-spark-1.2" | "muse-spark-1.2" => Some(usd_only_pricing(0.15, 1.25, 4.25)),
"meta/muse-spark-1.2-contributor" | "muse-spark-1.2-contributor" => {
Some(usd_only_pricing(0.002, 0.10, 0.20))
}
"grok-4.6" => Some(grok_4_6_pricing(false)),
"claude-opus-4-8" => Some(usd_pricing_with_write(0.50, 5.00, 25.00, 6.25)),
"claude-sonnet-4-6" => Some(usd_pricing_with_write(0.30, 3.00, 15.00, 3.75)),
"claude-haiku-4-5" => Some(usd_pricing_with_write(0.10, 1.00, 5.00, 1.25)),
"claude-fable-5" => Some(usd_pricing_with_write(1.00, 10.00, 50.00, 12.50)),
"z-ai/glm-5.2" | "glm-5.2" => Some(usd_only_pricing(0.26, 1.40, 4.40)),
"moonshotai/kimi-k2.7-code" | "kimi-k2.7-code" => Some(usd_only_pricing(0.19, 0.95, 4.00)),
"minimax-m3" => Some(minimax_m3_standard_pricing(false)),
"minimax-m2.7" => Some(usd_pricing_with_write(0.06, 0.30, 1.20, 0.375)),
"openai/gpt-5-codex" | "gpt-5-codex" => Some(usd_only_pricing(0.125, 1.25, 10.00)),
"openai/gpt-5.3-codex" | "gpt-5.3-codex" => Some(usd_only_pricing(0.175, 1.75, 14.00)),
_ => None,
};
if explicit.is_some() {
return explicit;
}
if let Some((input_usd_per_million, output_usd_per_million)) =
crate::model_catalog::resolved_usd_pricing(model_lower)
{
return Some(usd_only_pricing(
input_usd_per_million,
input_usd_per_million,
output_usd_per_million,
));
}
match model_lower {
"moonshotai/kimi-k2.6" | "kimi-k2.6" => Some(usd_only_pricing(0.16, 0.95, 4.00)),
"z-ai/glm-5.1" | "glm-5.1" => Some(usd_only_pricing(0.26, 1.40, 4.40)),
"z-ai/glm-5-turbo" | "glm-5-turbo" => Some(usd_only_pricing(0.24, 1.20, 4.00)),
"arcee-ai/trinity-large-thinking" | "trinity-large-thinking" => {
Some(usd_only_pricing(0.25, 0.25, 0.80))
}
"openai/gpt-5.5" | "gpt-5.5" => Some(usd_only_pricing(0.50, 5.00, 30.00)),
"openai/gpt-5.5-pro" | "gpt-5.5-pro" => Some(usd_only_pricing(30.00, 30.00, 180.00)),
"qwen/qwen3.6-flash" => Some(usd_only_pricing(0.1875, 0.1875, 1.125)),
"qwen/qwen3.6-35b-a3b" => Some(usd_only_pricing(0.05, 0.14, 1.00)),
"qwen/qwen3.6-max-preview" => Some(usd_only_pricing(1.04, 1.04, 6.24)),
"qwen/qwen3.6-27b" => Some(usd_only_pricing(0.15, 0.285, 2.40)),
"qwen/qwen3.6-plus" => Some(usd_only_pricing(0.325, 0.325, 1.95)),
"qwen/qwen3.7-plus" => Some(usd_pricing_with_write(0.064, 0.32, 1.28, 0.40)),
"qwen/qwen3.7-max" => Some(usd_only_pricing(0.25, 1.25, 3.75)),
"google/gemma-4-31b-it" => Some(usd_only_pricing(0.09, 0.12, 0.35)),
"google/gemma-4-26b-a4b-it" => Some(usd_only_pricing(0.06, 0.06, 0.33)),
"tencent/hy3-preview" => Some(usd_only_pricing(0.021, 0.063, 0.21)),
"nvidia/nemotron-3-ultra-550b-a55b" | "nvidia/nemotron-3-ultra" => {
Some(usd_only_pricing(0.10, 0.50, 2.20))
}
_ => None,
}
}
fn usd_only_pricing(
input_cache_hit_per_million: f64,
input_cache_miss_per_million: f64,
output_per_million: f64,
) -> ModelPricing {
usd_pricing(
input_cache_hit_per_million,
input_cache_miss_per_million,
output_per_million,
CacheWritePolicy::Unpublished,
)
}
fn usd_pricing_with_write(
input_cache_hit_per_million: f64,
input_cache_miss_per_million: f64,
output_per_million: f64,
cache_write_per_million: f64,
) -> ModelPricing {
usd_pricing(
input_cache_hit_per_million,
input_cache_miss_per_million,
output_per_million,
CacheWritePolicy::Rate(cache_write_per_million),
)
}
fn usd_pricing(
input_cache_hit_per_million: f64,
input_cache_miss_per_million: f64,
output_per_million: f64,
cache_write: CacheWritePolicy,
) -> ModelPricing {
ModelPricing {
usd: CurrencyPricing {
input_cache_hit_per_million,
input_cache_miss_per_million,
output_per_million,
cache_write,
},
cny: None,
}
}
const MINIMAX_M3_LONG_CONTEXT_THRESHOLD: u32 = 512_000;
const GROK_4_6_LONG_CONTEXT_THRESHOLD: u32 = 200_000;
const OPENAI_LONG_CONTEXT_SURCHARGE_THRESHOLD: u32 = 272_000;
fn direct_openai_long_context_tier_is_unpriced(
provider: ApiProvider,
model: &str,
input_tokens: u32,
) -> bool {
let model_lower = model.trim().to_ascii_lowercase();
let affected_model = matches!(
model_lower.as_str(),
"gpt-5.4"
| "gpt-5.4-pro"
| "gpt-5.5"
| "gpt-5.6"
| "gpt-5.6-sol"
| "gpt-5.6-terra"
| "gpt-5.6-luna"
) || has_date_snapshot_suffix(&model_lower, "gpt-5.4-")
|| has_date_snapshot_suffix(&model_lower, "gpt-5.4-pro-")
|| has_date_snapshot_suffix(&model_lower, "gpt-5.5-");
provider == ApiProvider::Openai
&& input_tokens > OPENAI_LONG_CONTEXT_SURCHARGE_THRESHOLD
&& affected_model
}
fn minimax_m3_standard_pricing(long_context: bool) -> ModelPricing {
if long_context {
usd_only_pricing(0.12, 0.60, 2.40)
} else {
usd_only_pricing(0.06, 0.30, 1.20)
}
}
fn is_minimax_m3(model: &str) -> bool {
matches!(
model.trim().to_ascii_lowercase().as_str(),
"minimax-m3" | "minimax/minimax-m3"
)
}
fn grok_4_6_pricing(long_context: bool) -> ModelPricing {
if long_context {
usd_only_pricing(1.00, 4.00, 12.00)
} else {
usd_only_pricing(0.50, 2.00, 6.00)
}
}
fn is_grok_4_6(model: &str) -> bool {
model.trim().eq_ignore_ascii_case("grok-4.6")
}
fn pricing_for_model_and_usage(model: &str, usage: &Usage) -> Option<ModelPricing> {
if is_minimax_m3(model) {
return Some(minimax_m3_standard_pricing(
usage.input_tokens > MINIMAX_M3_LONG_CONTEXT_THRESHOLD,
));
}
if is_grok_4_6(model) {
return Some(grok_4_6_pricing(
usage.input_tokens >= GROK_4_6_LONG_CONTEXT_THRESHOLD,
));
}
pricing_for_model(model)
}
fn claude_sonnet_5_pricing(now: DateTime<Utc>) -> ModelPricing {
let intro_ends = Utc
.with_ymd_and_hms(2026, 9, 1, 0, 0, 0)
.single()
.expect("valid intro-pricing cutoff");
if now < intro_ends {
usd_pricing_with_write(0.20, 2.00, 10.00, 2.50)
} else {
usd_pricing_with_write(0.30, 3.00, 15.00, 3.75)
}
}
fn deepseek_v4_pro_pricing() -> ModelPricing {
ModelPricing {
usd: CurrencyPricing {
input_cache_hit_per_million: 0.003625,
input_cache_miss_per_million: 0.435,
output_per_million: 0.87,
cache_write: CacheWritePolicy::DocumentedAsInputRate(DEEPSEEK_CACHE_WRITE_IS_FREE),
},
cny: Some(CurrencyPricing {
input_cache_hit_per_million: 0.025,
input_cache_miss_per_million: 3.0,
output_per_million: 6.0,
cache_write: CacheWritePolicy::DocumentedAsInputRate(DEEPSEEK_CACHE_WRITE_IS_FREE),
}),
}
}
fn deepseek_v4_flash_pricing() -> ModelPricing {
ModelPricing {
usd: CurrencyPricing {
input_cache_hit_per_million: 0.0028,
input_cache_miss_per_million: 0.14,
output_per_million: 0.28,
cache_write: CacheWritePolicy::DocumentedAsInputRate(DEEPSEEK_CACHE_WRITE_IS_FREE),
},
cny: Some(CurrencyPricing {
input_cache_hit_per_million: 0.02,
input_cache_miss_per_million: 1.0,
output_per_million: 2.0,
cache_write: CacheWritePolicy::DocumentedAsInputRate(DEEPSEEK_CACHE_WRITE_IS_FREE),
}),
}
}
#[must_use]
#[cfg(test)]
pub fn calculate_turn_cost_from_usage(model: &str, usage: &Usage) -> Option<f64> {
calculate_turn_cost_estimate_from_usage(model, usage).map(|estimate| estimate.usd)
}
#[must_use]
#[cfg(test)]
pub fn calculate_turn_cost_estimate_from_usage(model: &str, usage: &Usage) -> Option<CostEstimate> {
let pricing = pricing_for_model_and_usage(model, usage)?;
Some(cost_estimate_with_pricing(pricing, usage))
}
fn cost_estimate_with_pricing_checked(
pricing: ModelPricing,
usage: &Usage,
) -> Result<CostEstimate, Vec<TokenClass>> {
let classes = token_usage_for_pricing(usage);
if classes.cache_write > 0
&& pricing
.usd
.cache_write
.rate(pricing.usd.input_cache_miss_per_million)
.is_none()
{
return Err(vec![TokenClass::CacheWrite]);
}
Ok(CostEstimate {
usd: calculate_turn_cost_from_usage_with_pricing(pricing.usd, usage),
cny: pricing
.cny
.map(|pricing| calculate_turn_cost_from_usage_with_pricing(pricing, usage))
.unwrap_or(0.0),
})
}
#[cfg(test)]
fn cost_estimate_with_pricing(pricing: ModelPricing, usage: &Usage) -> CostEstimate {
CostEstimate {
usd: calculate_turn_cost_from_usage_with_pricing(pricing.usd, usage),
cny: pricing
.cny
.map(|pricing| calculate_turn_cost_from_usage_with_pricing(pricing, usage))
.unwrap_or(0.0),
}
}
#[must_use]
pub fn calculate_turn_cost_estimate_for_provider(
provider: ApiProvider,
model: &str,
usage: &Usage,
) -> Option<CostEstimate> {
calculate_turn_cost_estimate_for_provider_at(provider, model, usage, Utc::now())
}
#[must_use]
#[cfg(test)]
pub fn calculate_turn_cost_estimate_for_route(
provider: ApiProvider,
model: &str,
usage: &Usage,
billing: crate::route_billing::BillingPresentation,
) -> Option<CostEstimate> {
audit_turn_cost_for_route(provider, model, None, usage, Utc::now(), billing).estimate
}
#[must_use]
#[cfg(test)]
pub(crate) fn calculate_turn_cost_estimate_for_billing_surface(
provider: ApiProvider,
model: &str,
billing_surface: Option<&str>,
usage: &Usage,
) -> Option<CostEstimate> {
calculate_turn_cost_estimate_for_route_at(provider, model, billing_surface, usage, Utc::now())
}
#[must_use]
pub(crate) fn calculate_turn_cost_estimate_for_provider_at(
provider: ApiProvider,
model: &str,
usage: &Usage,
recorded_at: DateTime<Utc>,
) -> Option<CostEstimate> {
audit_turn_cost_for_provider_at(provider, model, usage, recorded_at).estimate
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UnpricedReason {
NotMoneyMetered,
UnknownBillingBasis,
AmbiguousBillingSurface,
UnestablishedEndpoint,
UnpricedBillingSurface,
UnverifiedLivePricing,
RetiredAlias,
UnrepresentedTier,
NoPricingRow,
MissingClassPrice,
InvalidPricingRow,
UnsupportedCurrency,
InconsistentUsage,
}
impl UnpricedReason {
#[must_use]
pub fn label(self) -> &'static str {
match self {
Self::NotMoneyMetered => "not_money_metered",
Self::UnknownBillingBasis => "unknown_billing_basis",
Self::AmbiguousBillingSurface => "ambiguous_billing_surface",
Self::UnestablishedEndpoint => "unestablished_endpoint",
Self::UnpricedBillingSurface => "unpriced_billing_surface",
Self::UnverifiedLivePricing => "unverified_live_pricing",
Self::RetiredAlias => "retired_alias",
Self::UnrepresentedTier => "unrepresented_pricing_tier",
Self::NoPricingRow => "no_pricing_row",
Self::MissingClassPrice => "missing_class_price",
Self::InvalidPricingRow => "invalid_pricing_row",
Self::UnsupportedCurrency => "unsupported_currency",
Self::InconsistentUsage => "inconsistent_usage",
}
}
#[must_use]
pub fn counts_toward_money_coverage(self) -> bool {
self != Self::NotMoneyMetered
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct TurnCostAudit {
pub estimate: Option<CostEstimate>,
pub provenance: Option<PricingProvenance>,
pub unpriced_classes: Vec<TokenClass>,
pub unpriced_reason: Option<UnpricedReason>,
pub live_pricing_defect: Option<LivePricingDefect>,
pub usd_priced: bool,
pub cny_priced: bool,
}
impl TurnCostAudit {
fn priced(
estimate: CostEstimate,
provenance: PricingProvenance,
usd_priced: bool,
cny_priced: bool,
) -> Self {
Self {
estimate: Some(estimate),
provenance: Some(provenance),
unpriced_classes: Vec::new(),
unpriced_reason: None,
live_pricing_defect: None,
usd_priced,
cny_priced,
}
}
pub(crate) fn unpriced(reason: UnpricedReason) -> Self {
Self {
estimate: None,
provenance: None,
unpriced_classes: Vec::new(),
unpriced_reason: Some(reason),
live_pricing_defect: None,
usd_priced: false,
cny_priced: false,
}
}
fn missing_classes(provenance: PricingProvenance, classes: Vec<TokenClass>) -> Self {
Self {
estimate: None,
provenance: Some(provenance),
unpriced_classes: classes,
unpriced_reason: Some(UnpricedReason::MissingClassPrice),
live_pricing_defect: None,
usd_priced: false,
cny_priced: false,
}
}
fn unverified_live(defect: LivePricingDefect) -> Self {
Self {
estimate: None,
provenance: Some(PricingProvenance::Unknown),
unpriced_classes: Vec::new(),
unpriced_reason: Some(UnpricedReason::UnverifiedLivePricing),
live_pricing_defect: Some(defect),
usd_priced: false,
cny_priced: false,
}
}
fn with_live_defect(mut self, defect: Option<LivePricingDefect>) -> Self {
if let Some(defect) = defect {
self.live_pricing_defect = Some(defect);
}
self
}
#[must_use]
#[cfg(test)]
pub fn is_priced(&self) -> bool {
self.estimate.is_some()
}
#[must_use]
pub fn is_priced_in(&self, currency: CostCurrency) -> bool {
self.estimate.is_some()
&& match currency {
CostCurrency::Usd => self.usd_priced,
CostCurrency::Cny => self.cny_priced,
}
}
#[must_use]
pub fn counts_toward_money_coverage(&self) -> bool {
self.unpriced_reason
.is_none_or(UnpricedReason::counts_toward_money_coverage)
}
}
#[must_use]
pub(crate) fn audit_turn_cost_for_provider_at(
provider: ApiProvider,
model: &str,
usage: &Usage,
recorded_at: DateTime<Utc>,
) -> TurnCostAudit {
audit_turn_cost_for_provider_on_endpoint_at(provider, model, None, usage, recorded_at)
}
#[must_use]
pub(crate) fn audit_turn_cost_for_provider_on_endpoint_at(
provider: ApiProvider,
model: &str,
endpoint_fingerprint: Option<&str>,
usage: &Usage,
recorded_at: DateTime<Utc>,
) -> TurnCostAudit {
if !usage_cache_partition_is_consistent(usage) {
return TurnCostAudit::unpriced(UnpricedReason::InconsistentUsage);
}
if provider == ApiProvider::OpenaiCodex {
return TurnCostAudit::unpriced(UnpricedReason::NotMoneyMetered);
}
if route_requires_billing_surface(provider, model) {
return TurnCostAudit::unpriced(UnpricedReason::AmbiguousBillingSurface);
}
let normalized_model = model.trim();
let model_lower = normalized_model.to_ascii_lowercase();
let direct_deepseek = matches!(
provider,
ApiProvider::Deepseek | ApiProvider::DeepseekCN | ApiProvider::DeepseekAnthropic
);
let Some(canonical_model) = canonical_model_id_for_provider(provider, normalized_model) else {
return TurnCostAudit::unpriced(UnpricedReason::NoPricingRow);
};
let catalog_model = if direct_deepseek
&& matches!(model_lower.as_str(), "deepseek-chat" | "deepseek-reasoner")
{
let Ok(retirement) = DateTime::parse_from_rfc3339(DEEPSEEK_ALIAS_RETIREMENT_UTC) else {
return TurnCostAudit::unpriced(UnpricedReason::NoPricingRow);
};
if recorded_at >= retirement.with_timezone(&Utc) {
return TurnCostAudit::unpriced(UnpricedReason::RetiredAlias);
}
DEEPSEEK_ALIAS_REPLACEMENT.to_string()
} else {
canonical_model
};
if direct_openai_long_context_tier_is_unpriced(provider, &catalog_model, usage.input_tokens) {
return TurnCostAudit::unpriced(UnpricedReason::UnrepresentedTier);
}
if matches!(
provider,
ApiProvider::Minimax | ApiProvider::MinimaxAnthropic
) && catalog_model.eq_ignore_ascii_case("minimax-m3")
{
return hand_priced_audit(pricing_for_model_and_usage(&catalog_model, usage), usage);
}
if provider == ApiProvider::Xai && catalog_model.eq_ignore_ascii_case("grok-4.6") {
return hand_priced_audit(pricing_for_model_and_usage(&catalog_model, usage), usage);
}
if direct_deepseek
|| (provider == ApiProvider::Anthropic
&& catalog_model.eq_ignore_ascii_case("claude-sonnet-5"))
{
return hand_priced_audit(
provider_owned_hand_pricing_at(provider, &catalog_model, recorded_at),
usage,
);
}
let classes = token_usage_for_pricing(usage);
let mut live_defect = None;
let offering = match verified_catalog_offering(
provider,
&catalog_model,
endpoint_fingerprint,
recorded_at,
) {
VerifiedOffering::Usable(offering) => Some(offering),
VerifiedOffering::DegradedToBundled { offering, defect } => {
live_defect = Some(defect);
Some(offering)
}
VerifiedOffering::Unusable(defect) => {
live_defect = Some(defect);
None
}
VerifiedOffering::Absent => None,
};
if let Some(audit) = offering.as_ref().and_then(invalid_catalog_pricing_audit) {
return audit.with_live_defect(live_defect);
}
if let Some(offering) = offering.as_ref()
&& let Some(pricing) =
effective_offering_pricing(provider, &catalog_model, offering, &classes)
{
if let Some(estimate) =
catalog_cost_estimate_for_route(provider, &catalog_model, offering, usage)
{
let (usd_priced, cny_priced) = match pricing.currency {
Currency::Usd => (true, false),
Currency::Cny => (false, true),
Currency::Other(_) => (false, false),
};
return TurnCostAudit::priced(
estimate,
pricing.provenance.clone(),
usd_priced,
cny_priced,
)
.with_live_defect(live_defect);
}
let classes = pricing.unpriced_used_classes(&classes);
if classes.is_empty() {
return TurnCostAudit::unpriced(UnpricedReason::UnsupportedCurrency)
.with_live_defect(live_defect);
}
return TurnCostAudit::missing_classes(pricing.provenance, classes)
.with_live_defect(live_defect);
}
let hand_row = provider_owned_hand_pricing_at(provider, &catalog_model, recorded_at);
match (live_defect, hand_row) {
(Some(defect), None) => TurnCostAudit::unverified_live(defect),
(defect, hand_row) => hand_priced_audit(hand_row, usage).with_live_defect(defect),
}
}
fn invalid_catalog_pricing_audit(
offering: &codewhale_config::catalog::CatalogOffering,
) -> Option<TurnCostAudit> {
offering
.cost
.as_ref()
.is_some_and(|cost| !codewhale_config::pricing::catalog_cost_is_valid(cost))
.then(|| TurnCostAudit::unpriced(UnpricedReason::InvalidPricingRow))
}
enum VerifiedOffering {
Usable(codewhale_config::catalog::CatalogOffering),
DegradedToBundled {
offering: codewhale_config::catalog::CatalogOffering,
defect: LivePricingDefect,
},
Unusable(LivePricingDefect),
Absent,
}
fn verified_catalog_offering(
provider: ApiProvider,
catalog_model: &str,
endpoint_fingerprint: Option<&str>,
recorded_at: DateTime<Utc>,
) -> VerifiedOffering {
let Some(offering) = crate::provider_lake::catalog_offering_for_model(provider, catalog_model)
else {
return VerifiedOffering::Absent;
};
let Some(pricing) = OfferingPricing::from_catalog_offering(&offering) else {
return VerifiedOffering::Usable(offering);
};
let now_unix = u64::try_from(recorded_at.timestamp()).ok();
let Some(defect) =
pricing.live_pricing_defect(endpoint_fingerprint, now_unix, LIVE_PRICING_MAX_AGE_SECS)
else {
return VerifiedOffering::Usable(offering);
};
match crate::provider_lake::bundled_catalog_offering_for_model(provider, catalog_model) {
Some(bundled) => VerifiedOffering::DegradedToBundled {
offering: bundled,
defect,
},
None => VerifiedOffering::Unusable(defect),
}
}
fn hand_priced_audit(pricing: Option<ModelPricing>, usage: &Usage) -> TurnCostAudit {
let Some(pricing) = pricing else {
return TurnCostAudit::unpriced(UnpricedReason::NoPricingRow);
};
let has_cny = pricing.cny.is_some();
match cost_estimate_with_pricing_checked(pricing, usage) {
Ok(estimate) => {
TurnCostAudit::priced(estimate, PricingProvenance::ProviderDocs, true, has_cny)
}
Err(classes) => TurnCostAudit::missing_classes(PricingProvenance::ProviderDocs, classes),
}
}
#[must_use]
#[cfg(test)]
pub(crate) fn calculate_turn_cost_estimate_for_route_at(
provider: ApiProvider,
model: &str,
billing_surface: Option<&str>,
usage: &Usage,
recorded_at: DateTime<Utc>,
) -> Option<CostEstimate> {
audit_turn_cost_for_route_at(provider, model, billing_surface, usage, recorded_at).estimate
}
#[must_use]
pub(crate) fn audit_turn_cost_for_route_at(
provider: ApiProvider,
model: &str,
billing_surface: Option<&str>,
usage: &Usage,
recorded_at: DateTime<Utc>,
) -> TurnCostAudit {
audit_turn_cost_for_route_on_endpoint_at(
provider,
model,
billing_surface,
None,
usage,
recorded_at,
)
}
#[must_use]
pub(crate) fn audit_turn_cost_for_route_on_endpoint_at(
provider: ApiProvider,
model: &str,
billing_surface: Option<&str>,
endpoint_fingerprint: Option<&str>,
usage: &Usage,
recorded_at: DateTime<Utc>,
) -> TurnCostAudit {
match endpoint_metering_for_billing_surface(billing_surface) {
EndpointMetering::ExactSubscription | EndpointMetering::LocalNoBill => {
return TurnCostAudit::unpriced(UnpricedReason::NotMoneyMetered);
}
EndpointMetering::Unknown if billing_surface.is_some() => {
return TurnCostAudit::unpriced(UnpricedReason::UnknownBillingBasis);
}
EndpointMetering::Unknown | EndpointMetering::Money => {}
}
if !usage_cache_partition_is_consistent(usage) {
return TurnCostAudit::unpriced(UnpricedReason::InconsistentUsage);
}
if provider == ApiProvider::Stepfun {
return match pricing_for_billing_surface(provider, model, billing_surface) {
Some(pricing) => match cost_estimate_with_pricing_checked(pricing, usage) {
Ok(estimate) => {
TurnCostAudit::priced(estimate, PricingProvenance::ProviderDocs, true, false)
}
Err(classes) => {
TurnCostAudit::missing_classes(PricingProvenance::ProviderDocs, classes)
}
},
None => TurnCostAudit::unpriced(match billing_surface {
Some(_) => UnpricedReason::UnpricedBillingSurface,
None => UnpricedReason::AmbiguousBillingSurface,
}),
};
}
if model.trim().eq_ignore_ascii_case(DEFAULT_STEPFUN_MODEL) {
return TurnCostAudit::unpriced(UnpricedReason::AmbiguousBillingSurface);
}
if billing_surface.is_none() {
return TurnCostAudit::unpriced(UnpricedReason::UnestablishedEndpoint);
}
audit_turn_cost_for_provider_on_endpoint_at(
provider,
model,
endpoint_fingerprint,
usage,
recorded_at,
)
}
#[must_use]
#[cfg(test)]
pub fn audit_turn_cost_for_route(
provider: ApiProvider,
model: &str,
billing_surface: Option<&str>,
usage: &Usage,
recorded_at: DateTime<Utc>,
billing: crate::route_billing::BillingPresentation,
) -> TurnCostAudit {
audit_turn_cost_for_route_on_endpoint(
provider,
model,
billing_surface,
None,
usage,
recorded_at,
billing,
)
}
#[must_use]
#[cfg(test)]
pub fn audit_turn_cost_for_route_on_endpoint(
provider: ApiProvider,
model: &str,
billing_surface: Option<&str>,
endpoint_fingerprint: Option<&str>,
usage: &Usage,
recorded_at: DateTime<Utc>,
billing: crate::route_billing::BillingPresentation,
) -> TurnCostAudit {
use crate::route_billing::BillingPresentation;
match billing {
BillingPresentation::Subscription(_) | BillingPresentation::Local => {
return TurnCostAudit::unpriced(UnpricedReason::NotMoneyMetered);
}
BillingPresentation::Unknown => {
return TurnCostAudit::unpriced(UnpricedReason::UnknownBillingBasis);
}
BillingPresentation::Metered => {}
}
match endpoint_metering_for_billing_surface(billing_surface) {
EndpointMetering::ExactSubscription | EndpointMetering::LocalNoBill => {
return TurnCostAudit::unpriced(UnpricedReason::NotMoneyMetered);
}
EndpointMetering::Unknown if billing_surface.is_some() => {
return TurnCostAudit::unpriced(UnpricedReason::UnknownBillingBasis);
}
EndpointMetering::Unknown | EndpointMetering::Money => {}
}
audit_turn_cost_for_route_on_endpoint_at(
provider,
model,
billing_surface,
endpoint_fingerprint,
usage,
recorded_at,
)
}
fn provider_owned_hand_pricing_at(
provider: ApiProvider,
model: &str,
recorded_at: DateTime<Utc>,
) -> Option<ModelPricing> {
let model_lower = model.trim().to_ascii_lowercase();
let provider_owns_row = match provider {
ApiProvider::Deepseek | ApiProvider::DeepseekCN | ApiProvider::DeepseekAnthropic => {
matches!(
model_lower.as_str(),
"deepseek-v4-pro" | "deepseek-v4-flash"
)
}
ApiProvider::Openai => matches!(
model_lower.as_str(),
"gpt-5-codex"
| "gpt-5.3-codex"
| "gpt-5.5"
| "gpt-5.5-pro"
| "gpt-5.6"
| "gpt-5.6-sol"
| "gpt-5.6-terra"
| "gpt-5.6-luna"
),
ApiProvider::Anthropic => matches!(
model_lower.as_str(),
"claude-opus-4-8"
| "claude-sonnet-4-6"
| "claude-haiku-4-5"
| "claude-fable-5"
| "claude-sonnet-5"
),
ApiProvider::Xai => model_lower == "grok-4.6",
ApiProvider::Zai => matches!(model_lower.as_str(), "glm-5.1" | "glm-5.2" | "glm-5-turbo"),
ApiProvider::Moonshot => {
matches!(model_lower.as_str(), "kimi-k2.6" | "kimi-k2.7-code")
}
ApiProvider::Minimax | ApiProvider::MinimaxAnthropic => {
matches!(model_lower.as_str(), "minimax-m3" | "minimax-m2.7")
}
ApiProvider::Arcee => model_lower == "trinity-large-thinking",
ApiProvider::Meta => matches!(
model_lower.as_str(),
"muse-spark-1.1" | "muse-spark-1.2" | "muse-spark-1.2-contributor"
),
ApiProvider::Fireworks => {
let bare = model_lower
.strip_prefix("accounts/fireworks/models/")
.unwrap_or(model_lower.as_str());
matches!(bare, "deepseek-v4-flash" | "deepseek-v4-pro")
}
ApiProvider::OpencodeZen => {
matches!(
model_lower.as_str(),
"deepseek-v4-flash" | "deepseek-v4-pro"
)
}
_ => false,
};
let lookup = if provider == ApiProvider::Fireworks {
model_lower
.strip_prefix("accounts/fireworks/models/")
.unwrap_or(model_lower.as_str())
.to_string()
} else {
model_lower
};
provider_owns_row
.then(|| pricing_for_model_at(&lookup, recorded_at))
.flatten()
}
fn effective_offering_pricing(
provider: ApiProvider,
model: &str,
offering: &codewhale_config::catalog::CatalogOffering,
classes: &TokenUsage,
) -> Option<OfferingPricing> {
let mut pricing = OfferingPricing::from_catalog_offering(offering)?;
let model_lower = model.trim().to_ascii_lowercase();
let cache_uses_input_rate = matches!(
(provider, model_lower.as_str()),
(ApiProvider::Openai, "gpt-5.5-pro") | (ApiProvider::Arcee, "trinity-large-thinking")
);
if cache_uses_input_rate {
if classes.cache_read > 0 && pricing.cache_read_per_million.is_none() {
pricing.cache_read_per_million = pricing.input_per_million;
}
if classes.cache_write > 0 && pricing.cache_write_per_million.is_none() {
pricing.cache_write_per_million = pricing.input_per_million;
}
}
Some(pricing)
}
fn catalog_cost_estimate_for_route(
provider: ApiProvider,
model: &str,
offering: &codewhale_config::catalog::CatalogOffering,
usage: &Usage,
) -> Option<CostEstimate> {
let classes = token_usage_for_pricing(usage);
let pricing = effective_offering_pricing(provider, model, offering, &classes)?;
let amount = pricing.estimate_cost(&classes)?;
match pricing.currency {
Currency::Usd => Some(CostEstimate::usd_only(amount)),
Currency::Cny => Some(CostEstimate {
usd: 0.0,
cny: amount,
}),
Currency::Other(_) => None,
}
}
#[must_use]
pub fn token_usage_for_pricing(usage: &Usage) -> TokenUsage {
let total_input = usage.input_tokens;
let cache_read = usage.prompt_cache_hit_tokens.unwrap_or(0).min(total_input);
let after_read = total_input.saturating_sub(cache_read);
let cache_write = usage.prompt_cache_write_tokens.unwrap_or(0).min(after_read);
let after_write = after_read.saturating_sub(cache_write);
let non_cached_reported = usage
.prompt_cache_miss_tokens
.unwrap_or(after_write)
.min(after_write);
let uncategorized_input = after_write.saturating_sub(non_cached_reported);
let input = non_cached_reported.saturating_add(uncategorized_input);
let output = usage.output_tokens;
TokenUsage {
input: u64::from(input),
output: u64::from(output),
cache_read: u64::from(cache_read),
cache_write: u64::from(cache_write),
}
}
fn usage_cache_partition_is_consistent(usage: &Usage) -> bool {
let reported = u64::from(usage.prompt_cache_hit_tokens.unwrap_or(0))
+ u64::from(usage.prompt_cache_miss_tokens.unwrap_or(0))
+ u64::from(usage.prompt_cache_write_tokens.unwrap_or(0));
reported <= u64::from(usage.input_tokens)
}
fn calculate_turn_cost_from_usage_with_pricing(pricing: CurrencyPricing, usage: &Usage) -> f64 {
let usage = token_usage_for_pricing(usage);
let hit_cost = (usage.cache_read as f64 / 1_000_000.0) * pricing.input_cache_hit_per_million;
let miss_cost = (usage.input as f64 / 1_000_000.0) * pricing.input_cache_miss_per_million;
let write_rate = pricing
.cache_write
.rate(pricing.input_cache_miss_per_million)
.unwrap_or(0.0);
let write_cost = (usage.cache_write as f64 / 1_000_000.0) * write_rate;
let output_cost = (usage.output as f64 / 1_000_000.0) * pricing.output_per_million;
hit_cost + miss_cost + write_cost + output_cost
}
#[must_use]
#[cfg(test)]
pub fn calculate_cache_savings(model: &str, cache_hit_tokens: u32) -> Option<CostEstimate> {
if cache_hit_tokens == 0 {
return None;
}
if is_minimax_m3(model) {
return None;
}
let pricing = pricing_for_model(model)?;
let tokens = cache_hit_tokens as f64 / 1_000_000.0;
Some(CostEstimate {
usd: tokens
* (pricing.usd.input_cache_miss_per_million - pricing.usd.input_cache_hit_per_million),
cny: pricing
.cny
.map(|pricing| {
tokens
* (pricing.input_cache_miss_per_million - pricing.input_cache_hit_per_million)
})
.unwrap_or(0.0),
})
}
#[must_use]
pub fn format_cost_amount(cost: f64, currency: CostCurrency) -> String {
let symbol = currency.symbol();
if cost == 0.0 {
format!("{symbol}0.00")
} else if cost > 0.0 && cost < 0.0001 {
format!("<{symbol}0.0001")
} else if cost < 0.01 {
format!("{symbol}{cost:.4}")
} else {
format!("{symbol}{cost:.2}")
}
}
#[must_use]
pub fn format_cost_amount_precise(cost: f64, currency: CostCurrency) -> String {
let symbol = currency.symbol();
if cost == 0.0 {
format!("{symbol}0.0000")
} else if cost > 0.0 && cost < 0.0001 {
format!("<{symbol}0.0001")
} else {
format!("{symbol}{cost:.4}")
}
}
#[must_use]
pub fn format_cost_estimate(estimate: CostEstimate, currency: CostCurrency) -> String {
format_cost_amount(estimate.amount(currency), currency)
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::BTreeMap;
#[test]
fn malformed_catalog_row_has_an_explicit_runtime_reason() {
let offering = codewhale_config::catalog::CatalogOffering {
provider: "openrouter".to_string(),
wire_model_id: "openai/gpt-5.5".to_string(),
cost: Some(codewhale_config::models_dev::ModelsDevCost {
input: Some(f64::NAN),
output: Some(30.0),
cache_read: Some(0.05),
cache_write: None,
}),
..Default::default()
};
let audit = invalid_catalog_pricing_audit(&offering)
.expect("malformed row must become an explicit failed-closed audit");
assert!(!audit.is_priced());
assert_eq!(
audit.unpriced_reason,
Some(UnpricedReason::InvalidPricingRow)
);
assert_eq!(
audit.unpriced_reason.unwrap().label(),
"invalid_pricing_row"
);
}
#[test]
fn unpublished_cache_write_fails_closed_but_documented_same_rate_prices() {
let write_heavy = Usage {
input_tokens: 1_000_000,
output_tokens: 0,
prompt_cache_hit_tokens: Some(0),
prompt_cache_miss_tokens: Some(900_000),
prompt_cache_write_tokens: Some(100_000),
..Usage::default()
};
let now = Utc::now();
let deepseek = deepseek_v4_flash_pricing();
assert_eq!(
deepseek.usd.cache_write,
CacheWritePolicy::DocumentedAsInputRate(DEEPSEEK_CACHE_WRITE_IS_FREE)
);
let priced = audit_turn_cost_for_provider_at(
ApiProvider::Deepseek,
"deepseek-v4-flash",
&write_heavy,
now,
);
assert!(priced.is_priced(), "{priced:?}");
let expected = (0.9 + 0.1) * 0.14;
assert!(
(priced.estimate.expect("priced").usd - expected).abs() < 1e-12,
"{priced:?}"
);
let stepfun = pricing_for_billing_surface(
ApiProvider::Stepfun,
DEFAULT_STEPFUN_MODEL,
Some(STEPFUN_PAYG_BILLING_SURFACE),
)
.expect("StepFun PAYG row");
assert_eq!(stepfun.usd.cache_write, CacheWritePolicy::Unpublished);
let failed = audit_turn_cost_for_route_at(
ApiProvider::Stepfun,
DEFAULT_STEPFUN_MODEL,
Some(STEPFUN_PAYG_BILLING_SURFACE),
&write_heavy,
now,
);
assert!(!failed.is_priced(), "{failed:?}");
assert_eq!(
failed.unpriced_reason,
Some(UnpricedReason::MissingClassPrice)
);
assert_eq!(failed.unpriced_classes, vec![TokenClass::CacheWrite]);
let no_write = Usage {
prompt_cache_write_tokens: None,
..write_heavy.clone()
};
assert!(
audit_turn_cost_for_route_at(
ApiProvider::Stepfun,
DEFAULT_STEPFUN_MODEL,
Some(STEPFUN_PAYG_BILLING_SURFACE),
&no_write,
now,
)
.is_priced()
);
}
#[test]
fn endpoint_classification_covers_every_exact_billing_surface() {
for (provider, base_url, expected_surface, expected_metering) in [
(
ApiProvider::Zai,
"https://api.z.ai/api/coding/paas/v4",
ZAI_CODING_PLAN_BILLING_SURFACE,
EndpointMetering::ExactSubscription,
),
(
ApiProvider::Zai,
"https://api.z.ai/api/paas/v4",
ZAI_PAYG_BILLING_SURFACE,
EndpointMetering::Money,
),
(
ApiProvider::Moonshot,
crate::config::DEFAULT_KIMI_CODE_BASE_URL,
MOONSHOT_KIMI_CODE_BILLING_SURFACE,
EndpointMetering::ExactSubscription,
),
(
ApiProvider::Moonshot,
"https://api.moonshot.ai/v1",
MOONSHOT_PAYG_BILLING_SURFACE,
EndpointMetering::Money,
),
(
ApiProvider::XiaomiMimo,
crate::config::XIAOMI_MIMO_PAY_AS_YOU_GO_BASE_URL,
XIAOMI_PAYG_BILLING_SURFACE,
EndpointMetering::Money,
),
(
ApiProvider::XiaomiMimo,
crate::config::DEFAULT_XIAOMI_MIMO_BASE_URL,
XIAOMI_TOKEN_PLAN_BILLING_SURFACE,
EndpointMetering::ExactSubscription,
),
(
ApiProvider::Stepfun,
"https://api.stepfun.ai/step_plan/v1",
STEPFUN_PLAN_BILLING_SURFACE,
EndpointMetering::ExactSubscription,
),
(
ApiProvider::Stepfun,
"https://api.stepfun.ai/v1",
STEPFUN_PAYG_BILLING_SURFACE,
EndpointMetering::Money,
),
(
ApiProvider::Anthropic,
"https://api.anthropic.com/v1",
FIRST_PARTY_PAYG_BILLING_SURFACE,
EndpointMetering::Money,
),
(
ApiProvider::Openrouter,
"https://openrouter.ai/api/v1",
AGGREGATOR_BILLING_SURFACE,
EndpointMetering::Money,
),
] {
let surface = billing_surface_for_route(provider, Some(base_url));
assert_eq!(surface, Some(expected_surface), "{provider:?} {base_url}");
assert_eq!(
endpoint_metering_for_billing_surface(surface),
expected_metering,
"{provider:?} {base_url}"
);
}
for (provider, expected_surface, expected_metering) in [
(
ApiProvider::OpenaiCodex,
OAUTH_SUBSCRIPTION_BILLING_SURFACE,
EndpointMetering::ExactSubscription,
),
(
ApiProvider::OpencodeGo,
OAUTH_SUBSCRIPTION_BILLING_SURFACE,
EndpointMetering::ExactSubscription,
),
(
ApiProvider::Ollama,
LOCAL_BILLING_SURFACE,
EndpointMetering::LocalNoBill,
),
(
ApiProvider::OllamaCloud,
UNCLASSIFIED_BILLING_SURFACE,
EndpointMetering::Unknown,
),
(
ApiProvider::Vllm,
LOCAL_BILLING_SURFACE,
EndpointMetering::LocalNoBill,
),
(
ApiProvider::Custom,
UNCLASSIFIED_BILLING_SURFACE,
EndpointMetering::Unknown,
),
] {
let surface = billing_surface_for_route(provider, None);
assert_eq!(surface, Some(expected_surface), "{provider:?}");
assert_eq!(
endpoint_metering_for_billing_surface(surface),
expected_metering,
"{provider:?}"
);
}
for unknown in [
Some("some-future-surface"),
Some(""),
Some(" "),
Some(UNCLASSIFIED_BILLING_SURFACE),
None,
] {
assert_eq!(
endpoint_metering_for_billing_surface(unknown),
EndpointMetering::Unknown,
"{unknown:?}"
);
}
}
#[test]
fn an_unestablished_endpoint_is_never_priced_as_the_official_one() {
let usage = Usage {
input_tokens: 10_000,
output_tokens: 1_000,
..Usage::default()
};
let now = Utc::now();
for (provider, model) in [
(ApiProvider::Openai, "gpt-5.5"),
(ApiProvider::Anthropic, "claude-haiku-4-5"),
(ApiProvider::Deepseek, "deepseek-v4-flash"),
(ApiProvider::Openrouter, "openai/gpt-5.5"),
(ApiProvider::Moonshot, "kimi-k2.7-code"),
] {
let audit = audit_turn_cost_for_route_at(provider, model, None, &usage, now);
assert_eq!(
audit.unpriced_reason,
Some(UnpricedReason::UnestablishedEndpoint),
"{provider:?}/{model}: {audit:?}"
);
assert!(!audit.is_priced(), "{provider:?}/{model}: {audit:?}");
assert_eq!(audit.estimate, None, "{provider:?}/{model}");
assert!(
audit.counts_toward_money_coverage(),
"{provider:?}/{model}: an unknown route must not leave money coverage"
);
if provider == ApiProvider::Openrouter {
continue;
}
let classified = audit_turn_cost_for_route_at(
provider,
model,
billing_surface_for_route(provider, Some(provider.default_base_url())),
&usage,
now,
);
assert!(
classified.is_priced(),
"{provider:?}/{model} must price on its own official endpoint: {classified:?}"
);
}
let unplaceable = audit_turn_cost_for_route_at(
ApiProvider::Openai,
"gpt-5.5",
billing_surface_for_route(ApiProvider::Openai, Some("https://proxy.example/v1")),
&usage,
now,
);
assert_eq!(
unplaceable.unpriced_reason,
Some(UnpricedReason::UnknownBillingBasis)
);
}
#[test]
fn builtin_provider_names_do_not_price_unofficial_proxy_endpoints() {
let usage = Usage {
input_tokens: 10_000,
output_tokens: 1_000,
..Usage::default()
};
for (provider, model) in [
(ApiProvider::Deepseek, "deepseek-v4-flash"),
(ApiProvider::Openai, "gpt-5.5"),
(ApiProvider::Anthropic, "claude-haiku-4-5"),
(ApiProvider::Openrouter, "openai/gpt-5.5"),
] {
let surface = billing_surface_for_route(provider, Some("https://proxy.example/v1"));
assert_eq!(surface, Some(UNCLASSIFIED_BILLING_SURFACE), "{provider:?}");
let audit = audit_turn_cost_for_route_at(provider, model, surface, &usage, Utc::now());
assert_eq!(
audit.unpriced_reason,
Some(UnpricedReason::UnknownBillingBasis),
"{provider:?}: {audit:?}"
);
assert!(!audit.is_priced(), "{provider:?}: {audit:?}");
}
assert_eq!(
billing_surface_for_route(
ApiProvider::Moonshot,
Some(crate::config::DEFAULT_KIMI_CODE_BASE_URL)
),
Some(MOONSHOT_KIMI_CODE_BILLING_SURFACE)
);
for (provider, endpoint) in [
(ApiProvider::Minimax, "https://api.minimax.io/v1"),
(
ApiProvider::MinimaxAnthropic,
"https://api.minimax.io/anthropic",
),
(ApiProvider::Minimax, "https://api.minimax.io/v1/token-plan"),
(
ApiProvider::XiaomiMimo,
"https://token-plan-proxy.example/v1",
),
(
ApiProvider::Zai,
"https://api.z.ai/api/coding/something-else",
),
] {
assert_eq!(
billing_surface_for_route(provider, Some(endpoint)),
Some(UNCLASSIFIED_BILLING_SURFACE),
"{provider:?} {endpoint}"
);
}
}
#[test]
fn exact_plan_surface_overrides_a_metered_presentation() {
let usage = Usage {
input_tokens: 100_000,
output_tokens: 10_000,
..Usage::default()
};
let audit = audit_turn_cost_for_route(
ApiProvider::Zai,
"glm-5.2",
Some(ZAI_CODING_PLAN_BILLING_SURFACE),
&usage,
Utc::now(),
crate::route_billing::BillingPresentation::Metered,
);
assert!(!audit.is_priced(), "{audit:?}");
assert_eq!(audit.unpriced_reason, Some(UnpricedReason::NotMoneyMetered));
assert!(!audit.counts_toward_money_coverage());
let payg = audit_turn_cost_for_route(
ApiProvider::Zai,
"glm-5.2",
Some(ZAI_PAYG_BILLING_SURFACE),
&usage,
Utc::now(),
crate::route_billing::BillingPresentation::Metered,
);
assert!(payg.counts_toward_money_coverage(), "{payg:?}");
}
#[test]
fn unknown_billing_basis_is_not_excused_as_not_money_metered() {
let usage = Usage {
input_tokens: 10_000,
output_tokens: 1_000,
..Usage::default()
};
let unknown = audit_turn_cost_for_route(
ApiProvider::Anthropic,
"claude-haiku-4-5",
None,
&usage,
Utc::now(),
crate::route_billing::BillingPresentation::Unknown,
);
assert!(!unknown.is_priced());
assert_eq!(
unknown.unpriced_reason,
Some(UnpricedReason::UnknownBillingBasis)
);
assert!(unknown.counts_toward_money_coverage());
for billing in [
crate::route_billing::BillingPresentation::Local,
crate::route_billing::BillingPresentation::Subscription("plan"),
] {
let audit = audit_turn_cost_for_route(
ApiProvider::Anthropic,
"claude-haiku-4-5",
None,
&usage,
Utc::now(),
billing,
);
assert_eq!(audit.unpriced_reason, Some(UnpricedReason::NotMoneyMetered));
assert!(!audit.counts_toward_money_coverage());
}
}
#[test]
fn audit_names_why_a_turn_is_missing_from_a_total() {
let write_heavy = Usage {
input_tokens: 1_000_000,
output_tokens: 100_000,
prompt_cache_hit_tokens: Some(200_000),
prompt_cache_write_tokens: Some(100_000),
..Usage::default()
};
let priced = audit_turn_cost_for_provider_at(
ApiProvider::Anthropic,
"claude-haiku-4-5",
&write_heavy,
Utc::now(),
);
assert!(priced.is_priced());
assert_eq!(priced.unpriced_reason, None);
assert!(priced.unpriced_classes.is_empty());
assert!(priced.provenance.is_some());
let missing = audit_turn_cost_for_provider_at(
ApiProvider::Moonshot,
"kimi-k2.7-code",
&write_heavy,
Utc::now(),
);
assert!(!missing.is_priced());
assert_eq!(
missing.unpriced_reason,
Some(UnpricedReason::MissingClassPrice)
);
assert_eq!(missing.unpriced_classes, vec![TokenClass::CacheWrite]);
let no_write = Usage {
prompt_cache_write_tokens: None,
..write_heavy.clone()
};
assert!(
audit_turn_cost_for_provider_at(
ApiProvider::Moonshot,
"kimi-k2.7-code",
&no_write,
Utc::now(),
)
.is_priced()
);
assert_eq!(
audit_turn_cost_for_provider_at(
ApiProvider::OpenaiCodex,
"gpt-5.5",
&write_heavy,
Utc::now(),
)
.unpriced_reason,
Some(UnpricedReason::NotMoneyMetered)
);
assert_eq!(
audit_turn_cost_for_route_at(
ApiProvider::Stepfun,
DEFAULT_STEPFUN_MODEL,
None,
&write_heavy,
Utc::now(),
)
.unpriced_reason,
Some(UnpricedReason::AmbiguousBillingSurface)
);
assert_eq!(
audit_turn_cost_for_provider_at(
ApiProvider::Openai,
"gpt-5.5",
&Usage {
input_tokens: OPENAI_LONG_CONTEXT_SURCHARGE_THRESHOLD + 1,
..Usage::default()
},
Utc::now(),
)
.unpriced_reason,
Some(UnpricedReason::UnrepresentedTier)
);
}
#[test]
fn audit_and_estimate_never_disagree() {
let usage = Usage {
input_tokens: 10_000,
output_tokens: 1_000,
prompt_cache_hit_tokens: Some(2_000),
prompt_cache_write_tokens: Some(1_000),
..Usage::default()
};
let now = Utc::now();
for (provider, model) in [
(ApiProvider::Anthropic, "claude-haiku-4-5"),
(ApiProvider::Anthropic, "claude-sonnet-5"),
(ApiProvider::Moonshot, "kimi-k2.7-code"),
(ApiProvider::Openai, "gpt-5.5"),
(ApiProvider::OpenaiCodex, "gpt-5.5"),
(ApiProvider::Deepseek, "deepseek-v4-pro"),
(ApiProvider::Ollama, "gpt-5.5"),
(ApiProvider::Stepfun, DEFAULT_STEPFUN_MODEL),
] {
let audit = audit_turn_cost_for_route_at(provider, model, None, &usage, now);
let estimate =
calculate_turn_cost_estimate_for_route_at(provider, model, None, &usage, now);
assert_eq!(audit.estimate, estimate, "{provider:?}/{model}");
assert_eq!(
audit.is_priced(),
audit.unpriced_reason.is_none(),
"{provider:?}/{model}"
);
}
}
#[test]
fn nvidia_nim_deepseek_model_does_not_use_deepseek_platform_pricing() {
assert!(!has_pricing_for_model("deepseek-ai/deepseek-v4-pro"));
}
#[test]
fn stepfun_billing_surface_keeps_payg_separate_from_step_plan() {
for base_url in [
"https://api.stepfun.ai",
"https://api.stepfun.ai/",
"https://api.stepfun.ai/v1",
"https://API.STEPFUN.AI/v1/",
] {
assert_eq!(
billing_surface_for_route(ApiProvider::Stepfun, Some(base_url)),
Some(STEPFUN_PAYG_BILLING_SURFACE),
"{base_url}"
);
}
for base_url in [
"https://api.stepfun.ai/step_plan",
"https://api.stepfun.ai/step_plan/v1/",
"https://api.stepfun.com/step_plan/v1",
] {
assert_eq!(
billing_surface_for_route(ApiProvider::Stepfun, Some(base_url)),
Some(STEPFUN_PLAN_BILLING_SURFACE),
"{base_url}"
);
}
for base_url in [
"http://api.stepfun.ai/v1",
"https://token@api.stepfun.ai/v1",
"https://api.stepfun.ai/v1?account=other",
"https://api.stepfun.ai/STEP_PLAN/v1",
"https://stepfun.example/v1",
] {
assert_eq!(
billing_surface_for_route(ApiProvider::Stepfun, Some(base_url)),
Some(UNCLASSIFIED_BILLING_SURFACE),
"{base_url}"
);
assert_eq!(
endpoint_metering_for_billing_surface(Some(UNCLASSIFIED_BILLING_SURFACE)),
EndpointMetering::Unknown
);
}
assert_eq!(
billing_surface_for_route(ApiProvider::Openrouter, Some(DEFAULT_STEPFUN_BASE_URL)),
Some(UNCLASSIFIED_BILLING_SURFACE)
);
assert_eq!(
billing_surface_for_route(ApiProvider::Stepfun, None),
None,
"an absent endpoint is not a classification"
);
let usage = Usage {
input_tokens: 1_000_000,
output_tokens: 500_000,
prompt_cache_hit_tokens: Some(250_000),
..Default::default()
};
let payg = calculate_turn_cost_estimate_for_billing_surface(
ApiProvider::Stepfun,
DEFAULT_STEPFUN_MODEL,
Some(STEPFUN_PAYG_BILLING_SURFACE),
&usage,
)
.expect("standard StepFun API has an authoritative token price");
assert!((payg.usd - 0.735).abs() < 1e-12);
assert_eq!(payg.cny, 0.0);
assert!(
calculate_turn_cost_estimate_for_provider(
ApiProvider::Stepfun,
DEFAULT_STEPFUN_MODEL,
&usage,
)
.is_none()
);
assert!(
calculate_turn_cost_estimate_for_provider_at(
ApiProvider::Stepfun,
DEFAULT_STEPFUN_MODEL,
&usage,
Utc::now(),
)
.is_none()
);
assert!(!has_pricing_for_provider(
ApiProvider::Stepfun,
DEFAULT_STEPFUN_MODEL
));
for surface in [None, Some(STEPFUN_PLAN_BILLING_SURFACE)] {
assert!(
calculate_turn_cost_estimate_for_billing_surface(
ApiProvider::Stepfun,
DEFAULT_STEPFUN_MODEL,
surface,
&usage,
)
.is_none()
);
}
assert!(
calculate_turn_cost_estimate_for_billing_surface(
ApiProvider::Stepfun,
"step-3.5-flash",
Some(STEPFUN_PAYG_BILLING_SURFACE),
&usage,
)
.is_none()
);
for provider in [
ApiProvider::Openrouter,
ApiProvider::Ollama,
ApiProvider::Custom,
] {
assert!(
calculate_turn_cost_estimate_for_billing_surface(
provider,
DEFAULT_STEPFUN_MODEL,
Some(STEPFUN_PAYG_BILLING_SURFACE),
&usage,
)
.is_none(),
"{provider:?}"
);
assert!(
calculate_turn_cost_estimate_for_provider(provider, DEFAULT_STEPFUN_MODEL, &usage,)
.is_none(),
"{provider:?}"
);
assert!(
calculate_turn_cost_estimate_for_provider_at(
provider,
DEFAULT_STEPFUN_MODEL,
&usage,
Utc::now(),
)
.is_none(),
"{provider:?}"
);
assert!(
!has_pricing_for_provider(provider, DEFAULT_STEPFUN_MODEL),
"{provider:?}"
);
}
let recorded = calculate_turn_cost_estimate_for_route_at(
ApiProvider::Stepfun,
DEFAULT_STEPFUN_MODEL,
Some(STEPFUN_PAYG_BILLING_SURFACE),
&usage,
Utc::now(),
)
.expect("recorded PAYG route retains provider-scoped pricing");
assert_eq!(recorded, payg);
}
#[test]
fn catalog_sourced_models_have_usd_pricing() {
for (model, input, output) in [
("minimax-m2.7", 0.3, 1.2),
("minimax/minimax-m2.7", 0.3, 1.2),
("step-3.7-flash", 0.2, 1.15),
("fugu-ultra-20260615", 5.0, 30.0),
("fugu-ultra", 5.0, 30.0),
] {
let pricing = pricing_for_model_at(model, Utc::now()).expect(model);
assert_eq!(pricing.usd.input_cache_miss_per_million, input, "{model}");
assert_eq!(pricing.usd.output_per_million, output, "{model}");
assert!(has_pricing_for_model(model));
}
}
#[test]
fn trinity_mini_stays_unpriced_without_verified_provider_rates() {
let usage = Usage {
input_tokens: 1_000,
output_tokens: 100,
..Usage::default()
};
assert!(pricing_for_model_at("trinity-mini", Utc::now()).is_none());
assert!(!has_pricing_for_model("trinity-mini"));
assert!(!has_pricing_for_provider(
ApiProvider::Arcee,
"trinity-mini"
));
assert!(
calculate_turn_cost_estimate_for_provider(ApiProvider::Arcee, "trinity-mini", &usage,)
.is_none()
);
}
#[test]
fn minimax_m3_standard_pricing_tracks_the_512k_input_boundary() {
for model in ["MiniMax-M3", "minimax/minimax-m3"] {
for (input_tokens, cache_read, input, output) in
[(512_000, 0.06, 0.30, 1.20), (512_001, 0.12, 0.60, 2.40)]
{
let usage = Usage {
input_tokens,
..Usage::default()
};
let pricing = pricing_for_model_and_usage(model, &usage).expect("M3 pricing");
assert_eq!(pricing.usd.input_cache_hit_per_million, cache_read);
assert_eq!(pricing.usd.input_cache_miss_per_million, input);
assert_eq!(pricing.usd.output_per_million, output);
}
assert!(calculate_cache_savings(model, 1).is_none());
}
}
#[test]
fn grok_46_pricing_tracks_the_200k_prompt_boundary() {
for (input_tokens, cache_read, input, output) in
[(199_999, 0.50, 2.00, 6.00), (200_000, 1.00, 4.00, 12.00)]
{
let usage = Usage {
input_tokens,
..Usage::default()
};
let pricing =
pricing_for_model_and_usage("grok-4.6", &usage).expect("Grok 4.6 pricing");
assert_eq!(pricing.usd.input_cache_hit_per_million, cache_read);
assert_eq!(pricing.usd.input_cache_miss_per_million, input);
assert_eq!(pricing.usd.output_per_million, output);
}
}
#[test]
fn direct_xai_grok_46_owns_usage_tier_without_leaking_to_other_providers() {
for (input_tokens, input_rate) in [(199_999, 2.00), (200_000, 4.00)] {
let usage = Usage {
input_tokens,
..Usage::default()
};
let estimate = calculate_turn_cost_estimate_for_provider_at(
ApiProvider::Xai,
"grok-4.6",
&usage,
Utc::now(),
)
.expect("direct xAI route has authoritative tiered pricing");
let expected = f64::from(input_tokens) / 1_000_000.0 * input_rate;
assert!((estimate.usd - expected).abs() < 1e-12);
}
assert!(
provider_owned_hand_pricing_at(ApiProvider::Openrouter, "grok-4.6", Utc::now(),)
.is_none()
);
}
#[test]
fn provider_scoped_minimax_m3_keeps_usage_tiers_for_both_wire_protocols() {
for provider in [ApiProvider::Minimax, ApiProvider::MinimaxAnthropic] {
for (input_tokens, input_rate) in [(512_000, 0.30), (512_001, 0.60)] {
let usage = Usage {
input_tokens,
..Usage::default()
};
let estimate = calculate_turn_cost_estimate_for_provider_at(
provider,
"MiniMax-M3",
&usage,
Utc::now(),
)
.expect("direct MiniMax route has authoritative pricing");
let expected = f64::from(input_tokens) / 1_000_000.0 * input_rate;
assert!((estimate.usd - expected).abs() < 1e-12, "{provider:?}");
}
}
}
#[test]
fn direct_openai_long_context_estimates_fail_closed_above_272k() {
for model in [
"gpt-5.5",
"gpt-5.6",
"gpt-5.6-sol",
"gpt-5.6-terra",
"gpt-5.6-luna",
] {
let at_boundary = Usage {
input_tokens: OPENAI_LONG_CONTEXT_SURCHARGE_THRESHOLD,
..Usage::default()
};
let above_boundary = Usage {
input_tokens: OPENAI_LONG_CONTEXT_SURCHARGE_THRESHOLD + 1,
..Usage::default()
};
assert!(
calculate_turn_cost_estimate_for_provider(
ApiProvider::Openai,
model,
&at_boundary,
)
.is_some(),
"{model} should retain its standard price at 272K"
);
assert!(
calculate_turn_cost_estimate_for_provider(
ApiProvider::Openai,
model,
&above_boundary,
)
.is_none(),
"{model} must not report the lower static price above 272K"
);
}
}
#[test]
fn direct_openai_gpt54_family_is_guarded_even_without_a_bundled_catalog_row() {
for model in ["gpt-5.4", "gpt-5.4-pro"] {
assert!(!direct_openai_long_context_tier_is_unpriced(
ApiProvider::Openai,
model,
OPENAI_LONG_CONTEXT_SURCHARGE_THRESHOLD,
));
assert!(direct_openai_long_context_tier_is_unpriced(
ApiProvider::Openai,
model,
OPENAI_LONG_CONTEXT_SURCHARGE_THRESHOLD + 1,
));
let above_boundary = Usage {
input_tokens: OPENAI_LONG_CONTEXT_SURCHARGE_THRESHOLD + 1,
..Usage::default()
};
assert!(
calculate_turn_cost_estimate_for_provider(
ApiProvider::Openai,
model,
&above_boundary,
)
.is_none(),
"{model} must remain unpriced if a live catalog row is available"
);
}
}
#[test]
fn openai_long_context_guard_is_exact_and_provider_scoped() {
let input_tokens = OPENAI_LONG_CONTEXT_SURCHARGE_THRESHOLD + 1;
for provider in [
ApiProvider::Openrouter,
ApiProvider::OpenaiCodex,
ApiProvider::Ollama,
ApiProvider::Custom,
] {
assert!(
!direct_openai_long_context_tier_is_unpriced(provider, "gpt-5.5", input_tokens,),
"{provider:?} must not inherit direct OpenAI tier handling"
);
}
for model in [
"gpt-5.4-mini",
"gpt-5.4-nano",
"gpt-5.5-pro",
"gpt-5.5-pro-2026-04-23",
"gpt-5.5-2026-04-23-extra",
"openai/gpt-5.5",
"gpt-5.6-sol-preview",
] {
assert!(
!direct_openai_long_context_tier_is_unpriced(
ApiProvider::Openai,
model,
input_tokens,
),
"non-documented id {model} must not be treated as an alias"
);
}
let usage = Usage {
input_tokens,
output_tokens: 1,
..Usage::default()
};
assert!(calculate_turn_cost_estimate_from_usage("gpt-5.5", &usage).is_some());
assert!(
calculate_turn_cost_estimate_for_provider(ApiProvider::OpenaiCodex, "gpt-5.5", &usage,)
.is_none()
);
}
#[test]
fn direct_openai_snapshots_use_the_same_strict_272k_boundary() {
for snapshot in [
"gpt-5.4-2026-03-05",
"gpt-5.4-pro-2026-03-05",
"gpt-5.5-2026-04-23",
] {
assert!(!direct_openai_long_context_tier_is_unpriced(
ApiProvider::Openai,
snapshot,
OPENAI_LONG_CONTEXT_SURCHARGE_THRESHOLD,
));
assert!(direct_openai_long_context_tier_is_unpriced(
ApiProvider::Openai,
snapshot,
OPENAI_LONG_CONTEXT_SURCHARGE_THRESHOLD + 1,
));
let above_boundary = Usage {
input_tokens: OPENAI_LONG_CONTEXT_SURCHARGE_THRESHOLD + 1,
..Usage::default()
};
assert!(
calculate_turn_cost_estimate_for_provider(
ApiProvider::Openai,
snapshot,
&above_boundary,
)
.is_none(),
"{snapshot} must not report the lower static price above 272K"
);
}
}
#[test]
fn direct_openai_long_context_guard_uses_total_input_with_mixed_cache_classes() {
let at_boundary = Usage {
input_tokens: OPENAI_LONG_CONTEXT_SURCHARGE_THRESHOLD,
output_tokens: 1_000,
prompt_cache_hit_tokens: Some(100_000),
prompt_cache_miss_tokens: Some(100_000),
prompt_cache_write_tokens: Some(72_000),
..Usage::default()
};
let above_boundary = Usage {
input_tokens: OPENAI_LONG_CONTEXT_SURCHARGE_THRESHOLD + 1,
prompt_cache_write_tokens: Some(72_001),
..at_boundary.clone()
};
assert!(
calculate_turn_cost_estimate_for_provider(
ApiProvider::Openai,
"gpt-5.6-sol",
&at_boundary,
)
.is_some()
);
assert!(
calculate_turn_cost_estimate_for_provider(
ApiProvider::Openai,
"gpt-5.6-sol",
&above_boundary,
)
.is_none()
);
}
#[test]
fn minimax_m2_7_preserves_cache_read_and_write_rates() {
let pricing = pricing_for_model_at("MiniMax-M2.7", Utc::now()).expect("M2.7 pricing");
assert_eq!(pricing.usd.input_cache_hit_per_million, 0.06);
assert_eq!(pricing.usd.input_cache_miss_per_million, 0.30);
assert_eq!(pricing.usd.output_per_million, 1.20);
assert_eq!(pricing.usd.cache_write, CacheWritePolicy::Rate(0.375));
}
#[test]
fn curated_usd_only_models_have_pricing_and_accrue_cost() {
let usage = Usage {
input_tokens: 1_000_000,
output_tokens: 500_000,
prompt_cache_hit_tokens: Some(250_000),
prompt_cache_miss_tokens: Some(750_000),
..Default::default()
};
for (model, hit, miss, output) in [
("kimi-k2.6", 0.16, 0.95, 4.00),
("kimi-k2.7-code", 0.19, 0.95, 4.00),
("moonshotai/kimi-k2.7-code", 0.19, 0.95, 4.00),
("z-ai/glm-5.1", 0.26, 1.40, 4.40),
("glm-5.2", 0.26, 1.40, 4.40),
("z-ai/glm-5.2", 0.26, 1.40, 4.40),
("glm-5-turbo", 0.24, 1.20, 4.00),
("z-ai/glm-5-turbo", 0.24, 1.20, 4.00),
("qwen/qwen3.6-plus", 0.325, 0.325, 1.95),
("qwen/qwen3.6-35b-a3b", 0.05, 0.14, 1.00),
("qwen/qwen3.6-27b", 0.15, 0.285, 2.40),
("trinity-large-thinking", 0.25, 0.25, 0.80),
("nvidia/nemotron-3-ultra-550b-a55b", 0.10, 0.50, 2.20),
("claude-opus-4-8", 0.50, 5.00, 25.00),
("claude-sonnet-4-6", 0.30, 3.00, 15.00),
("claude-haiku-4-5", 0.10, 1.00, 5.00),
("claude-fable-5", 1.00, 10.00, 50.00),
("gpt-5.5", 0.50, 5.00, 30.00),
("gpt-5.5-pro", 30.00, 30.00, 180.00),
("gpt-5.6-sol", 0.50, 5.00, 30.00),
("gpt-5.6-terra", 0.25, 2.50, 15.00),
("gpt-5.6-luna", 0.10, 1.00, 6.00),
("gpt-5-codex", 0.125, 1.25, 10.00),
("gpt-5.3-codex", 0.175, 1.75, 14.00),
("qwen/qwen3.7-plus", 0.064, 0.32, 1.28),
("muse-spark-1.1", 0.15, 1.25, 4.25),
("muse-spark-1.2", 0.15, 1.25, 4.25),
("muse-spark-1.2-contributor", 0.002, 0.10, 0.20),
] {
let pricing = pricing_for_model_at(model, Utc::now()).expect(model);
assert_eq!(pricing.usd.input_cache_hit_per_million, hit);
assert_eq!(pricing.usd.input_cache_miss_per_million, miss);
assert_eq!(pricing.usd.output_per_million, output);
assert!(pricing.cny.is_none());
assert!(has_pricing_for_model(model));
let estimate = calculate_turn_cost_estimate_from_usage(model, &usage).expect(model);
assert!(estimate.usd > 0.0, "expected positive USD for {model}");
assert_eq!(estimate.cny, 0.0);
}
for (model, write) in [
("claude-opus-4-8", CacheWritePolicy::Rate(6.25)),
("claude-sonnet-4-6", CacheWritePolicy::Rate(3.75)),
("claude-haiku-4-5", CacheWritePolicy::Rate(1.25)),
("claude-fable-5", CacheWritePolicy::Rate(12.50)),
("qwen/qwen3.7-plus", CacheWritePolicy::Rate(0.40)),
("gpt-5.5", CacheWritePolicy::Unpublished),
] {
let pricing = pricing_for_model_at(model, Utc::now()).expect(model);
assert_eq!(
pricing.usd.cache_write, write,
"cache-write policy for {model}"
);
}
}
#[test]
fn glm_5_3_has_no_hardcoded_price() {
for model in ["glm-5.3", "z-ai/glm-5.3"] {
assert!(
pricing_for_model_at(model, Utc::now()).is_none(),
"{model} must have no price row until Z.ai publishes one"
);
assert!(!has_pricing_for_model(model), "{model} must be unpriced");
assert!(
calculate_turn_cost_estimate_from_usage(
model,
&Usage {
input_tokens: 1_000_000,
output_tokens: 500_000,
..Default::default()
},
)
.is_none(),
"{model} must not accrue an invented cost estimate"
);
}
assert!(has_pricing_for_model("glm-5.2"));
}
#[test]
fn cache_write_tokens_increase_anthropic_cost_estimate() {
let with_write = Usage {
input_tokens: 12_048,
output_tokens: 1,
prompt_cache_hit_tokens: Some(10_000),
prompt_cache_miss_tokens: Some(3),
prompt_cache_write_tokens: Some(2_045),
..Default::default()
};
let write_as_miss = Usage {
input_tokens: 12_048,
output_tokens: 1,
prompt_cache_hit_tokens: Some(10_000),
prompt_cache_miss_tokens: Some(2_048),
prompt_cache_write_tokens: None,
..Default::default()
};
let priced =
calculate_turn_cost_estimate_from_usage("claude-fable-5", &with_write).expect("priced");
let undercounted =
calculate_turn_cost_estimate_from_usage("claude-fable-5", &write_as_miss)
.expect("priced");
assert!(
priced.usd > undercounted.usd,
"write premium should raise cost: priced={} undercounted={}",
priced.usd,
undercounted.usd
);
let expected_premium = (2_045.0 / 1_000_000.0) * (12.50 - 10.00);
assert!(
(priced.usd - undercounted.usd - expected_premium).abs() < 1e-9,
"premium delta mismatch: {}",
priced.usd - undercounted.usd
);
}
#[test]
fn catalog_pricing_uses_its_cache_write_rate() {
let offering = codewhale_config::catalog::CatalogOffering {
provider: "anthropic".to_string(),
wire_model_id: "catalog-priced-model".to_string(),
endpoint_key: "chat".to_string(),
cost: Some(codewhale_config::models_dev::ModelsDevCost {
input: Some(10.0),
output: Some(50.0),
cache_read: Some(1.0),
cache_write: Some(12.5),
}),
..Default::default()
};
let usage = Usage {
input_tokens: 13,
output_tokens: 5,
prompt_cache_hit_tokens: Some(2),
prompt_cache_miss_tokens: Some(3),
prompt_cache_write_tokens: Some(8),
..Default::default()
};
let estimate = catalog_cost_estimate_for_route(
ApiProvider::Anthropic,
"catalog-priced-model",
&offering,
&usage,
)
.expect("catalog cost estimate");
assert!((estimate.usd - 0.000_382).abs() < 1e-15);
assert_eq!(estimate.cny, 0.0);
}
#[test]
fn recorded_time_provider_cost_keeps_catalog_cache_write_tier() {
let usage = Usage {
input_tokens: 1_000_000,
output_tokens: 0,
prompt_cache_hit_tokens: Some(0),
prompt_cache_miss_tokens: Some(0),
prompt_cache_write_tokens: Some(1_000_000),
..Default::default()
};
let estimate = calculate_turn_cost_estimate_for_provider_at(
ApiProvider::Openrouter,
"qwen/qwen3.7-plus",
&usage,
Utc::now(),
)
.expect("provider catalog write price");
assert!((estimate.usd - 0.40).abs() < f64::EPSILON);
assert_eq!(estimate.cny, 0.0);
}
#[test]
fn recorded_time_provider_cost_rejects_foreign_model_ids() {
let usage = Usage {
input_tokens: 1_000,
output_tokens: 100,
..Default::default()
};
assert!(
calculate_turn_cost_estimate_for_provider_at(
ApiProvider::Ollama,
"gpt-5.5",
&usage,
Utc::now(),
)
.is_none()
);
}
#[test]
fn provider_cost_keeps_owned_hand_price_without_catalog_offering() {
let usage = Usage {
input_tokens: 1_000_000,
output_tokens: 0,
..Default::default()
};
assert!(
crate::provider_lake::catalog_offering_for_model(ApiProvider::Openai, "gpt-5-codex")
.is_none(),
"regression fixture must exercise the hand-price fallback"
);
let estimate = calculate_turn_cost_estimate_for_provider_at(
ApiProvider::Openai,
"gpt-5-codex",
&usage,
Utc::now(),
)
.expect("OpenAI API owns the hand-priced model");
assert!((estimate.usd - 1.25).abs() < f64::EPSILON);
assert_eq!(estimate.cny, 0.0);
assert!(has_pricing_for_provider(ApiProvider::Openai, "gpt-5-codex"));
}
#[test]
fn provider_price_does_not_invent_catalog_missing_cache_write_class() {
let offering =
crate::provider_lake::catalog_offering_for_model(ApiProvider::Openai, "gpt-5.5")
.expect("bundled OpenAI route");
let catalog_pricing =
OfferingPricing::from_catalog_offering(&offering).expect("catalog pricing");
assert!(catalog_pricing.cache_write_per_million.is_none());
let usage = Usage {
input_tokens: 250_000,
output_tokens: 0,
prompt_cache_miss_tokens: Some(0),
prompt_cache_write_tokens: Some(250_000),
..Default::default()
};
let audit =
audit_turn_cost_for_provider_at(ApiProvider::Openai, "gpt-5.5", &usage, Utc::now());
assert!(audit.estimate.is_none());
assert_eq!(
audit.unpriced_reason,
Some(UnpricedReason::MissingClassPrice)
);
assert_eq!(audit.unpriced_classes, vec![TokenClass::CacheWrite]);
}
#[test]
fn provider_cost_does_not_fabricate_price_for_costless_catalog_route() {
let offering = crate::provider_lake::catalog_offering_for_model(
ApiProvider::Openai,
"deepseek-v4-pro",
)
.expect("bundled OpenAI-compatible route");
assert!(OfferingPricing::from_catalog_offering(&offering).is_none());
let usage = Usage {
input_tokens: 1_000_000,
output_tokens: 0,
..Default::default()
};
assert!(
calculate_turn_cost_estimate_for_provider_at(
ApiProvider::Openai,
"deepseek-v4-pro",
&usage,
Utc::now(),
)
.is_none()
);
assert!(
calculate_turn_cost_estimate_for_provider(
ApiProvider::Openai,
"deepseek-v4-pro",
&usage,
)
.is_none()
);
assert!(!has_pricing_for_provider(
ApiProvider::Openai,
"deepseek-v4-pro"
));
}
#[test]
fn recorded_time_provider_cost_bounds_deepseek_compatibility_aliases() {
let usage = Usage {
input_tokens: 1_000,
output_tokens: 100,
..Default::default()
};
let before_retirement: DateTime<Utc> =
"2026-07-24T15:58:59Z".parse().expect("pre-retirement time");
let at_retirement: DateTime<Utc> = DEEPSEEK_ALIAS_RETIREMENT_UTC
.parse()
.expect("retirement time");
assert!(
calculate_turn_cost_estimate_for_provider_at(
ApiProvider::Deepseek,
"deepseek-chat",
&usage,
before_retirement,
)
.is_some()
);
assert!(
calculate_turn_cost_estimate_for_provider_at(
ApiProvider::Deepseek,
"deepseek-reasoner",
&usage,
at_retirement,
)
.is_none()
);
}
#[test]
fn token_usage_for_pricing_maps_cache_classes_without_double_billing_reasoning() {
let usage = Usage {
input_tokens: 1_000,
output_tokens: 100,
prompt_cache_hit_tokens: Some(250),
prompt_cache_miss_tokens: Some(700),
prompt_cache_write_tokens: Some(50),
reasoning_tokens: Some(50),
..Default::default()
};
assert_eq!(
token_usage_for_pricing(&usage),
TokenUsage {
input: 700,
output: 100,
cache_read: 250,
cache_write: 50,
}
);
let without_reasoning = Usage {
reasoning_tokens: None,
..usage.clone()
};
assert_eq!(
token_usage_for_pricing(&usage).output,
token_usage_for_pricing(&without_reasoning).output
);
assert_eq!(
calculate_turn_cost_estimate_for_provider(
ApiProvider::Anthropic,
"claude-haiku-4-5",
&usage,
),
calculate_turn_cost_estimate_for_provider(
ApiProvider::Anthropic,
"claude-haiku-4-5",
&without_reasoning,
)
);
}
#[test]
fn contradictory_cache_partition_is_bounded_and_fails_closed() {
let usage = Usage {
input_tokens: 100,
output_tokens: 10,
prompt_cache_hit_tokens: Some(80),
prompt_cache_miss_tokens: Some(40),
prompt_cache_write_tokens: Some(30),
..Usage::default()
};
let classes = token_usage_for_pricing(&usage);
assert_eq!(
classes.input + classes.cache_read + classes.cache_write,
u64::from(usage.input_tokens),
"token projection may never exceed the provider's input total"
);
let audit = audit_turn_cost_for_provider_on_endpoint_at(
ApiProvider::Deepseek,
"deepseek-v4-flash",
None,
&usage,
Utc::now(),
);
assert!(audit.estimate.is_none());
assert_eq!(
audit.unpriced_reason,
Some(UnpricedReason::InconsistentUsage)
);
let overflow_shape = Usage {
input_tokens: u32::MAX,
prompt_cache_hit_tokens: Some(u32::MAX),
prompt_cache_miss_tokens: Some(1),
..Usage::default()
};
assert!(
!usage_cache_partition_is_consistent(&overflow_shape),
"consistency validation must not hide overflow via saturation"
);
}
#[test]
fn openai_codex_gpt55_cost_is_unavailable_even_with_usage() {
let usage = Usage {
input_tokens: 1_000,
output_tokens: 100,
prompt_cache_hit_tokens: Some(250),
prompt_cache_miss_tokens: Some(750),
..Default::default()
};
assert!(calculate_turn_cost_estimate_from_usage("gpt-5.5", &usage).is_some());
assert!(has_pricing_for_provider(ApiProvider::Openai, "gpt-5.5"));
assert!(!has_pricing_for_provider(
ApiProvider::OpenaiCodex,
"gpt-5.5"
));
assert!(
calculate_turn_cost_estimate_for_provider(ApiProvider::OpenaiCodex, "gpt-5.5", &usage)
.is_none()
);
}
#[test]
fn subscription_route_does_not_inherit_same_models_api_price() {
let usage = Usage {
input_tokens: 1_000,
output_tokens: 100,
..Default::default()
};
assert!(
calculate_turn_cost_estimate_for_billing_surface(
ApiProvider::Anthropic,
"claude-sonnet-5",
Some(FIRST_PARTY_PAYG_BILLING_SURFACE),
&usage,
)
.is_some()
);
assert!(
calculate_turn_cost_estimate_for_route(
ApiProvider::Anthropic,
"claude-sonnet-5",
&usage,
crate::route_billing::BillingPresentation::Subscription("Claude OAuth quota"),
)
.is_none()
);
}
#[test]
fn token_usage_for_pricing_infers_missing_cache_miss_from_hit_source() {
let usage = Usage {
input_tokens: 1_000,
output_tokens: 100,
prompt_cache_hit_tokens: Some(250),
prompt_cache_miss_tokens: None,
..Default::default()
};
assert_eq!(
token_usage_for_pricing(&usage),
TokenUsage {
input: 750,
output: 100,
cache_read: 250,
cache_write: 0,
}
);
}
#[test]
fn catalog_pricing_overrides_known_row_when_present() {
let _lock = crate::model_catalog::test_catalog_lock();
let mut overrides = BTreeMap::new();
overrides.insert(
"catalog-priced-model".to_string(),
crate::model_catalog::CatalogEntry {
id: "catalog-priced-model".to_string(),
context_window: None,
max_output: None,
supports_reasoning: None,
input_usd_per_million: Some(0.25),
output_usd_per_million: Some(1.25),
modalities: Vec::new(),
supported_parameters: Vec::new(),
provider_model_id: None,
provenance: crate::model_catalog::MetadataProvenance::UserOverride,
},
);
let catalog = crate::model_catalog::MergedCatalog::from_sources(
overrides,
None,
crate::model_catalog::bundled_catalog(),
Utc::now(),
);
let _guard = crate::model_catalog::replace_active_catalog_for_test(catalog);
let pricing = pricing_for_model_at("catalog-priced-model", Utc::now()).expect("pricing");
assert_eq!(pricing.usd.input_cache_hit_per_million, 0.25);
assert_eq!(pricing.usd.input_cache_miss_per_million, 0.25);
assert_eq!(pricing.usd.output_per_million, 1.25);
assert!(pricing.cny.is_none());
}
#[test]
fn sonnet_5_uses_intro_pricing_before_2026_08_31_expiry() {
let before_expiry = Utc
.with_ymd_and_hms(2026, 8, 31, 23, 59, 59)
.single()
.unwrap();
let pricing = pricing_for_model_at("claude-sonnet-5", before_expiry).unwrap();
assert_eq!(pricing.usd.input_cache_hit_per_million, 0.20);
assert_eq!(pricing.usd.input_cache_miss_per_million, 2.00);
assert_eq!(pricing.usd.output_per_million, 10.00);
assert_eq!(pricing.usd.cache_write, CacheWritePolicy::Rate(2.50));
assert!(pricing.cny.is_none());
}
#[test]
fn sonnet_5_uses_standard_pricing_after_intro_window() {
let after_expiry = Utc.with_ymd_and_hms(2026, 9, 1, 0, 0, 0).single().unwrap();
let pricing = pricing_for_model_at("claude-sonnet-5", after_expiry).unwrap();
assert_eq!(pricing.usd.input_cache_hit_per_million, 0.30);
assert_eq!(pricing.usd.input_cache_miss_per_million, 3.00);
assert_eq!(pricing.usd.output_per_million, 15.00);
assert_eq!(pricing.usd.cache_write, CacheWritePolicy::Rate(3.75));
assert!(pricing.cny.is_none());
assert!(has_pricing_for_model("claude-sonnet-5"));
}
#[test]
fn v4_pro_uses_limited_time_discount_before_expiry() {
let before_expiry = Utc
.with_ymd_and_hms(2026, 5, 31, 15, 58, 59)
.single()
.unwrap();
let pricing = pricing_for_model_at("deepseek-v4-pro", before_expiry).unwrap();
assert_eq!(pricing.usd.input_cache_hit_per_million, 0.003625);
assert_eq!(pricing.usd.input_cache_miss_per_million, 0.435);
assert_eq!(pricing.usd.output_per_million, 0.87);
let cny = pricing.cny.expect("DeepSeek pricing has CNY");
assert_eq!(cny.input_cache_hit_per_million, 0.025);
assert_eq!(cny.input_cache_miss_per_million, 3.0);
assert_eq!(cny.output_per_million, 6.0);
}
#[test]
fn v4_pro_keeps_adjusted_rates_after_discount_window() {
let after_expiry = Utc.with_ymd_and_hms(2026, 6, 1, 0, 0, 0).single().unwrap();
let pricing = pricing_for_model_at("deepseek-v4-pro", after_expiry).unwrap();
assert_eq!(pricing.usd.input_cache_hit_per_million, 0.003625);
assert_eq!(pricing.usd.input_cache_miss_per_million, 0.435);
assert_eq!(pricing.usd.output_per_million, 0.87);
let cny = pricing.cny.expect("DeepSeek pricing has CNY");
assert_eq!(cny.input_cache_hit_per_million, 0.025);
assert_eq!(cny.input_cache_miss_per_million, 3.0);
assert_eq!(cny.output_per_million, 6.0);
}
#[test]
fn v4_pro_discount_still_applies_just_before_old_may5_expiry() {
let after_old_expiry = Utc.with_ymd_and_hms(2026, 5, 6, 0, 0, 0).single().unwrap();
let pricing = pricing_for_model_at("deepseek-v4-pro", after_old_expiry).unwrap();
assert_eq!(pricing.usd.input_cache_hit_per_million, 0.003625);
assert_eq!(pricing.usd.input_cache_miss_per_million, 0.435);
assert_eq!(pricing.usd.output_per_million, 0.87);
}
#[test]
fn fireworks_and_zen_flash_use_bundled_family_rates() {
let now = Utc.with_ymd_and_hms(2026, 8, 14, 0, 0, 0).single().unwrap();
let fireworks = provider_owned_hand_pricing_at(
ApiProvider::Fireworks,
"accounts/fireworks/models/deepseek-v4-flash",
now,
)
.expect("Fireworks Flash should inherit the bundled DeepSeek family row");
let zen =
provider_owned_hand_pricing_at(ApiProvider::OpencodeZen, "deepseek-v4-flash", now)
.expect("OpenCode Zen Flash should inherit the bundled DeepSeek family row");
assert_eq!(fireworks.usd.output_per_million, zen.usd.output_per_million);
assert!(
provider_owned_hand_pricing_at(
ApiProvider::Fireworks,
"accounts/fireworks/models/kimi-k3",
now,
)
.is_none(),
"kimi-k3 has no published bundled rate; do not invent one"
);
}
#[test]
fn v4_flash_keeps_current_published_rates() {
let now = Utc.with_ymd_and_hms(2026, 4, 25, 0, 0, 0).single().unwrap();
let pricing = pricing_for_model_at("deepseek-v4-flash", now).unwrap();
assert_eq!(pricing.usd.input_cache_hit_per_million, 0.0028);
assert_eq!(pricing.usd.input_cache_miss_per_million, 0.14);
assert_eq!(pricing.usd.output_per_million, 0.28);
let cny = pricing.cny.expect("DeepSeek pricing has CNY");
assert_eq!(cny.input_cache_hit_per_million, 0.02);
assert_eq!(cny.input_cache_miss_per_million, 1.0);
assert_eq!(cny.output_per_million, 2.0);
}
#[test]
fn xiaomi_mimo_token_plan_models_leave_cost_unknown() {
let now = Utc.with_ymd_and_hms(2026, 6, 4, 0, 0, 0).single().unwrap();
for model in [
"mimo-v2.5-pro",
"mimo-v2.5-pro-ultraspeed",
"mimo-v2.5",
"xiaomi/mimo-v2.5",
] {
assert!(pricing_for_model_at(model, now).is_none());
assert!(!has_pricing_for_model(model));
}
}
#[test]
fn cost_estimate_calculates_usd_and_cny() {
let usage = Usage {
input_tokens: 1_000_000,
output_tokens: 500_000,
..Default::default()
};
let estimate =
calculate_turn_cost_estimate_from_usage("deepseek-v4-flash", &usage).expect("estimate");
assert_eq!(estimate.usd, 0.28);
assert_eq!(estimate.cny, 2.0);
}
#[test]
fn cost_currency_accepts_yuan_aliases() {
assert_eq!(CostCurrency::from_setting("usd"), Some(CostCurrency::Usd));
assert_eq!(CostCurrency::from_setting("yuan"), Some(CostCurrency::Cny));
assert_eq!(CostCurrency::from_setting("rmb"), Some(CostCurrency::Cny));
assert_eq!(CostCurrency::from_setting("cny"), Some(CostCurrency::Cny));
assert_eq!(CostCurrency::from_setting("eur"), None);
}
#[test]
fn format_cost_amount_uses_selected_symbol() {
assert_eq!(format_cost_amount(0.42, CostCurrency::Usd), "$0.42");
assert_eq!(format_cost_amount(2.0, CostCurrency::Cny), "¥2.00");
assert_eq!(format_cost_amount(0.0, CostCurrency::Usd), "$0.00");
assert_eq!(format_cost_amount(0.00001, CostCurrency::Usd), "<$0.0001");
}
#[test]
fn format_cost_amount_precise_keeps_report_precision() {
assert_eq!(
format_cost_amount_precise(0.1234, CostCurrency::Usd),
"$0.1234"
);
assert_eq!(
format_cost_amount_precise(0.1234, CostCurrency::Cny),
"¥0.1234"
);
assert_eq!(
format_cost_amount_precise(0.0, CostCurrency::Usd),
"$0.0000"
);
assert_eq!(
format_cost_amount_precise(0.00001, CostCurrency::Usd),
"<$0.0001"
);
}
#[test]
fn accumulated_cost_stays_finite_and_nonnegative() {
let saturated = CostEstimate {
usd: f64::MAX,
cny: 1.0,
}
.saturating_add(CostEstimate {
usd: f64::MAX,
cny: -1.0,
});
assert_eq!(saturated.usd, f64::MAX);
assert_eq!(saturated.cny, 1.0);
assert!(saturated.is_finite_nonnegative());
assert_eq!(
CostEstimate {
usd: f64::NAN,
cny: f64::INFINITY,
}
.sanitized(),
CostEstimate::default()
);
}
#[test]
fn balance_response_deserializes_from_json() {
let json = r#"{
"is_available": true,
"balance_infos": [
{
"currency": "CNY",
"total_balance": "123.45",
"topped_up_balance": "100.00",
"granted_balance": "23.45"
}
]
}"#;
let resp: BalanceResponse = serde_json::from_str(json).expect("valid JSON");
assert!(resp.is_available);
assert_eq!(resp.balance_infos.len(), 1);
let info = &resp.balance_infos[0];
assert_eq!(info.currency, "CNY");
assert_eq!(info.total_balance, "123.45");
assert_eq!(info.topped_up_balance, "100.00");
assert_eq!(info.granted_balance, "23.45");
}
#[test]
fn balance_response_defaults_empty_balance_infos_when_unavailable() {
let json = r#"{"is_available": false, "balance_infos": []}"#;
let resp: BalanceResponse = serde_json::from_str(json).expect("valid JSON");
assert!(!resp.is_available);
assert!(resp.balance_infos.is_empty());
}
#[test]
fn balance_response_empty_list_is_valid() {
let json = r#"{"is_available": true, "balance_infos": []}"#;
let resp: BalanceResponse = serde_json::from_str(json).expect("valid JSON");
assert!(resp.is_available);
assert!(resp.balance_infos.is_empty());
}
}