mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-04 16:37:46 +08:00
feat(usage): enrich audit metadata and detail views
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user