fix(usage): align fast-tier pricing and model metadata

This commit is contained in:
elky
2026-07-18 16:57:04 +08:00
parent 6c33b8d8fb
commit e3d97b573b
11 changed files with 430 additions and 47 deletions
@@ -702,6 +702,82 @@ mod tests {
);
}
#[tokio::test]
async fn openai_fast_usage_without_overlay_inherits_global_model_pricing() {
let lookup = TestLookup {
name_context: Some(
StoredBillingModelContext::new(
"provider-1".to_string(),
Some("pay_as_you_go".to_string()),
Some("key-1".to_string()),
None,
None,
"global-model-1".to_string(),
"gpt-5.6-sol".to_string(),
None,
None,
Some(json!({
"tiers": [{
"up_to": null,
"input_price_per_1m": 3.0,
"output_price_per_1m": 15.0
}]
})),
Some("model-1".to_string()),
Some("gpt-5.6-sol".to_string()),
None,
None,
None,
)
.expect("billing context should build"),
),
model_id_context: None,
};
let mut event = UsageEvent::new(
UsageEventType::Completed,
"req-fast-global-fallback",
UsageEventData {
provider_name: "OpenAI".to_string(),
model: "gpt-5.6-sol".to_string(),
target_model: Some("gpt-5.6-sol".to_string()),
provider_id: Some("provider-1".to_string()),
provider_api_key_id: Some("key-1".to_string()),
request_type: Some("chat".to_string()),
api_format: Some("openai:responses".to_string()),
endpoint_api_format: Some("openai:responses".to_string()),
// OpenAI calls the Fast request tier `priority` on the wire.
provider_request_body: Some(json!({
"model": "gpt-5.6-sol",
"service_tier": "priority"
})),
input_tokens: Some(1_000_000),
status_code: Some(200),
..UsageEventData::default()
},
);
enrich_usage_event_with_billing(&lookup, &mut event)
.await
.expect("billing should succeed");
assert_eq!(event.data.total_cost_usd, Some(3.0));
let metadata = event.data.request_metadata.as_ref().expect("metadata");
assert_eq!(
metadata.pointer("/billing_snapshot/status"),
Some(&json!("complete"))
);
let pricing = metadata
.pointer("/settlement_snapshot/pricing_snapshot")
.expect("settlement pricing snapshot");
assert_eq!(pricing["billing_processing_tier"], "priority");
assert_eq!(pricing["pricing_source"], "global_default");
assert_eq!(pricing["tiered_pricing_source"], "global_default");
assert_eq!(
pricing["tiered_pricing"]["tiers"][0]["input_price_per_1m"],
3.0
);
}
#[tokio::test]
async fn settlement_uses_requested_processing_tier_catalog_and_ignores_response_tier() {
let lookup = TestLookup {
+52 -1
View File
@@ -192,6 +192,7 @@ impl BillingModelPricingSnapshot {
)?;
if !processing_tier_is_standard(&requested_billing_tier)
&& requested_resolution.tiered_pricing.is_none()
&& requested_resolution.price_per_request.is_none()
{
return Ok(None);
}
@@ -295,7 +296,9 @@ impl BillingModelPricingSnapshot {
processing_tier,
reason,
)),
ProcessingTierOverlay::Missing => Ok(None),
ProcessingTierOverlay::Missing => Ok(self
.resolve_standard_tiered_pricing_checked()?
.map(|(pricing, source)| (pricing.clone(), source, None))),
}
}
}
@@ -799,6 +802,54 @@ mod tests {
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(
+61 -2
View File
@@ -174,6 +174,7 @@ impl BillingService {
}
if !pricing_resolution.bills_standard_processing_tier()
&& pricing_resolution.tiered_pricing.is_none()
&& pricing_resolution.price_per_request.is_none()
{
return Ok(no_rule_computation(
pricing,
@@ -1123,6 +1124,64 @@ mod tests {
);
}
#[test]
fn openai_fast_request_without_overlay_uses_global_standard_catalog() {
let pricing = BillingModelPricingSnapshot {
default_price_per_request: None,
default_tiered_pricing: Some(json!({
"tiers": [{
"up_to": null,
"input_price_per_1m": 3.0,
"output_price_per_1m": 15.0
}]
})),
model_tiered_pricing: None,
..pricing()
};
let result = BillingService::new()
.calculate(
&pricing,
&BillingUsageInput {
api_format: Some("openai:responses".to_string()),
requested_processing_tier: Some("priority".to_string()),
input_tokens: 1_000_000,
..BillingUsageInput::new("chat")
},
)
.expect("global Standard pricing should calculate OpenAI Fast usage");
assert_eq!(result.cost_result.status, BillingSnapshotStatus::Complete);
assert_eq!(result.cost_result.cost, 3.0);
assert_eq!(
result.pricing_resolution.tiered_pricing_source,
Some(crate::BillingPricingSource::GlobalDefault)
);
assert_eq!(
result.cost_result.snapshot.resolved_variables["input_price_per_1m"],
json!(3.0)
);
}
#[test]
fn nonstandard_request_with_only_fixed_price_is_still_billable() {
let pricing = BillingModelPricingSnapshot {
default_price_per_request: Some(0.02),
default_tiered_pricing: None,
model_tiered_pricing: None,
..pricing()
};
let result = BillingService::new()
.calculate(&pricing, &processing_usage(Some("fast"), None, 1_000))
.expect("fixed request pricing should calculate Fast usage");
assert_eq!(result.cost_result.status, BillingSnapshotStatus::Complete);
assert_eq!(result.cost_result.cost, 0.02);
assert_eq!(result.pricing_resolution.tiered_pricing, None);
assert_eq!(result.pricing_resolution.price_per_request, Some(0.02));
}
#[test]
fn response_actual_tier_does_not_override_requested_catalog() {
let cases = ["default", "flex", "priority"];
@@ -1592,8 +1651,8 @@ mod tests {
assert_eq!(
service
.estimate_authorization_cost_upper_bound(&processing_pricing(), &estimate)
.expect("unknown tier estimate should resolve"),
None
.expect("unknown tier should inherit the effective Standard catalog"),
Some(0.000925)
);
estimate.requested_processing_tier = Some("priority".to_string());