fix(billing): preserve effective cache and tier facts

This commit is contained in:
MMEXA
2026-07-11 18:12:55 +08:00
parent 14c4e3a04e
commit 3014fd50c6
12 changed files with 568 additions and 41 deletions
@@ -2,17 +2,18 @@ mod types;
pub use types::{
extract_provider_actual_service_tier_from_response,
extract_provider_reasoning_effort_from_body, extract_provider_service_tier_from_body,
normalize_provider_service_tier, parse_usage_body_ref, usage_body_ref,
usage_request_metadata_client_family, ApiKeyLastUsedDelta, ManagementTokenCounterDelta,
PendingUsageCleanupSummary, ProviderApiKeyWindowUsageRequest, ProxyNodeCounterDelta,
StoredProviderApiKeyUsageSummary, StoredProviderApiKeyWindowUsageSummary,
StoredProviderUsageSummary, StoredProviderUsageWindow, StoredRequestUsageAudit,
StoredUsageAuditAggregation, StoredUsageAuditSummary, StoredUsageBreakdownSummaryRow,
StoredUsageCacheAffinityHitSummary, StoredUsageCacheAffinityIntervalRow,
StoredUsageCacheHitSummary, StoredUsageCostSavingsSummary, StoredUsageDailySummary,
StoredUsageDashboardDailyBreakdownRow, StoredUsageDashboardProviderCount,
StoredUsageDashboardSummary, StoredUsageErrorDistributionRow, StoredUsageLeaderboardSummary,
extract_provider_cache_ttl_minutes_from_metadata, extract_provider_reasoning_effort_from_body,
extract_provider_service_tier_from_body, normalize_provider_service_tier, parse_usage_body_ref,
resolve_provider_cache_ttl_minutes, usage_body_ref, usage_request_metadata_client_family,
ApiKeyLastUsedDelta, ManagementTokenCounterDelta, PendingUsageCleanupSummary,
ProviderApiKeyWindowUsageRequest, ProxyNodeCounterDelta, StoredProviderApiKeyUsageSummary,
StoredProviderApiKeyWindowUsageSummary, StoredProviderUsageSummary, StoredProviderUsageWindow,
StoredRequestUsageAudit, StoredUsageAuditAggregation, StoredUsageAuditSummary,
StoredUsageBreakdownSummaryRow, StoredUsageCacheAffinityHitSummary,
StoredUsageCacheAffinityIntervalRow, StoredUsageCacheHitSummary, StoredUsageCostSavingsSummary,
StoredUsageDailySummary, StoredUsageDashboardDailyBreakdownRow,
StoredUsageDashboardProviderCount, StoredUsageDashboardSummary,
StoredUsageErrorDistributionRow, StoredUsageLeaderboardSummary,
StoredUsagePerformancePercentilesRow, StoredUsageProviderPerformance,
StoredUsageProviderPerformanceProviderRow, StoredUsageProviderPerformanceSummary,
StoredUsageProviderPerformanceTimelineRow, StoredUsageSettledCostSummary,
@@ -30,5 +31,6 @@ pub use types::{
UsagePerformancePercentilesQuery, UsageProviderPerformanceQuery, UsageReadRepository,
UsageRepository, UsageSettledCostSummaryQuery, UsageTimeSeriesGranularity,
UsageTimeSeriesQuery, UsageWriteRepository, PROVIDER_ACTUAL_SERVICE_TIER_METADATA_KEY,
PROVIDER_REASONING_EFFORT_METADATA_KEY, PROVIDER_SERVICE_TIER_METADATA_KEY,
PROVIDER_CACHE_TTL_MINUTES_METADATA_KEY, PROVIDER_REASONING_EFFORT_METADATA_KEY,
PROVIDER_SERVICE_TIER_METADATA_KEY,
};
@@ -5,6 +5,7 @@ use serde_json::Value;
pub const PROVIDER_REASONING_EFFORT_METADATA_KEY: &str = "provider_reasoning_effort";
pub const PROVIDER_SERVICE_TIER_METADATA_KEY: &str = "provider_service_tier";
pub const PROVIDER_ACTUAL_SERVICE_TIER_METADATA_KEY: &str = "provider_actual_service_tier";
pub const PROVIDER_CACHE_TTL_MINUTES_METADATA_KEY: &str = "provider_cache_ttl_minutes";
pub fn extract_provider_reasoning_effort_from_body(value: Option<&Value>) -> Option<String> {
let object = value.and_then(Value::as_object)?;
@@ -71,6 +72,48 @@ pub fn normalize_provider_service_tier(value: &str) -> Option<String> {
Some(normalized)
}
pub fn resolve_provider_cache_ttl_minutes(
provider_api_format: Option<&str>,
provider_model: Option<&str>,
source_model: Option<&str>,
provider_request_body: Option<&Value>,
) -> Option<i64> {
let provider_api_format = provider_api_format?.trim();
let provider_request_body = provider_request_body?;
let provider_model = provider_request_body
.get("model")
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.or_else(|| {
provider_model
.map(str::trim)
.filter(|value| !value.is_empty())
})?;
let source_model = source_model
.map(str::trim)
.filter(|value| !value.is_empty())
.unwrap_or(provider_model);
aether_ai_formats::resolve_openai_prompt_cache_ttl_minutes(
provider_api_format,
provider_model,
source_model,
provider_request_body,
)
}
pub fn extract_provider_cache_ttl_minutes_from_metadata(value: Option<&Value>) -> Option<i64> {
value
.and_then(Value::as_object)
.and_then(|metadata| metadata.get(PROVIDER_CACHE_TTL_MINUTES_METADATA_KEY))
.and_then(|value| {
value
.as_i64()
.or_else(|| value.as_u64().and_then(|value| i64::try_from(value).ok()))
})
.filter(|value| *value > 0)
}
/// Joined usage read model assembled from the accounting row plus the newer audit/snapshot
/// satellite tables.
///
@@ -482,6 +525,20 @@ impl StoredRequestUsageAudit {
})
}
pub fn provider_cache_ttl_minutes(&self) -> Option<i64> {
resolve_provider_cache_ttl_minutes(
self.endpoint_api_format
.as_deref()
.or(self.api_format.as_deref()),
self.target_model.as_deref().or(Some(self.model.as_str())),
Some(self.model.as_str()),
self.provider_request_body.as_ref(),
)
.or_else(|| {
extract_provider_cache_ttl_minutes_from_metadata(self.request_metadata.as_ref())
})
}
pub fn body_ref(&self, field: UsageBodyField) -> Option<&str> {
match field {
UsageBodyField::RequestBody => self.request_body_ref.as_deref(),
@@ -2097,8 +2154,9 @@ fn parse_timestamp(value: i64, field_name: &str) -> Result<u64, crate::DataLayer
#[cfg(test)]
mod tests {
use super::{
extract_provider_actual_service_tier_from_response, StoredRequestUsageAudit,
UpsertUsageRecord, UsageBodyCaptureState, UsageBodyCaptureStorage, UsageBodyField,
extract_provider_actual_service_tier_from_response, resolve_provider_cache_ttl_minutes,
StoredRequestUsageAudit, UpsertUsageRecord, UsageBodyCaptureState, UsageBodyCaptureStorage,
UsageBodyField,
};
use serde_json::{json, Value};
@@ -2514,6 +2572,43 @@ mod tests {
assert_eq!(usage.provider_service_tier(), None);
}
#[test]
fn provider_cache_ttl_uses_final_openai_contract_then_preserved_metadata() {
let mut usage = sample_usage();
usage.model = "gpt-5.6-sol".to_string();
usage.target_model = Some("gpt-5.6-sol".to_string());
usage.provider_request_body = Some(json!({"model": "gpt-5.6-sol"}));
usage.request_metadata = Some(json!({"provider_cache_ttl_minutes": 60}));
assert_eq!(usage.provider_cache_ttl_minutes(), Some(30));
usage.provider_request_body = None;
usage.request_metadata = Some(json!({"provider_cache_ttl_minutes": 30}));
assert_eq!(usage.provider_cache_ttl_minutes(), Some(30));
}
#[test]
fn provider_cache_ttl_prefers_final_body_model() {
assert_eq!(
resolve_provider_cache_ttl_minutes(
Some("openai:responses"),
Some("gpt-5.5"),
Some("client-alias"),
Some(&json!({"model": "gpt-5.6-sol"})),
),
Some(30)
);
assert_eq!(
resolve_provider_cache_ttl_minutes(
Some("openai:responses"),
Some("gpt-5.6-sol"),
Some("client-alias"),
Some(&json!({"model": "gpt-5.5"})),
),
None
);
}
#[test]
fn actual_service_tier_is_independent_from_requested_tier() {
let mut usage = sample_usage();