use aether_data_contracts::repository::{ billing::StoredBillingModelContext, global_models::{explicit_pricing_catalog_state, ExplicitPricingCatalogState}, usage::normalize_provider_service_tier, }; use serde::{Deserialize, Serialize}; use serde_json::Value; #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum BillingPricingSource { ProviderOverride, GlobalDefault, Mixed, } impl BillingPricingSource { pub fn as_str(self) -> &'static str { match self { Self::ProviderOverride => "provider_override", Self::GlobalDefault => "global_default", Self::Mixed => "mixed", } } } #[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] #[error("invalid billing pricing configuration: {message}")] pub struct BillingPricingConfigurationError { message: String, } impl BillingPricingConfigurationError { fn new(message: impl Into) -> Self { Self { message: message.into(), } } } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct BillingPricingResolution { pub requested_processing_tier: Option, pub actual_processing_tier: Option, pub billing_processing_tier: Option, pub tiered_pricing: Option, pub tiered_pricing_source: Option, pub processing_tier_price_multiplier: Option, pub price_per_request: Option, pub price_per_request_source: Option, } impl BillingPricingResolution { pub fn requires_actual_processing_tier(&self) -> bool { self.billing_processing_tier.is_none() } pub fn pricing_source(&self) -> &'static str { match (self.tiered_pricing_source, self.price_per_request_source) { (Some(tiered), Some(request)) if tiered != request => "mixed", (Some(source), _) | (_, Some(source)) => source.as_str(), (None, None) => "unpriced", } } pub fn bills_standard_processing_tier(&self) -> bool { self.billing_processing_tier .as_deref() .is_some_and(processing_tier_is_standard) } pub fn bills_requested_processing_tier(&self) -> bool { let requested = self .requested_processing_tier .as_deref() .map(canonical_processing_tier) .unwrap_or_else(|| "standard".to_string()); self.billing_processing_tier.as_deref() == Some(requested.as_str()) } } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct BillingModelPricingSnapshot { pub provider_id: String, pub provider_billing_type: Option, pub provider_api_key_id: Option, pub provider_api_key_rate_multipliers: Option, pub provider_api_key_cache_ttl_minutes: Option, pub global_model_id: String, pub global_model_name: String, pub global_model_config: Option, pub default_price_per_request: Option, pub default_tiered_pricing: Option, pub model_id: Option, pub model_provider_model_name: Option, pub model_config: Option, pub model_price_per_request: Option, pub model_tiered_pricing: Option, } impl BillingModelPricingSnapshot { pub fn resolve_pricing( &self, requested_processing_tier: Option<&str>, actual_processing_tier: Option<&str>, ) -> BillingPricingResolution { let requested_processing_tier = normalize_processing_tier(requested_processing_tier); let actual_processing_tier = normalize_processing_tier(actual_processing_tier); // The provider-reported actual tier is retained in the resolution for audit only. The // final upstream request is the sole authority for selecting a billing catalog. let billing_processing_tier = requested_processing_tier .as_deref() .map(canonical_processing_tier) .or_else(|| Some("standard".to_string())); let (tiered_pricing, tiered_pricing_source, processing_tier_price_multiplier) = billing_processing_tier .as_deref() .and_then(|tier| self.resolve_tiered_pricing(tier)) .map_or((None, None, None), |(pricing, source, multiplier)| { (Some(pricing), Some(source), multiplier) }); let (price_per_request, price_per_request_source) = self .resolve_price_per_request() .map_or((None, None), |(price, source)| (Some(price), Some(source))); BillingPricingResolution { requested_processing_tier, actual_processing_tier, billing_processing_tier, tiered_pricing, tiered_pricing_source, processing_tier_price_multiplier, price_per_request, price_per_request_source, } } pub fn resolve_pricing_checked( &self, requested_processing_tier: Option<&str>, actual_processing_tier: Option<&str>, ) -> Result { self.validate_processing_tier_containers()?; let requested_processing_tier = normalize_processing_tier(requested_processing_tier); let actual_processing_tier = normalize_processing_tier(actual_processing_tier); // Keep this checked path aligned with `resolve_pricing`: response facts must never choose // the catalog used for settlement. let billing_processing_tier = requested_processing_tier .as_deref() .map(canonical_processing_tier) .or_else(|| Some("standard".to_string())); let (tiered_pricing, tiered_pricing_source, processing_tier_price_multiplier) = match billing_processing_tier.as_deref() { Some(tier) => self .resolve_tiered_pricing_checked(tier)? .map_or((None, None, None), |(pricing, source, multiplier)| { (Some(pricing), Some(source), multiplier) }), None => (None, None, None), }; let (price_per_request, price_per_request_source) = self .resolve_price_per_request() .map_or((None, None), |(price, source)| (Some(price), Some(source))); Ok(BillingPricingResolution { requested_processing_tier, actual_processing_tier, billing_processing_tier, tiered_pricing, tiered_pricing_source, processing_tier_price_multiplier, price_per_request, price_per_request_source, }) } pub fn resolve_authorization_pricing_candidates( &self, requested_processing_tier: Option<&str>, ) -> Result>, BillingPricingConfigurationError> { self.validate_processing_tier_containers()?; let requested_processing_tier = normalize_processing_tier(requested_processing_tier); let requested_billing_tier = requested_processing_tier .as_deref() .map(canonical_processing_tier) .unwrap_or_else(|| "standard".to_string()); let requested_resolution = self.authorization_pricing_for_tier( requested_processing_tier.clone(), Some(requested_billing_tier.clone()), )?; if !processing_tier_is_standard(&requested_billing_tier) && requested_resolution.tiered_pricing.is_none() && requested_resolution.price_per_request.is_none() { return Ok(None); } Ok(Some(vec![requested_resolution])) } pub fn validate_authorization_pricing_configuration( &self, requested_processing_tier: Option<&str>, ) -> Result<(), BillingPricingConfigurationError> { self.resolve_authorization_pricing_candidates(requested_processing_tier) .map(|_| ()) } fn authorization_pricing_for_tier( &self, requested_processing_tier: Option, billing_processing_tier: Option, ) -> Result { let (tiered_pricing, tiered_pricing_source, processing_tier_price_multiplier) = match billing_processing_tier.as_deref() { Some(tier) => self .resolve_tiered_pricing_checked(tier)? .map_or((None, None, None), |(pricing, source, multiplier)| { (Some(pricing), Some(source), multiplier) }), None => (None, None, None), }; let (price_per_request, price_per_request_source) = self .resolve_price_per_request() .map_or((None, None), |(price, source)| (Some(price), Some(source))); Ok(BillingPricingResolution { requested_processing_tier, actual_processing_tier: None, billing_processing_tier, tiered_pricing, tiered_pricing_source, processing_tier_price_multiplier, price_per_request, price_per_request_source, }) } fn resolve_tiered_pricing( &self, processing_tier: &str, ) -> Option<(Value, BillingPricingSource, Option)> { self.resolve_tiered_pricing_checked(processing_tier) .ok() .flatten() } fn resolve_tiered_pricing_checked( &self, processing_tier: &str, ) -> Result)>, BillingPricingConfigurationError> { if processing_tier_is_standard(processing_tier) { return Ok(self .resolve_standard_tiered_pricing_checked()? .map(|(pricing, source)| (pricing.clone(), source, None))); } match processing_tier_overlay(self.model_tiered_pricing.as_ref(), processing_tier) { ProcessingTierOverlay::Explicit(pricing) => Ok(Some(( pricing.clone(), BillingPricingSource::ProviderOverride, None, ))), ProcessingTierOverlay::Multiplier(multiplier) => self .resolve_multiplied_standard_tiered_pricing_checked( multiplier, BillingPricingSource::ProviderOverride, processing_tier, ) .map(|(pricing, source)| Some((pricing, source, Some(multiplier)))), ProcessingTierOverlay::Invalid(reason) => Err(invalid_processing_tier_error( BillingPricingSource::ProviderOverride, processing_tier, reason, )), ProcessingTierOverlay::Missing => { match processing_tier_overlay(self.default_tiered_pricing.as_ref(), processing_tier) { ProcessingTierOverlay::Explicit(pricing) => Ok(Some(( pricing.clone(), BillingPricingSource::GlobalDefault, None, ))), ProcessingTierOverlay::Multiplier(multiplier) => self .resolve_multiplied_standard_tiered_pricing_checked( multiplier, BillingPricingSource::GlobalDefault, processing_tier, ) .map(|(pricing, source)| Some((pricing, source, Some(multiplier)))), ProcessingTierOverlay::Invalid(reason) => Err(invalid_processing_tier_error( BillingPricingSource::GlobalDefault, processing_tier, reason, )), ProcessingTierOverlay::Missing => Ok(self .resolve_standard_tiered_pricing_checked()? .map(|(pricing, source)| (pricing.clone(), source, None))), } } } } fn resolve_standard_tiered_pricing_checked( &self, ) -> Result, BillingPricingConfigurationError> { match self.model_tiered_pricing.as_ref() { Some(value) if explicit_pricing_catalog_state(value) == ExplicitPricingCatalogState::Valid => { return Ok(Some((value, BillingPricingSource::ProviderOverride))); } Some(value) if explicit_pricing_catalog_state(value) == ExplicitPricingCatalogState::Invalid => { return Err(BillingPricingConfigurationError::new( "provider Standard catalog contains malformed or unrecognized prices", )); } _ => {} } match self.default_tiered_pricing.as_ref() { Some(value) if explicit_pricing_catalog_state(value) == ExplicitPricingCatalogState::Valid => { Ok(Some((value, BillingPricingSource::GlobalDefault))) } Some(value) if explicit_pricing_catalog_state(value) == ExplicitPricingCatalogState::Invalid => { Err(BillingPricingConfigurationError::new( "global Standard catalog contains malformed or unrecognized prices", )) } _ => Ok(None), } } fn resolve_multiplied_standard_tiered_pricing_checked( &self, multiplier: f64, multiplier_source: BillingPricingSource, processing_tier: &str, ) -> Result<(Value, BillingPricingSource), BillingPricingConfigurationError> { let Some((standard, standard_source)) = self.resolve_standard_tiered_pricing_checked()? else { return Err(invalid_processing_tier_error( multiplier_source, processing_tier, "price_multiplier cannot be materialized without a valid Standard catalog", )); }; let source = if standard_source == multiplier_source { standard_source } else { BillingPricingSource::Mixed }; let pricing = multiply_pricing_catalog(standard, multiplier, processing_tier).ok_or_else(|| { invalid_processing_tier_error( multiplier_source, processing_tier, "price_multiplier produced an invalid pricing catalog", ) })?; Ok((pricing, source)) } fn validate_processing_tier_containers(&self) -> Result<(), BillingPricingConfigurationError> { for (pricing, source) in [ ( self.model_tiered_pricing.as_ref(), BillingPricingSource::ProviderOverride, ), ( self.default_tiered_pricing.as_ref(), BillingPricingSource::GlobalDefault, ), ] { let Some(pricing) = pricing else { continue; }; let Some(pricing) = pricing.as_object() else { return Err(BillingPricingConfigurationError::new(format!( "{} tiered pricing must be an object", source.as_str() ))); }; let Some(processing_tiers) = pricing.get("processing_tiers") else { continue; }; if processing_tiers.is_null() { continue; } let Some(processing_tiers) = processing_tiers.as_object() else { return Err(BillingPricingConfigurationError::new(format!( "{} processing_tiers must be an object", source.as_str() ))); }; for (tier, overlay) in processing_tiers { let normalized_tier = normalize_processing_tier(Some(tier)); if normalized_tier.as_deref() != Some(tier.as_str()) { return Err(BillingPricingConfigurationError::new(format!( "{} processing tier name `{tier}` must be canonical lowercase without surrounding whitespace", source.as_str(), ))); } if !overlay.is_object() { return Err(invalid_processing_tier_error( source, tier, "overlay must be an object", )); } } } Ok(()) } fn resolve_price_per_request(&self) -> Option<(f64, BillingPricingSource)> { self.model_price_per_request .map(|price| (price, BillingPricingSource::ProviderOverride)) .or_else(|| { self.default_price_per_request .map(|price| (price, BillingPricingSource::GlobalDefault)) }) } pub fn is_free_tier(&self) -> bool { self.provider_billing_type .as_deref() .map(|value| value.eq_ignore_ascii_case("free_tier")) .unwrap_or(false) } pub fn rate_multiplier_for_api_format(&self, api_format: Option<&str>) -> f64 { let Some(api_format) = api_format.map(str::trim).filter(|value| !value.is_empty()) else { return 1.0; }; let normalized = api_format.to_ascii_lowercase(); let Some(mapping) = self .provider_api_key_rate_multipliers .as_ref() .and_then(Value::as_object) else { return 1.0; }; mapping .get(&normalized) .and_then(|value| value.as_f64()) .unwrap_or(1.0) } } impl From<&StoredBillingModelContext> for BillingModelPricingSnapshot { fn from(context: &StoredBillingModelContext) -> Self { Self { provider_id: context.provider_id.clone(), provider_billing_type: context.provider_billing_type.clone(), provider_api_key_id: context.provider_api_key_id.clone(), provider_api_key_rate_multipliers: context.provider_api_key_rate_multipliers.clone(), provider_api_key_cache_ttl_minutes: context.provider_api_key_cache_ttl_minutes, global_model_id: context.global_model_id.clone(), global_model_name: context.global_model_name.clone(), global_model_config: context.global_model_config.clone(), default_price_per_request: context.default_price_per_request, default_tiered_pricing: context.default_tiered_pricing.clone(), model_id: context.model_id.clone(), model_provider_model_name: context.model_provider_model_name.clone(), model_config: context.model_config.clone(), model_price_per_request: context.model_price_per_request, model_tiered_pricing: context.model_tiered_pricing.clone(), } } } impl From for BillingModelPricingSnapshot { fn from(context: StoredBillingModelContext) -> Self { Self { provider_id: context.provider_id, provider_billing_type: context.provider_billing_type, provider_api_key_id: context.provider_api_key_id, provider_api_key_rate_multipliers: context.provider_api_key_rate_multipliers, provider_api_key_cache_ttl_minutes: context.provider_api_key_cache_ttl_minutes, global_model_id: context.global_model_id, global_model_name: context.global_model_name, global_model_config: context.global_model_config, default_price_per_request: context.default_price_per_request, default_tiered_pricing: context.default_tiered_pricing, model_id: context.model_id, model_provider_model_name: context.model_provider_model_name, model_config: context.model_config, model_price_per_request: context.model_price_per_request, model_tiered_pricing: context.model_tiered_pricing, } } } fn normalize_processing_tier(value: Option<&str>) -> Option { value.and_then(normalize_provider_service_tier) } fn canonical_processing_tier(value: &str) -> String { if processing_tier_is_standard(value) { "standard".to_string() } else { value.to_string() } } fn processing_tier_is_standard(value: &str) -> bool { matches!(value, "auto" | "default" | "standard") } enum ProcessingTierOverlay<'a> { Explicit(&'a Value), Multiplier(f64), Invalid(&'static str), Missing, } fn processing_tier_overlay<'a>( pricing: Option<&'a Value>, tier: &str, ) -> ProcessingTierOverlay<'a> { let Some(pricing) = pricing else { return ProcessingTierOverlay::Missing; }; let Some(processing_tiers) = pricing.get("processing_tiers") else { return ProcessingTierOverlay::Missing; }; if processing_tiers.is_null() { return ProcessingTierOverlay::Missing; } let Some(processing_tiers) = processing_tiers.as_object() else { return ProcessingTierOverlay::Invalid("processing_tiers must be an object"); }; let Some(overlay) = processing_tiers.get(tier) else { return ProcessingTierOverlay::Missing; }; if !overlay.is_object() { return ProcessingTierOverlay::Invalid("overlay must be an object"); } // An explicit catalog is authoritative even when a stale or future multiplier field is // present beside it. This preserves the legacy processing-tier contract. let explicit_catalog_state = explicit_pricing_catalog_state(overlay); match explicit_catalog_state { ExplicitPricingCatalogState::Valid => return ProcessingTierOverlay::Explicit(overlay), ExplicitPricingCatalogState::Invalid => { return ProcessingTierOverlay::Invalid( "explicit catalog contains malformed or unrecognized prices", ); } ExplicitPricingCatalogState::Absent => {} } let Some(multiplier) = overlay.get("price_multiplier") else { return ProcessingTierOverlay::Invalid( "overlay must contain an explicit catalog or price_multiplier", ); }; match multiplier .as_f64() .filter(|multiplier| multiplier.is_finite() && *multiplier >= 0.0) { Some(multiplier) => ProcessingTierOverlay::Multiplier(multiplier), None => { ProcessingTierOverlay::Invalid("price_multiplier must be a non-negative finite number") } } } fn invalid_processing_tier_error( source: BillingPricingSource, processing_tier: &str, reason: &str, ) -> BillingPricingConfigurationError { BillingPricingConfigurationError::new(format!( "{} processing tier `{processing_tier}` {reason}", source.as_str() )) } fn multiply_pricing_catalog( pricing: &Value, multiplier: f64, processing_tier: &str, ) -> Option { if !multiplier.is_finite() || multiplier < 0.0 { return None; } let mut multiplied = pricing.clone(); let object = multiplied.as_object_mut()?; // A resolved catalog is self-contained. Keeping the source overlays here would make every // settlement snapshot recursively carry unrelated processing-tier configuration. object.remove("processing_tiers"); // Fast pricing is flat: its multiplier applies to Standard's first band at every context size. // Fast prices are fixed from the base Standard band; long-context premiums do not stack. if matches!(processing_tier, "priority" | "fast") { collapse_token_tiers_to_first(object)?; } if let Some(tiers) = object.get_mut("tiers").and_then(Value::as_array_mut) { for tier in tiers.iter_mut().filter_map(Value::as_object_mut) { multiply_price_fields( tier, &[ "input_price_per_1m", "output_price_per_1m", "cache_creation_price_per_1m", "cache_read_price_per_1m", ], multiplier, )?; if let Some(ttl_prices) = tier .get_mut("cache_ttl_pricing") .and_then(Value::as_array_mut) { for ttl_price in ttl_prices.iter_mut().filter_map(Value::as_object_mut) { multiply_price_fields( ttl_price, &["cache_creation_price_per_1m", "cache_read_price_per_1m"], multiplier, )?; } } } } multiply_price_fields( object, &["image_output_price_default", "image_price_default"], multiplier, )?; for key in [ "image_output_prices", "image_output_price_per_image", "image_output_price_matrix", "image_prices", ] { if let Some(value) = object.get_mut(key) { multiply_image_price_entries(value, multiplier)?; } } if let Some(value) = object.get_mut("image_output_price_ranges") { multiply_image_price_ranges(value, multiplier)?; } Some(multiplied) } fn collapse_token_tiers_to_first(object: &mut serde_json::Map) -> Option<()> { let Some(tiers_value) = object.get_mut("tiers") else { return Some(()); }; if tiers_value.is_null() { return Some(()); } let tiers = tiers_value.as_array_mut()?; let Some(mut first_tier) = tiers.first().cloned() else { return Some(()); }; first_tier .as_object_mut()? .insert("up_to".to_string(), Value::Null); *tiers = vec![first_tier]; Some(()) } fn multiply_price_fields( object: &mut serde_json::Map, fields: &[&str], multiplier: f64, ) -> Option<()> { for field in fields { let Some(value) = object.get_mut(*field) else { continue; }; multiply_numeric_price(value, multiplier)?; } Some(()) } fn multiply_numeric_price(value: &mut Value, multiplier: f64) -> Option<()> { let Some(price) = value.as_f64() else { // Existing nulls and extension values keep their legacy meaning. Only numeric prices are // materialized, so bounds and unknown metadata are never interpreted as money. return Some(()); }; let multiplied = price * multiplier; if !multiplied.is_finite() { return None; } *value = Value::from(multiplied); Some(()) } fn multiply_image_price_entries(value: &mut Value, multiplier: f64) -> Option<()> { match value { Value::Number(_) => multiply_numeric_price(value, multiplier)?, Value::Object(entries) => { for entry in entries.values_mut() { if entry.is_number() { multiply_numeric_price(entry, multiplier)?; continue; } if let Some(prices) = entry.as_object_mut() { for price in prices.values_mut().filter(|price| price.is_number()) { multiply_numeric_price(price, multiplier)?; } } } } Value::Array(entries) => { for entry in entries.iter_mut().filter_map(Value::as_object_mut) { multiply_price_fields(entry, &["price_per_image", "price", "cost"], multiplier)?; } } _ => {} } Some(()) } fn multiply_image_price_ranges(value: &mut Value, multiplier: f64) -> Option<()> { match value { Value::Array(ranges) => { for range in ranges.iter_mut().filter_map(Value::as_object_mut) { multiply_image_price_range(range, multiplier)?; } } Value::Object(ranges) => { for range in ranges.values_mut().filter_map(Value::as_object_mut) { multiply_image_price_range(range, multiplier)?; } } _ => {} } Some(()) } fn multiply_image_price_range( range: &mut serde_json::Map, multiplier: f64, ) -> Option<()> { if let Some(prices) = range.get_mut("prices").and_then(Value::as_object_mut) { for price in prices.values_mut().filter(|price| price.is_number()) { multiply_numeric_price(price, multiplier)?; } } multiply_price_fields( range, &["low", "medium", "high", "price_per_image", "price", "value"], multiplier, ) } #[cfg(test)] mod tests { use serde_json::json; use super::{BillingModelPricingSnapshot, BillingPricingSource}; fn snapshot( model_tiered_pricing: Option, default_tiered_pricing: Option, ) -> BillingModelPricingSnapshot { BillingModelPricingSnapshot { provider_id: "provider-1".to_string(), provider_billing_type: None, provider_api_key_id: None, provider_api_key_rate_multipliers: None, provider_api_key_cache_ttl_minutes: None, global_model_id: "global-model-1".to_string(), global_model_name: "gpt-5".to_string(), global_model_config: None, default_price_per_request: None, default_tiered_pricing, model_id: Some("model-1".to_string()), model_provider_model_name: Some("gpt-5-upstream".to_string()), model_config: None, model_price_per_request: None, model_tiered_pricing, } } #[test] fn empty_provider_tiered_pricing_inherits_global_default() { let default_pricing = json!({"tiers":[{"up_to":null,"input_price_per_1m":3.0,"output_price_per_1m":15.0}]}); let pricing = snapshot(Some(json!({})), Some(default_pricing.clone())); let resolution = pricing.resolve_pricing(None, None); assert_eq!(resolution.tiered_pricing, Some(default_pricing.clone())); assert_eq!( resolution.tiered_pricing_source, Some(BillingPricingSource::GlobalDefault) ); let pricing = snapshot(Some(json!({"tiers": []})), Some(default_pricing.clone())); let resolution = pricing.resolve_pricing(None, None); assert_eq!(resolution.tiered_pricing, Some(default_pricing)); assert_eq!( resolution.tiered_pricing_source, Some(BillingPricingSource::GlobalDefault) ); } #[test] fn populated_provider_tiered_pricing_overrides_global_default() { let provider_pricing = json!({"tiers":[{"up_to":null,"input_price_per_1m":1.0,"output_price_per_1m":2.0}]}); let default_pricing = json!({"tiers":[{"up_to":null,"input_price_per_1m":3.0,"output_price_per_1m":15.0}]}); let pricing = snapshot(Some(provider_pricing.clone()), Some(default_pricing)); let resolution = pricing.resolve_pricing(None, None); assert_eq!(resolution.tiered_pricing, Some(provider_pricing)); assert_eq!( resolution.tiered_pricing_source, Some(BillingPricingSource::ProviderOverride) ); assert_eq!(resolution.processing_tier_price_multiplier, None); } #[test] fn missing_processing_overlay_inherits_effective_standard_catalog() { let provider_pricing = json!({"tiers":[{"up_to":null,"input_price_per_1m":2.0}]}); let default_pricing = json!({"tiers":[{"up_to":null,"input_price_per_1m":3.0}]}); let provider = snapshot( Some(provider_pricing.clone()), Some(default_pricing.clone()), ) .resolve_pricing(Some("fast"), None); assert_eq!(provider.billing_processing_tier.as_deref(), Some("fast")); assert_eq!(provider.tiered_pricing, Some(provider_pricing)); assert_eq!( provider.tiered_pricing_source, Some(BillingPricingSource::ProviderOverride) ); assert_eq!(provider.processing_tier_price_multiplier, None); let global = snapshot(None, Some(default_pricing.clone())).resolve_pricing(Some("priority"), None); assert_eq!(global.billing_processing_tier.as_deref(), Some("priority")); assert_eq!(global.tiered_pricing, Some(default_pricing)); assert_eq!( global.tiered_pricing_source, Some(BillingPricingSource::GlobalDefault) ); assert_eq!(global.processing_tier_price_multiplier, None); } #[test] fn nonstandard_tier_keeps_fixed_request_price_without_token_catalog() { let mut pricing = snapshot(None, None); pricing.default_price_per_request = Some(0.02); let resolution = pricing.resolve_pricing(Some("fast"), None); assert_eq!(resolution.billing_processing_tier.as_deref(), Some("fast")); assert_eq!(resolution.tiered_pricing, None); assert_eq!(resolution.price_per_request, Some(0.02)); assert_eq!( resolution.price_per_request_source, Some(BillingPricingSource::GlobalDefault) ); assert!(pricing .resolve_authorization_pricing_candidates(Some("fast")) .expect("fixed request pricing should be valid") .is_some()); } #[test] fn explicit_nonstandard_request_selects_requested_catalog_without_actual_tier() { let pricing = snapshot( None, Some(json!({ "tiers": [{"up_to": null, "input_price_per_1m": 3.0}], "processing_tiers": { "priority": {"tiers": [{"up_to": 272000, "input_price_per_1m": 6.0}]} } })), ); let resolution = pricing.resolve_pricing(Some("Priority"), None); assert!(!resolution.requires_actual_processing_tier()); assert_eq!( resolution.requested_processing_tier.as_deref(), Some("priority") ); assert_eq!( resolution.billing_processing_tier.as_deref(), Some("priority") ); assert_eq!( resolution .tiered_pricing .as_ref() .and_then(|value| value.pointer("/tiers/0/input_price_per_1m")) .and_then(serde_json::Value::as_f64), Some(6.0) ); } #[test] fn response_actual_tier_is_audited_but_does_not_select_pricing_catalog() { let pricing = snapshot( Some(json!({ "processing_tiers": { "priority": {"tiers": [{"up_to": 272000, "input_price_per_1m": 9.0}]} } })), Some(json!({ "tiers": [{"up_to": null, "input_price_per_1m": 3.0}], "processing_tiers": { "flex": {"tiers": [{"up_to": null, "input_price_per_1m": 1.5}]} } })), ); let flex = pricing.resolve_pricing(Some("priority"), Some("flex")); assert_eq!(flex.actual_processing_tier.as_deref(), Some("flex")); assert_eq!(flex.billing_processing_tier.as_deref(), Some("priority")); assert_eq!( flex.tiered_pricing_source, Some(BillingPricingSource::ProviderOverride) ); assert_eq!( flex.tiered_pricing .as_ref() .and_then(|value| value.pointer("/tiers/0/input_price_per_1m")) .and_then(serde_json::Value::as_f64), Some(9.0) ); let standard = pricing.resolve_pricing(Some("priority"), Some("Default")); assert_eq!(standard.actual_processing_tier.as_deref(), Some("default")); assert_eq!( standard.billing_processing_tier.as_deref(), Some("priority") ); assert_eq!( standard.tiered_pricing_source, Some(BillingPricingSource::ProviderOverride) ); } #[test] fn tiered_and_fixed_price_sources_are_recorded_independently() { let mut pricing = snapshot( None, Some(json!({"tiers": [{"up_to": null, "input_price_per_1m": 3.0}]})), ); pricing.model_price_per_request = Some(0.02); let resolution = pricing.resolve_pricing(None, None); assert_eq!( resolution.tiered_pricing_source, Some(BillingPricingSource::GlobalDefault) ); assert_eq!( resolution.price_per_request_source, Some(BillingPricingSource::ProviderOverride) ); assert_eq!(resolution.pricing_source(), "mixed"); } #[test] fn authorization_candidates_include_requested_catalog_without_inventing_actual_tier() { let pricing = snapshot( None, Some(json!({ "tiers": [{"up_to": null, "input_price_per_1m": 3.0}], "processing_tiers": { "priority": {"tiers": [{"up_to": 272000, "input_price_per_1m": 6.0}]} } })), ); let candidates = pricing .resolve_authorization_pricing_candidates(Some("Priority")) .expect("authorization catalogs should resolve") .expect("authorization catalogs should be configured"); let resolution = candidates .iter() .find(|resolution| resolution.billing_processing_tier.as_deref() == Some("priority")) .expect("priority catalog should be included"); assert_eq!( resolution.requested_processing_tier.as_deref(), Some("priority") ); assert_eq!(resolution.actual_processing_tier, None); assert_eq!( resolution.billing_processing_tier.as_deref(), Some("priority") ); assert_eq!( resolution .tiered_pricing .as_ref() .and_then(|value| value.pointer("/tiers/0/input_price_per_1m")) .and_then(serde_json::Value::as_f64), Some(6.0) ); assert_eq!(candidates.len(), 1); } #[test] fn fast_processing_multiplier_materializes_known_prices_and_preserves_extensions() { let mut pricing = snapshot( Some(json!({ "tiers": [{ "up_to": 272000, "input_price_per_1m": 2.0, "output_price_per_1m": 4.0, "cache_creation_price_per_1m": 2.5, "cache_read_price_per_1m": 0.2, "cache_ttl_pricing": [{ "ttl_minutes": 60, "cache_creation_price_per_1m": 5.0, "cache_read_price_per_1m": null, "future_ttl_option": 17 }], "future_tier_option": 23 }], "image_output_price_default": 0.01, "image_output_price_per_image": 0.05, "image_output_prices": { "default": 0.02, "1024x1024": {"high": 0.03} }, "image_output_price_ranges": [{ "up_to_pixels": 1048576, "prices": {"medium": 0.04}, "future_range_option": 29 }], "future_catalog_option": 31, "processing_tiers": { "priority": {"price_multiplier": 2.5} } })), None, ); pricing.model_price_per_request = Some(0.02); let resolution = pricing.resolve_pricing(Some("priority"), Some("priority")); let catalog = resolution .tiered_pricing .as_ref() .expect("multiplier should materialize the Standard catalog"); assert_eq!(resolution.price_per_request, Some(0.02)); assert_eq!( resolution.tiered_pricing_source, Some(BillingPricingSource::ProviderOverride) ); assert_eq!(resolution.processing_tier_price_multiplier, Some(2.5)); assert_eq!( catalog.pointer("/tiers/0/up_to"), Some(&serde_json::Value::Null) ); assert_eq!( catalog.pointer("/tiers/0/input_price_per_1m"), Some(&json!(5.0)) ); assert_eq!( catalog.pointer("/tiers/0/output_price_per_1m"), Some(&json!(10.0)) ); assert_eq!( catalog.pointer("/tiers/0/cache_creation_price_per_1m"), Some(&json!(6.25)) ); assert_eq!( catalog.pointer("/tiers/0/cache_read_price_per_1m"), Some(&json!(0.5)) ); assert_eq!( catalog.pointer("/tiers/0/cache_ttl_pricing/0/ttl_minutes"), Some(&json!(60)) ); assert_eq!( catalog.pointer("/tiers/0/cache_ttl_pricing/0/cache_creation_price_per_1m"), Some(&json!(12.5)) ); assert_eq!( catalog.pointer("/tiers/0/cache_ttl_pricing/0/cache_read_price_per_1m"), Some(&serde_json::Value::Null) ); assert_eq!( catalog.pointer("/image_output_price_default"), Some(&json!(0.025)) ); assert_eq!( catalog.pointer("/image_output_price_per_image"), Some(&json!(0.125)) ); assert_eq!( catalog.pointer("/image_output_prices/default"), Some(&json!(0.05)) ); assert_eq!( catalog.pointer("/image_output_prices/1024x1024/high"), Some(&json!(0.075)) ); assert_eq!( catalog.pointer("/image_output_price_ranges/0/up_to_pixels"), Some(&json!(1048576)) ); assert_eq!( catalog.pointer("/image_output_price_ranges/0/prices/medium"), Some(&json!(0.1)) ); assert!(catalog.get("processing_tiers").is_none()); for pointer in [ "/tiers/0/future_tier_option", "/tiers/0/cache_ttl_pricing/0/future_ttl_option", "/image_output_price_ranges/0/future_range_option", "/future_catalog_option", ] { assert_eq!( catalog.pointer(pointer), pricing .model_tiered_pricing .as_ref() .and_then(|value| value.pointer(pointer)), "unknown field changed at {pointer}" ); } } #[test] fn fast_multipliers_flatten_multiband_standard_while_flex_preserves_all_bands() { let pricing = snapshot( Some(json!({ "tiers": [ { "up_to": 272000, "input_price_per_1m": 2.0, "output_price_per_1m": 4.0, "cache_creation_price_per_1m": 6.0, "cache_read_price_per_1m": 8.0, "future_tier_option": "first" }, { "up_to": null, "input_price_per_1m": 5.0, "output_price_per_1m": 7.0, "cache_creation_price_per_1m": 9.0, "cache_read_price_per_1m": 11.0, "future_tier_option": "second" } ], "image_output_price_default": 10.0, "future_catalog_option": 31, "processing_tiers": { "priority": {"price_multiplier": 2.0}, "fast": {"price_multiplier": 3.0}, "flex": {"price_multiplier": 4.0} } })), None, ); for (tier, expected_tiers, expected_image_price, expected_multiplier) in [ ( "priority", json!([{ "up_to": null, "input_price_per_1m": 4.0, "output_price_per_1m": 8.0, "cache_creation_price_per_1m": 12.0, "cache_read_price_per_1m": 16.0, "future_tier_option": "first" }]), json!(20.0), 2.0, ), ( "fast", json!([{ "up_to": null, "input_price_per_1m": 6.0, "output_price_per_1m": 12.0, "cache_creation_price_per_1m": 18.0, "cache_read_price_per_1m": 24.0, "future_tier_option": "first" }]), json!(30.0), 3.0, ), ] { let resolution = pricing.resolve_pricing(Some(tier), None); let catalog = resolution .tiered_pricing .as_ref() .expect("fast multiplier should materialize a pricing catalog"); assert_eq!(catalog["tiers"], expected_tiers, "{tier}"); assert_eq!( catalog["image_output_price_default"], expected_image_price, "{tier}" ); assert_eq!(catalog["future_catalog_option"], json!(31), "{tier}"); assert!(catalog.get("processing_tiers").is_none(), "{tier}"); assert_eq!( resolution.processing_tier_price_multiplier, Some(expected_multiplier), "{tier}" ); } let flex = pricing.resolve_pricing(Some("flex"), None); let flex_catalog = flex .tiered_pricing .as_ref() .expect("flex multiplier should materialize a pricing catalog"); assert_eq!( flex_catalog["tiers"], json!([ { "up_to": 272000, "input_price_per_1m": 8.0, "output_price_per_1m": 16.0, "cache_creation_price_per_1m": 24.0, "cache_read_price_per_1m": 32.0, "future_tier_option": "first" }, { "up_to": null, "input_price_per_1m": 20.0, "output_price_per_1m": 28.0, "cache_creation_price_per_1m": 36.0, "cache_read_price_per_1m": 44.0, "future_tier_option": "second" } ]) ); assert_eq!(flex_catalog["image_output_price_default"], json!(40.0)); assert_eq!(flex_catalog["future_catalog_option"], json!(31)); assert!(flex_catalog.get("processing_tiers").is_none()); assert_eq!(flex.processing_tier_price_multiplier, Some(4.0)); } #[test] fn processing_overlay_precedence_is_provider_explicit_multiplier_then_global() { let provider_explicit = snapshot( Some(json!({ "tiers": [{"up_to": null, "input_price_per_1m": 2.0}], "processing_tiers": { "priority": { "price_multiplier": "invalid-but-shadowed", "tiers": [{"up_to": null, "input_price_per_1m": 9.0}] } } })), Some(json!({ "tiers": [{"up_to": null, "input_price_per_1m": 3.0}], "processing_tiers": { "priority": {"tiers": [{"up_to": null, "input_price_per_1m": 7.0}]} } })), ); let resolved = provider_explicit.resolve_pricing(Some("priority"), Some("priority")); assert_eq!( resolved .tiered_pricing .as_ref() .and_then(|value| value.pointer("/tiers/0/input_price_per_1m")), Some(&json!(9.0)) ); assert_eq!( resolved.tiered_pricing_source, Some(BillingPricingSource::ProviderOverride) ); assert_eq!(resolved.processing_tier_price_multiplier, None); let provider_multiplier = snapshot( Some(json!({ "tiers": [{"up_to": null, "input_price_per_1m": 2.0}], "processing_tiers": {"priority": {"price_multiplier": 3.0}} })), Some(json!({ "tiers": [{"up_to": null, "input_price_per_1m": 3.0}], "processing_tiers": { "priority": {"tiers": [{"up_to": null, "input_price_per_1m": 7.0}]} } })), ); let resolved = provider_multiplier.resolve_pricing(Some("priority"), Some("priority")); assert_eq!( resolved .tiered_pricing .as_ref() .and_then(|value| value.pointer("/tiers/0/input_price_per_1m")), Some(&json!(6.0)) ); assert_eq!( resolved.tiered_pricing_source, Some(BillingPricingSource::ProviderOverride) ); assert_eq!(resolved.processing_tier_price_multiplier, Some(3.0)); let global_explicit = snapshot( Some(json!({"tiers": [{"up_to": null, "input_price_per_1m": 2.0}]})), Some(json!({ "tiers": [{"up_to": null, "input_price_per_1m": 3.0}], "processing_tiers": { "priority": { "price_multiplier": 4.0, "tiers": [{"up_to": null, "input_price_per_1m": 7.0}] } } })), ); let resolved = global_explicit.resolve_pricing(Some("priority"), Some("priority")); assert_eq!( resolved .tiered_pricing .as_ref() .and_then(|value| value.pointer("/tiers/0/input_price_per_1m")), Some(&json!(7.0)) ); assert_eq!( resolved.tiered_pricing_source, Some(BillingPricingSource::GlobalDefault) ); assert_eq!(resolved.processing_tier_price_multiplier, None); let global_multiplier = snapshot( Some(json!({"tiers": [{"up_to": null, "input_price_per_1m": 2.0}]})), Some(json!({ "tiers": [{"up_to": null, "input_price_per_1m": 3.0}], "processing_tiers": {"priority": {"price_multiplier": 4.0}} })), ); let resolved = global_multiplier.resolve_pricing(Some("priority"), Some("priority")); assert_eq!( resolved .tiered_pricing .as_ref() .and_then(|value| value.pointer("/tiers/0/input_price_per_1m")), Some(&json!(8.0)), "global multiplier must apply to the effective provider Standard catalog" ); assert_eq!( resolved.tiered_pricing_source, Some(BillingPricingSource::Mixed) ); assert_eq!(resolved.processing_tier_price_multiplier, Some(4.0)); assert_eq!(resolved.pricing_source(), "mixed"); let provider_multiplier_with_global_standard = snapshot( Some(json!({ "processing_tiers": {"priority": {"price_multiplier": 3.0}} })), Some(json!({ "tiers": [{"up_to": null, "input_price_per_1m": 3.0}] })), ); let resolved = provider_multiplier_with_global_standard .resolve_pricing(Some("priority"), Some("priority")); assert_eq!( resolved .tiered_pricing .as_ref() .and_then(|value| value.pointer("/tiers/0/input_price_per_1m")), Some(&json!(9.0)) ); assert_eq!( resolved.tiered_pricing_source, Some(BillingPricingSource::Mixed) ); assert_eq!(resolved.processing_tier_price_multiplier, Some(3.0)); assert_eq!(resolved.pricing_source(), "mixed"); } #[test] fn requested_claude_fast_uses_the_exact_provider_multiplier_overlay() { let pricing = snapshot( Some(json!({ "tiers": [{"up_to": null, "input_price_per_1m": 2.0}], "processing_tiers": {"fast": {"price_multiplier": 2.0}} })), Some(json!({ "tiers": [{"up_to": null, "input_price_per_1m": 3.0}], "processing_tiers": {"fast": {"price_multiplier": 9.0}} })), ); let resolved = pricing.resolve_pricing(Some("fast"), Some("priority")); assert_eq!(resolved.actual_processing_tier.as_deref(), Some("priority")); assert_eq!(resolved.billing_processing_tier.as_deref(), Some("fast")); assert_eq!( resolved .tiered_pricing .as_ref() .and_then(|value| value.pointer("/tiers/0/input_price_per_1m")), Some(&json!(4.0)) ); assert_eq!( resolved.tiered_pricing_source, Some(BillingPricingSource::ProviderOverride) ); assert_eq!(resolved.processing_tier_price_multiplier, Some(2.0)); } #[test] fn invalid_processing_multiplier_blocks_lower_precedence_fallback() { let pricing = snapshot( Some(json!({ "tiers": [{"up_to": null, "input_price_per_1m": 2.0}], "processing_tiers": {"priority": {"price_multiplier": "2.5"}} })), Some(json!({ "tiers": [{"up_to": null, "input_price_per_1m": 3.0}], "processing_tiers": { "priority": {"tiers": [{"up_to": null, "input_price_per_1m": 7.0}]} } })), ); let resolution = pricing.resolve_pricing(Some("priority"), Some("priority")); assert_eq!( resolution.billing_processing_tier.as_deref(), Some("priority") ); assert_eq!(resolution.tiered_pricing, None); assert_eq!(resolution.tiered_pricing_source, None); assert_eq!(resolution.processing_tier_price_multiplier, None); } #[test] fn authorization_candidates_include_multiplier_only_processing_catalog() { let pricing = snapshot( None, Some(json!({ "tiers": [{"up_to": null, "input_price_per_1m": 3.0}], "processing_tiers": {"priority": {"price_multiplier": 2.5}} })), ); let candidates = pricing .resolve_authorization_pricing_candidates(Some("priority")) .expect("multiplier-only processing catalog should authorize") .expect("multiplier-only processing catalog should be configured"); let priority = candidates .iter() .find(|resolution| resolution.billing_processing_tier.as_deref() == Some("priority")) .expect("priority candidate should exist"); assert_eq!( priority .tiered_pricing .as_ref() .and_then(|value| value.pointer("/tiers/0/input_price_per_1m")), Some(&json!(7.5)) ); assert_eq!( priority.tiered_pricing_source, Some(BillingPricingSource::GlobalDefault) ); assert_eq!(priority.processing_tier_price_multiplier, Some(2.5)); } } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct BillingUsageInput { pub task_type: String, pub api_format: Option, #[serde(default)] pub requested_processing_tier: Option, #[serde(default)] pub actual_processing_tier: Option, pub request_count: i64, pub input_tokens: i64, pub output_tokens: i64, pub cache_creation_tokens: i64, pub cache_creation_ephemeral_5m_tokens: i64, pub cache_creation_ephemeral_1h_tokens: i64, pub cache_read_tokens: i64, pub image_count: i64, pub image_size: Option, pub image_quality: Option, pub image_output_format: Option, pub cache_ttl_minutes: Option, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct BillingAuthorizationEstimateInput { pub task_type: String, pub api_format: Option, pub requested_processing_tier: Option, #[serde(default)] pub cache_ttl_minutes: Option, pub input_tokens: i64, pub max_output_tokens: Option, } impl BillingAuthorizationEstimateInput { pub fn new(task_type: impl Into, input_tokens: i64) -> Self { Self { task_type: task_type.into(), api_format: None, requested_processing_tier: None, cache_ttl_minutes: None, input_tokens: input_tokens.max(0), max_output_tokens: None, } } } impl BillingUsageInput { pub fn new(task_type: impl Into) -> Self { Self { task_type: task_type.into(), api_format: None, requested_processing_tier: None, actual_processing_tier: None, request_count: 1, input_tokens: 0, output_tokens: 0, cache_creation_tokens: 0, cache_creation_ephemeral_5m_tokens: 0, cache_creation_ephemeral_1h_tokens: 0, cache_read_tokens: 0, image_count: 0, image_size: None, image_quality: None, image_output_format: None, cache_ttl_minutes: None, } } } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct BillingComputation { pub cost_result: crate::CostResult, pub actual_total_cost: f64, pub rate_multiplier: f64, pub is_free_tier: bool, pub pricing_resolution: BillingPricingResolution, }