feat(usage): enrich audit metadata and detail views

This commit is contained in:
elky
2026-07-17 19:20:16 +08:00
parent 0be380243b
commit 664c063a06
79 changed files with 6968 additions and 1294 deletions
+120 -40
View File
@@ -1,9 +1,7 @@
use aether_data_contracts::repository::billing::StoredBillingModelContext;
use aether_data_contracts::repository::usage::{
extract_provider_actual_service_tier_from_response,
extract_provider_cache_ttl_minutes_from_metadata, extract_provider_service_tier_from_body,
normalize_provider_service_tier, resolve_provider_cache_ttl_minutes,
PROVIDER_ACTUAL_SERVICE_TIER_METADATA_KEY, PROVIDER_SERVICE_TIER_METADATA_KEY,
extract_provider_cache_ttl_minutes_from_metadata, resolve_provider_cache_ttl_minutes,
resolve_provider_service_tier_from_request_capture,
};
use aether_data_contracts::DataLayerError;
use aether_usage_runtime::{UsageEvent, UsageEventType};
@@ -160,7 +158,9 @@ fn calculate_billing_computation(
.clone()
.or_else(|| event.data.api_format.clone()),
requested_processing_tier: processing_tiers.requested,
actual_processing_tier: processing_tiers.actual,
// The response-reported tier remains usage audit data, but it is not authoritative for
// pricing. Settlement follows the final request that was sent upstream.
actual_processing_tier: None,
request_count,
input_tokens: event.data.input_tokens.unwrap_or_default() as i64,
output_tokens: event.data.output_tokens.unwrap_or_default() as i64,
@@ -192,29 +192,18 @@ fn calculate_billing_computation(
#[derive(Debug, Clone, PartialEq, Eq)]
struct UsageEventProcessingTiers {
requested: Option<String>,
actual: Option<String>,
}
fn usage_event_processing_tiers(
data: &aether_usage_runtime::UsageEventData,
) -> UsageEventProcessingTiers {
let metadata = data.request_metadata.as_ref().and_then(Value::as_object);
let requested = extract_provider_service_tier_from_body(data.provider_request_body.as_ref())
.or_else(|| {
metadata
.and_then(|metadata| metadata.get(PROVIDER_SERVICE_TIER_METADATA_KEY))
.and_then(Value::as_str)
.and_then(normalize_provider_service_tier)
});
let actual = metadata
.and_then(|metadata| metadata.get(PROVIDER_ACTUAL_SERVICE_TIER_METADATA_KEY))
.and_then(Value::as_str)
.and_then(normalize_provider_service_tier)
.or_else(|| {
extract_provider_actual_service_tier_from_response(data.response_body.as_ref())
});
let requested = resolve_provider_service_tier_from_request_capture(
data.provider_request_body.as_ref(),
data.provider_request_body_state,
data.request_metadata.as_ref(),
);
UsageEventProcessingTiers { requested, actual }
UsageEventProcessingTiers { requested }
}
fn usage_event_provider_cache_ttl_minutes(
@@ -406,6 +395,7 @@ fn build_settlement_snapshot(
#[cfg(test)]
mod tests {
use aether_data_contracts::repository::billing::StoredBillingModelContext;
use aether_data_contracts::repository::usage::UsageBodyCaptureState;
use aether_usage_runtime::{UsageEvent, UsageEventData, UsageEventType};
use async_trait::async_trait;
use serde_json::json;
@@ -444,7 +434,7 @@ mod tests {
}
#[test]
fn processing_tier_facts_keep_request_and_terminal_response_independent() {
fn processing_tier_fact_uses_final_provider_request_body() {
let data = UsageEventData {
provider_request_body: Some(json!({"service_tier": "Priority"})),
response_body: Some(json!({"service_tier": "priority"})),
@@ -458,7 +448,6 @@ mod tests {
let tiers = usage_event_processing_tiers(&data);
assert_eq!(tiers.requested.as_deref(), Some("priority"));
assert_eq!(tiers.actual.as_deref(), Some("default"));
}
#[test]
@@ -474,7 +463,83 @@ mod tests {
let tiers = usage_event_processing_tiers(&data);
assert_eq!(tiers.requested.as_deref(), Some("fast"));
assert_eq!(tiers.actual.as_deref(), Some("fast"));
}
#[test]
fn processing_tier_does_not_fall_back_to_stale_metadata_when_body_is_present() {
let data = UsageEventData {
provider_request_body: Some(json!({"model": "gpt-5"})),
response_body: Some(json!({"service_tier": "priority"})),
request_metadata: Some(json!({
"provider_service_tier": "priority",
"provider_actual_service_tier": "priority"
})),
..UsageEventData::default()
};
let tiers = usage_event_processing_tiers(&data);
assert_eq!(tiers.requested, None);
}
#[test]
fn processing_tier_uses_request_derived_metadata_when_body_capture_was_disabled() {
let data = UsageEventData {
provider_request_body: None,
provider_request_body_state: Some(UsageBodyCaptureState::Disabled),
response_body: Some(json!({"service_tier": "flex"})),
request_metadata: Some(json!({
"provider_service_tier": "priority",
"provider_actual_service_tier": "flex"
})),
..UsageEventData::default()
};
let tiers = usage_event_processing_tiers(&data);
assert_eq!(tiers.requested.as_deref(), Some("priority"));
}
#[test]
fn processing_tier_does_not_use_metadata_or_response_when_final_request_body_is_missing() {
let data = UsageEventData {
provider_request_body: None,
provider_request_body_state: Some(UsageBodyCaptureState::None),
response_body: Some(json!({"service_tier": "priority"})),
request_metadata: Some(json!({
"provider_service_tier": "priority",
"provider_actual_service_tier": "priority"
})),
..UsageEventData::default()
};
let tiers = usage_event_processing_tiers(&data);
assert_eq!(tiers.requested, None);
}
#[test]
fn processing_tier_uses_request_derived_metadata_when_body_capture_was_truncated() {
let data = UsageEventData {
provider_request_body: Some(json!({
"truncated": true,
"reason": "body_capture_limit_exceeded",
"max_bytes": 128,
"source_bytes": 4096,
"value_kind": "object"
})),
provider_request_body_state: Some(UsageBodyCaptureState::Truncated),
response_body: Some(json!({"service_tier": "default"})),
request_metadata: Some(json!({
"provider_service_tier": "priority",
"provider_actual_service_tier": "default"
})),
..UsageEventData::default()
};
let tiers = usage_event_processing_tiers(&data);
assert_eq!(tiers.requested.as_deref(), Some("priority"));
}
#[tokio::test]
@@ -638,7 +703,7 @@ mod tests {
}
#[tokio::test]
async fn settlement_uses_actual_processing_tier_catalog_and_source() {
async fn settlement_uses_requested_processing_tier_catalog_and_ignores_response_tier() {
let lookup = TestLookup {
name_context: Some(
StoredBillingModelContext::new(
@@ -706,13 +771,28 @@ mod tests {
.and_then(|value| value.pointer("/settlement_snapshot/pricing_snapshot"))
.expect("settlement pricing snapshot should exist");
assert_eq!(pricing_snapshot["requested_processing_tier"], "priority");
assert_eq!(pricing_snapshot["actual_processing_tier"], "flex");
assert_eq!(pricing_snapshot["billing_processing_tier"], "flex");
assert_eq!(pricing_snapshot["tiered_pricing_source"], "global_default");
assert_eq!(pricing_snapshot["processing_tier_price_multiplier"], 0.5);
assert!(pricing_snapshot["actual_processing_tier"].is_null());
assert_eq!(pricing_snapshot["billing_processing_tier"], "priority");
assert_eq!(
pricing_snapshot["tiered_pricing_source"],
"provider_override"
);
assert_eq!(
pricing_snapshot["processing_tier_price_multiplier"],
Value::Null
);
assert_eq!(
pricing_snapshot["tiered_pricing"]["tiers"][0]["input_price_per_1m"],
2.5
10.0
);
// The response fact remains available for audit, but does not influence settlement.
assert_eq!(
event
.data
.request_metadata
.as_ref()
.and_then(|value| value.get("provider_actual_service_tier")),
Some(&json!("flex"))
);
assert_eq!(
event
@@ -720,12 +800,12 @@ mod tests {
.request_metadata
.as_ref()
.and_then(|value| { value.pointer("/billing_dimensions/actual_processing_tier") }),
Some(&json!("flex"))
Some(&Value::Null)
);
}
#[tokio::test]
async fn actual_processing_catalog_controls_image_price_with_independent_fixed_price() {
async fn requested_processing_catalog_controls_image_price_with_independent_fixed_price() {
let lookup = TestLookup {
name_context: Some(
StoredBillingModelContext::new(
@@ -783,27 +863,27 @@ mod tests {
.await
.expect("billing should succeed");
assert_eq!(event.data.total_cost_usd, Some(0.44));
assert_eq!(event.data.actual_total_cost_usd, Some(0.44));
assert_eq!(event.data.total_cost_usd, Some(0.84));
assert_eq!(event.data.actual_total_cost_usd, Some(0.84));
let metadata = event.data.request_metadata.as_ref().expect("metadata");
let pricing = metadata
.pointer("/settlement_snapshot/pricing_snapshot")
.expect("pricing snapshot");
assert_eq!(pricing["billing_processing_tier"], "flex");
assert_eq!(pricing["tiered_pricing_source"], "global_default");
assert_eq!(pricing["billing_processing_tier"], "priority");
assert_eq!(pricing["tiered_pricing_source"], "provider_override");
assert_eq!(pricing["price_per_request_source"], "provider_override");
assert_eq!(pricing["pricing_source"], "mixed");
assert_eq!(pricing["pricing_source"], "provider_override");
assert_eq!(
metadata
.pointer("/billing_snapshot/resolved_variables/image_output_price_per_image")
.and_then(Value::as_f64),
Some(0.2)
Some(0.4)
);
assert_eq!(
metadata
.pointer("/billing_snapshot/cost_breakdown/image_output_cost")
.and_then(Value::as_f64),
Some(0.4)
Some(0.8)
);
assert_eq!(
metadata
+34 -63
View File
@@ -1,5 +1,3 @@
use std::collections::BTreeSet;
use aether_data_contracts::repository::{
billing::StoredBillingModelContext,
global_models::{explicit_pricing_catalog_state, ExplicitPricingCatalogState},
@@ -108,17 +106,12 @@ impl BillingModelPricingSnapshot {
) -> BillingPricingResolution {
let requested_processing_tier = normalize_processing_tier(requested_processing_tier);
let actual_processing_tier = normalize_processing_tier(actual_processing_tier);
let billing_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(|| {
requested_processing_tier.as_deref().map_or(
Some("standard".to_string()),
|requested| {
processing_tier_is_standard(requested).then(|| "standard".to_string())
},
)
});
.or_else(|| Some("standard".to_string()));
let (tiered_pricing, tiered_pricing_source, processing_tier_price_multiplier) =
billing_processing_tier
@@ -151,17 +144,12 @@ impl BillingModelPricingSnapshot {
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);
let billing_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(|| {
requested_processing_tier.as_deref().map_or(
Some("standard".to_string()),
|requested| {
processing_tier_is_standard(requested).then(|| "standard".to_string())
},
)
});
.or_else(|| Some("standard".to_string()));
let (tiered_pricing, tiered_pricing_source, processing_tier_price_multiplier) =
match billing_processing_tier.as_deref() {
@@ -208,34 +196,7 @@ impl BillingModelPricingSnapshot {
return Ok(None);
}
let mut billing_tiers = BTreeSet::from(["standard".to_string(), requested_billing_tier]);
for pricing in [
self.model_tiered_pricing.as_ref(),
self.default_tiered_pricing.as_ref(),
]
.into_iter()
.flatten()
{
let Some(processing_tiers) = pricing.get("processing_tiers").and_then(Value::as_object)
else {
continue;
};
billing_tiers.extend(processing_tiers.keys().filter_map(|tier| {
normalize_processing_tier(Some(tier)).map(|tier| canonical_processing_tier(&tier))
}));
}
let mut candidates = Vec::new();
for billing_tier in billing_tiers {
let resolution = self.authorization_pricing_for_tier(
requested_processing_tier.clone(),
Some(billing_tier),
)?;
if resolution.bills_standard_processing_tier() || resolution.tiered_pricing.is_some() {
candidates.push(resolution);
}
}
Ok((!candidates.is_empty()).then_some(candidates))
Ok(Some(vec![requested_resolution]))
}
pub fn validate_authorization_pricing_configuration(
@@ -839,7 +800,7 @@ mod tests {
}
#[test]
fn explicit_nonstandard_request_requires_actual_tier() {
fn explicit_nonstandard_request_selects_requested_catalog_without_actual_tier() {
let pricing = snapshot(
None,
Some(json!({
@@ -852,17 +813,27 @@ mod tests {
let resolution = pricing.resolve_pricing(Some("Priority"), None);
assert!(resolution.requires_actual_processing_tier());
assert!(!resolution.requires_actual_processing_tier());
assert_eq!(
resolution.requested_processing_tier.as_deref(),
Some("priority")
);
assert_eq!(resolution.billing_processing_tier, None);
assert_eq!(resolution.tiered_pricing, 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)
);
}
#[test]
fn actual_tier_selects_exact_catalog_and_source() {
fn response_actual_tier_is_audited_but_does_not_select_pricing_catalog() {
let pricing = snapshot(
Some(json!({
"processing_tiers": {
@@ -878,28 +849,29 @@ mod tests {
);
let flex = pricing.resolve_pricing(Some("priority"), Some("flex"));
assert_eq!(flex.billing_processing_tier.as_deref(), 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::GlobalDefault)
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(1.5)
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("standard")
Some("priority")
);
assert_eq!(
standard.tiered_pricing_source,
Some(BillingPricingSource::GlobalDefault)
Some(BillingPricingSource::ProviderOverride)
);
}
@@ -962,9 +934,7 @@ mod tests {
.and_then(serde_json::Value::as_f64),
Some(6.0)
);
assert!(candidates.iter().any(|resolution| {
resolution.billing_processing_tier.as_deref() == Some("standard")
}));
assert_eq!(candidates.len(), 1);
}
#[test]
@@ -1222,7 +1192,7 @@ mod tests {
}
#[test]
fn actual_claude_fast_uses_the_exact_provider_multiplier_overlay() {
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}],
@@ -1234,8 +1204,9 @@ mod tests {
})),
);
let resolved = pricing.resolve_pricing(Some("priority"), Some("fast"));
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
+50 -44
View File
@@ -1104,7 +1104,7 @@ mod tests {
}
#[test]
fn nonstandard_request_without_actual_tier_fails_closed() {
fn nonstandard_request_without_actual_tier_uses_requested_catalog() {
let result = BillingService::new()
.calculate(
&processing_pricing(),
@@ -1112,57 +1112,49 @@ mod tests {
)
.expect("billing should calculate");
assert_eq!(result.cost_result.status, BillingSnapshotStatus::NoRule);
assert_eq!(
result.cost_result.snapshot.missing_required,
vec!["actual_processing_tier"]
);
assert_eq!(result.cost_result.status, BillingSnapshotStatus::Complete);
assert_eq!(
result.cost_result.snapshot.resolved_dimensions["billing_processing_tier"],
json!(null)
json!("priority")
);
assert_eq!(
result.cost_result.snapshot.resolved_variables["input_price_per_1m"],
json!(10.0)
);
}
#[test]
fn actual_tier_controls_standard_flex_and_priority_catalogs() {
let cases = [
(
"default",
100,
5.0,
6.25,
BillingPricingSource::GlobalDefault,
),
("flex", 100, 2.5, 3.125, BillingPricingSource::GlobalDefault),
(
"priority",
100,
10.0,
12.5,
BillingPricingSource::ProviderOverride,
),
];
fn response_actual_tier_does_not_override_requested_catalog() {
let cases = ["default", "flex", "priority"];
for (actual, input_tokens, input_price, cache_write_price, source) in cases {
for actual in cases {
let result = BillingService::new()
.calculate(
&processing_pricing(),
&processing_usage(Some("priority"), Some(actual), input_tokens),
&processing_usage(Some("priority"), Some(actual), 100),
)
.expect("processing tier should resolve");
assert_eq!(result.cost_result.status, BillingSnapshotStatus::Complete);
assert_eq!(
result.pricing_resolution.actual_processing_tier.as_deref(),
Some(actual)
);
assert_eq!(
result.pricing_resolution.billing_processing_tier.as_deref(),
Some("priority")
);
assert_eq!(
result.cost_result.snapshot.resolved_variables["input_price_per_1m"],
json!(input_price)
json!(10.0)
);
assert_eq!(
result.cost_result.snapshot.resolved_variables["cache_creation_price_per_1m"],
json!(cache_write_price)
json!(12.5)
);
assert_eq!(
result.pricing_resolution.tiered_pricing_source,
Some(source)
Some(BillingPricingSource::ProviderOverride)
);
}
}
@@ -1373,7 +1365,7 @@ mod tests {
}
#[test]
fn finite_processing_catalog_and_unknown_actual_tier_fail_closed() {
fn finite_processing_catalog_fails_only_on_requested_catalog_bounds() {
let priority = BillingService::new()
.calculate(
&processing_pricing(),
@@ -1386,16 +1378,22 @@ mod tests {
vec!["input_context_tier"]
);
let unknown = BillingService::new()
let conflicting_actual = BillingService::new()
.calculate(
&processing_pricing(),
&processing_usage(Some("priority"), Some("expedited"), 100),
)
.expect("billing should calculate");
assert_eq!(unknown.cost_result.status, BillingSnapshotStatus::NoRule);
assert_eq!(
unknown.cost_result.snapshot.missing_required,
vec!["processing_tier_catalog"]
conflicting_actual.cost_result.status,
BillingSnapshotStatus::Complete
);
assert_eq!(
conflicting_actual
.pricing_resolution
.billing_processing_tier
.as_deref(),
Some("priority")
);
}
@@ -1502,13 +1500,17 @@ mod tests {
estimate.max_output_tokens = Some(0);
estimate.cache_ttl_minutes = Some(30);
for requested_processing_tier in [None, Some("standard"), Some("flex")] {
for (requested_processing_tier, expected) in [
(None, 3.75),
(Some("standard"), 3.75),
(Some("flex"), 1.875),
] {
estimate.requested_processing_tier = requested_processing_tier.map(ToOwned::to_owned);
assert_eq!(
service
.estimate_authorization_cost_upper_bound(&processing_pricing(), &estimate)
.expect("eligible processing catalogs should calculate"),
Some(3.75),
Some(expected),
"requested tier: {requested_processing_tier:?}"
);
}
@@ -1523,7 +1525,7 @@ mod tests {
}
#[test]
fn unknown_actual_tier_cannot_fall_back_to_fixed_request_price() {
fn unknown_actual_tier_does_not_override_requested_tier_or_fixed_price() {
let pricing = BillingModelPricingSnapshot {
default_price_per_request: Some(0.02),
..processing_pricing()
@@ -1535,16 +1537,20 @@ mod tests {
)
.expect("billing should calculate");
assert_eq!(result.cost_result.status, BillingSnapshotStatus::NoRule);
assert_eq!(result.cost_result.status, BillingSnapshotStatus::Complete);
assert_eq!(
result.cost_result.snapshot.missing_required,
vec!["processing_tier_catalog"]
result.pricing_resolution.billing_processing_tier.as_deref(),
Some("priority")
);
assert_eq!(
result.pricing_resolution.actual_processing_tier.as_deref(),
Some("expedited")
);
assert_eq!(result.pricing_resolution.price_per_request, Some(0.02));
}
#[test]
fn authorization_estimate_bounds_requested_and_provider_actual_catalogs() {
fn authorization_estimate_bounds_only_the_requested_catalog() {
let service = BillingService::new();
let mut estimate = BillingAuthorizationEstimateInput::new("chat", 100_000);
estimate.api_format = Some("openai:responses".to_string());
@@ -1563,8 +1569,8 @@ mod tests {
.expect("flex estimate should be bounded");
assert_eq!(priority, 61.25);
assert_eq!(flex, 61.25);
assert_eq!(priority, flex);
assert_eq!(flex, 15.3125);
assert!(priority > flex);
}
#[test]