持久化 provider key 使用统计并移除 usage 汇总覆盖 (#313)

* 持久化 provider key 使用统计并移除 usage 汇总覆盖

- 在 usage upsert 时按请求前后差值同步 provider_api_keys 统计
- 增加基于保留 usage 记录的 provider key 统计重建能力
- 号池管理列表直接读取 provider_api_keys 统计字段

* fix: harden provider key usage stats sync

---------

Co-authored-by: fawney19 <elky0401@gmail.com>
This commit is contained in:
AAEE86
2026-04-19 01:02:09 +08:00
committed by GitHub
parent 425227509a
commit bafb63a665
9 changed files with 1166 additions and 191 deletions

View File

@@ -8,6 +8,7 @@ use super::{
ProviderCatalogWriteRepository, StoredProviderCatalogEndpoint, StoredProviderCatalogKey,
StoredProviderCatalogKeyPage, StoredProviderCatalogKeyStats, StoredProviderCatalogProvider,
};
use crate::repository::usage::{ProviderApiKeyUsageContribution, ProviderApiKeyUsageDelta};
use crate::DataLayerError;
#[derive(Debug, Default)]
@@ -42,6 +43,117 @@ impl InMemoryProviderCatalogReadRepository {
}),
}
}
pub(crate) fn apply_usage_stats_delta(
&self,
key_id: &str,
delta: &ProviderApiKeyUsageDelta,
recomputed_last_used_at_unix_secs: Option<u64>,
) {
let mut index = self
.index
.write()
.expect("provider catalog repository lock");
let Some(key) = index.keys.get_mut(key_id) else {
return;
};
key.request_count = Some(apply_i64_delta_to_u32(
key.request_count.unwrap_or_default(),
delta.request_count,
));
key.success_count = Some(apply_i64_delta_to_u32(
key.success_count.unwrap_or_default(),
delta.success_count,
));
key.error_count = Some(apply_i64_delta_to_u32(
key.error_count.unwrap_or_default(),
delta.error_count,
));
key.total_tokens = apply_i64_delta_to_u64(key.total_tokens, delta.total_tokens);
key.total_cost_usd = apply_f64_delta(key.total_cost_usd, delta.total_cost_usd);
key.total_response_time_ms = Some(apply_i64_delta_to_u32(
key.total_response_time_ms.unwrap_or_default(),
delta.total_response_time_ms,
));
if let Some(candidate) = delta.candidate_last_used_at_unix_secs {
key.last_used_at_unix_secs = Some(
key.last_used_at_unix_secs
.map(|existing| existing.max(candidate))
.unwrap_or(candidate),
);
} else if delta.removed_last_used_at_unix_secs.is_some()
&& key.last_used_at_unix_secs == delta.removed_last_used_at_unix_secs
{
key.last_used_at_unix_secs = recomputed_last_used_at_unix_secs;
}
}
pub(crate) fn rebuild_usage_stats(
&self,
contributions: &BTreeMap<String, ProviderApiKeyUsageContribution>,
) {
let mut index = self
.index
.write()
.expect("provider catalog repository lock");
for key in index.keys.values_mut() {
key.request_count = Some(0);
key.success_count = Some(0);
key.error_count = Some(0);
key.total_tokens = 0;
key.total_cost_usd = 0.0;
key.total_response_time_ms = Some(0);
key.last_used_at_unix_secs = None;
}
for (key_id, contribution) in contributions {
let Some(key) = index.keys.get_mut(key_id) else {
continue;
};
key.request_count = Some(clamp_i64_to_u32(contribution.request_count));
key.success_count = Some(clamp_i64_to_u32(contribution.success_count));
key.error_count = Some(clamp_i64_to_u32(contribution.error_count));
key.total_tokens = clamp_i64_to_u64(contribution.total_tokens);
key.total_cost_usd = contribution.total_cost_usd.max(0.0);
key.total_response_time_ms =
Some(clamp_i64_to_u32(contribution.total_response_time_ms));
key.last_used_at_unix_secs = contribution.last_used_at_unix_secs;
}
}
}
fn apply_i64_delta_to_u32(current: u32, delta: i64) -> u32 {
clamp_i64_to_u32(i64::from(current).saturating_add(delta))
}
fn apply_i64_delta_to_u64(current: u64, delta: i64) -> u64 {
if delta >= 0 {
current.saturating_add(delta as u64)
} else {
current.saturating_sub(delta.unsigned_abs())
}
}
fn clamp_i64_to_u32(value: i64) -> u32 {
value.clamp(0, i64::from(u32::MAX)) as u32
}
fn clamp_i64_to_u64(value: i64) -> u64 {
value.max(0) as u64
}
fn apply_f64_delta(current: f64, delta: f64) -> f64 {
if !current.is_finite() && !delta.is_finite() {
return 0.0;
}
let next = current.max(0.0) + delta;
if next.is_finite() {
next.max(0.0)
} else {
0.0
}
}
#[async_trait]

View File

@@ -1,4 +1,5 @@
use std::collections::BTreeMap;
use std::sync::Arc;
use std::sync::RwLock;
use aether_data_contracts::repository::usage::{
@@ -23,11 +24,13 @@ use chrono::Utc;
use serde_json::Value;
use super::{
strip_deprecated_usage_display_fields, usage_can_recover_terminal_failure,
provider_api_key_usage_contribution, strip_deprecated_usage_display_fields,
usage_can_recover_terminal_failure, ProviderApiKeyUsageContribution, ProviderApiKeyUsageDelta,
StoredProviderApiKeyUsageSummary, StoredProviderUsageSummary, StoredProviderUsageWindow,
StoredRequestUsageAudit, StoredUsageDailySummary, UpsertUsageRecord, UsageAuditListQuery,
UsageDailyHeatmapQuery, UsageReadRepository, UsageWriteRepository,
};
use crate::repository::provider_catalog::InMemoryProviderCatalogReadRepository;
use crate::DataLayerError;
#[derive(Debug, Default)]
@@ -35,6 +38,7 @@ pub struct InMemoryUsageReadRepository {
by_request_id: RwLock<BTreeMap<String, StoredRequestUsageAudit>>,
detached_bodies: RwLock<BTreeMap<String, Value>>,
provider_usage_windows: RwLock<Vec<StoredProviderUsageWindow>>,
provider_catalog: Option<Arc<InMemoryProviderCatalogReadRepository>>,
}
impl InMemoryUsageReadRepository {
@@ -51,6 +55,7 @@ impl InMemoryUsageReadRepository {
by_request_id: RwLock::new(by_request_id),
detached_bodies: RwLock::new(BTreeMap::new()),
provider_usage_windows: RwLock::new(Vec::new()),
provider_catalog: None,
}
}
@@ -101,6 +106,7 @@ impl InMemoryUsageReadRepository {
by_request_id: RwLock::new(by_request_id),
detached_bodies: RwLock::new(detached_bodies),
provider_usage_windows: RwLock::new(Vec::new()),
provider_catalog: None,
}
}
@@ -112,8 +118,17 @@ impl InMemoryUsageReadRepository {
by_request_id: self.by_request_id,
detached_bodies: self.detached_bodies,
provider_usage_windows: RwLock::new(items.into_iter().collect()),
provider_catalog: self.provider_catalog,
}
}
pub fn with_provider_catalog_repository(
mut self,
repository: Arc<InMemoryProviderCatalogReadRepository>,
) -> Self {
self.provider_catalog = Some(repository);
self
}
}
fn usage_status_is_finalized(status: &str) -> bool {
@@ -124,6 +139,38 @@ fn usage_status_is_lifecycle(status: &str) -> bool {
matches!(status, "pending" | "streaming")
}
fn accumulate_provider_api_key_usage_contribution(
aggregates: &mut BTreeMap<String, ProviderApiKeyUsageContribution>,
contribution: ProviderApiKeyUsageContribution,
) {
let entry = aggregates
.entry(contribution.key_id.clone())
.or_insert_with(|| ProviderApiKeyUsageContribution {
key_id: contribution.key_id.clone(),
..ProviderApiKeyUsageContribution::default()
});
entry.request_count = entry
.request_count
.saturating_add(contribution.request_count);
entry.success_count = entry
.success_count
.saturating_add(contribution.success_count);
entry.error_count = entry.error_count.saturating_add(contribution.error_count);
entry.total_tokens = entry.total_tokens.saturating_add(contribution.total_tokens);
entry.total_cost_usd += contribution.total_cost_usd;
entry.total_response_time_ms = entry
.total_response_time_ms
.saturating_add(contribution.total_response_time_ms);
entry.last_used_at_unix_secs = match (
entry.last_used_at_unix_secs,
contribution.last_used_at_unix_secs,
) {
(Some(existing), Some(candidate)) => Some(existing.max(candidate)),
(None, Some(candidate)) => Some(candidate),
(existing, None) => existing,
};
}
fn usage_matches_list_query(item: &StoredRequestUsageAudit, query: &UsageAuditListQuery) -> bool {
// The field is historically named `created_at_unix_ms`, but usage audit rows
// across gateway handlers, SQL repositories and tests are stored as epoch seconds.
@@ -2039,6 +2086,7 @@ impl UsageWriteRepository for InMemoryUsageReadRepository {
usage.validate()?;
let usage = strip_deprecated_usage_display_fields(usage);
let mut by_request_id = self.by_request_id.write().expect("usage repository lock");
let existing = by_request_id.get(&usage.request_id).cloned();
let created_at_unix_ms = by_request_id
.get(&usage.request_id)
@@ -2055,8 +2103,7 @@ impl UsageWriteRepository for InMemoryUsageReadRepository {
)
})
.unwrap_or_default();
let existing = by_request_id.get(&usage.request_id);
if existing.is_some_and(|existing| {
if existing.as_ref().is_some_and(|existing| {
usage_status_is_finalized(existing.status.as_str())
&& usage_status_is_lifecycle(usage.status.as_str())
&& !usage_can_recover_terminal_failure(
@@ -2068,7 +2115,7 @@ impl UsageWriteRepository for InMemoryUsageReadRepository {
}) {
return Ok(existing.expect("existing usage should be present").clone());
}
if existing.is_some_and(|existing| {
if existing.as_ref().is_some_and(|existing| {
existing.billing_status == "pending"
&& existing.status == "streaming"
&& usage.status == "pending"
@@ -2076,47 +2123,53 @@ impl UsageWriteRepository for InMemoryUsageReadRepository {
return Ok(existing.expect("existing usage should be present").clone());
}
let request_metadata = usage
.request_metadata
.clone()
.or_else(|| existing.and_then(|existing| existing.request_metadata.clone()));
let request_metadata = usage.request_metadata.clone().or_else(|| {
existing
.as_ref()
.and_then(|existing| existing.request_metadata.clone())
});
let request_body_ref = persisted_usage_body_ref(
usage.request_body_ref.as_deref(),
usage.request_body.as_ref(),
request_metadata.as_ref(),
existing,
existing.as_ref(),
UsageBodyField::RequestBody,
);
let provider_request_body_ref = persisted_usage_body_ref(
usage.provider_request_body_ref.as_deref(),
usage.provider_request_body.as_ref(),
request_metadata.as_ref(),
existing,
existing.as_ref(),
UsageBodyField::ProviderRequestBody,
);
let response_body_ref = persisted_usage_body_ref(
usage.response_body_ref.as_deref(),
usage.response_body.as_ref(),
request_metadata.as_ref(),
existing,
existing.as_ref(),
UsageBodyField::ResponseBody,
);
let client_response_body_ref = persisted_usage_body_ref(
usage.client_response_body_ref.as_deref(),
usage.client_response_body.as_ref(),
request_metadata.as_ref(),
existing,
existing.as_ref(),
UsageBodyField::ClientResponseBody,
);
let stored = StoredRequestUsageAudit {
id: existing
.as_ref()
.map(|existing| existing.id.clone())
.unwrap_or_else(|| format!("usage-{}", usage.request_id)),
request_id: usage.request_id.clone(),
user_id: usage.user_id,
api_key_id: usage.api_key_id,
username: existing.and_then(|existing| existing.username.clone()),
api_key_name: existing.and_then(|existing| existing.api_key_name.clone()),
username: existing
.as_ref()
.and_then(|existing| existing.username.clone()),
api_key_name: existing
.as_ref()
.and_then(|existing| existing.api_key_name.clone()),
provider_name: usage.provider_name,
model: usage.model,
target_model: usage.target_model,
@@ -2137,6 +2190,7 @@ impl UsageWriteRepository for InMemoryUsageReadRepository {
total_tokens,
cache_creation_input_tokens: usage.cache_creation_input_tokens.unwrap_or_else(|| {
existing
.as_ref()
.map(|existing| existing.cache_creation_input_tokens)
.unwrap_or_default()
}),
@@ -2144,6 +2198,7 @@ impl UsageWriteRepository for InMemoryUsageReadRepository {
.cache_creation_ephemeral_5m_input_tokens
.unwrap_or_else(|| {
existing
.as_ref()
.map(|existing| existing.cache_creation_ephemeral_5m_input_tokens)
.unwrap_or_default()
}),
@@ -2151,32 +2206,40 @@ impl UsageWriteRepository for InMemoryUsageReadRepository {
.cache_creation_ephemeral_1h_input_tokens
.unwrap_or_else(|| {
existing
.as_ref()
.map(|existing| existing.cache_creation_ephemeral_1h_input_tokens)
.unwrap_or_default()
}),
cache_read_input_tokens: usage.cache_read_input_tokens.unwrap_or_else(|| {
existing
.as_ref()
.map(|existing| existing.cache_read_input_tokens)
.unwrap_or_default()
}),
cache_creation_cost_usd: usage.cache_creation_cost_usd.unwrap_or_else(|| {
existing
.as_ref()
.map(|existing| existing.cache_creation_cost_usd)
.unwrap_or_default()
}),
cache_read_cost_usd: usage.cache_read_cost_usd.unwrap_or_else(|| {
existing
.as_ref()
.map(|existing| existing.cache_read_cost_usd)
.unwrap_or_default()
}),
output_price_per_1m: existing.and_then(|existing| existing.output_price_per_1m),
output_price_per_1m: existing
.as_ref()
.and_then(|existing| existing.output_price_per_1m),
total_cost_usd: usage.total_cost_usd.unwrap_or_else(|| {
existing
.as_ref()
.map(|existing| existing.total_cost_usd)
.unwrap_or_default()
}),
actual_total_cost_usd: usage.actual_total_cost_usd.unwrap_or_else(|| {
existing
.as_ref()
.map(|existing| existing.actual_total_cost_usd)
.unwrap_or_default()
}),
@@ -2187,71 +2250,108 @@ impl UsageWriteRepository for InMemoryUsageReadRepository {
first_byte_time_ms: usage.first_byte_time_ms,
status: usage.status,
billing_status: usage.billing_status,
request_headers: usage
.request_headers
.or_else(|| existing.and_then(|existing| existing.request_headers.clone())),
request_body: usage
.request_body
.or_else(|| existing.and_then(|existing| existing.request_body.clone())),
request_headers: usage.request_headers.or_else(|| {
existing
.as_ref()
.and_then(|existing| existing.request_headers.clone())
}),
request_body: usage.request_body.or_else(|| {
existing
.as_ref()
.and_then(|existing| existing.request_body.clone())
}),
request_body_ref,
request_body_state: usage
.request_body_state
.or_else(|| existing.and_then(|existing| existing.request_body_state)),
request_body_state: usage.request_body_state.or_else(|| {
existing
.as_ref()
.and_then(|existing| existing.request_body_state)
}),
provider_request_headers: usage.provider_request_headers.or_else(|| {
existing.and_then(|existing| existing.provider_request_headers.clone())
existing
.as_ref()
.and_then(|existing| existing.provider_request_headers.clone())
}),
provider_request_body: usage.provider_request_body.or_else(|| {
existing
.as_ref()
.and_then(|existing| existing.provider_request_body.clone())
}),
provider_request_body: usage
.provider_request_body
.or_else(|| existing.and_then(|existing| existing.provider_request_body.clone())),
provider_request_body_ref,
provider_request_body_state: usage
.provider_request_body_state
.or_else(|| existing.and_then(|existing| existing.provider_request_body_state)),
response_headers: usage
.response_headers
.or_else(|| existing.and_then(|existing| existing.response_headers.clone())),
response_body: usage
.response_body
.or_else(|| existing.and_then(|existing| existing.response_body.clone())),
response_body_ref,
response_body_state: usage
.response_body_state
.or_else(|| existing.and_then(|existing| existing.response_body_state)),
client_response_headers: usage
.client_response_headers
.or_else(|| existing.and_then(|existing| existing.client_response_headers.clone())),
client_response_body: usage
.client_response_body
.or_else(|| existing.and_then(|existing| existing.client_response_body.clone())),
client_response_body_ref,
client_response_body_state: usage
.client_response_body_state
.or_else(|| existing.and_then(|existing| existing.client_response_body_state)),
candidate_id: usage.candidate_id.or_else(|| {
existing.and_then(|existing| existing.routing_candidate_id().map(ToOwned::to_owned))
provider_request_body_state: usage.provider_request_body_state.or_else(|| {
existing
.as_ref()
.and_then(|existing| existing.provider_request_body_state)
}),
response_headers: usage.response_headers.or_else(|| {
existing
.as_ref()
.and_then(|existing| existing.response_headers.clone())
}),
response_body: usage.response_body.or_else(|| {
existing
.as_ref()
.and_then(|existing| existing.response_body.clone())
}),
response_body_ref,
response_body_state: usage.response_body_state.or_else(|| {
existing
.as_ref()
.and_then(|existing| existing.response_body_state)
}),
client_response_headers: usage.client_response_headers.or_else(|| {
existing
.as_ref()
.and_then(|existing| existing.client_response_headers.clone())
}),
client_response_body: usage.client_response_body.or_else(|| {
existing
.as_ref()
.and_then(|existing| existing.client_response_body.clone())
}),
client_response_body_ref,
client_response_body_state: usage.client_response_body_state.or_else(|| {
existing
.as_ref()
.and_then(|existing| existing.client_response_body_state)
}),
candidate_id: usage.candidate_id.or_else(|| {
existing
.as_ref()
.and_then(|existing| existing.routing_candidate_id().map(ToOwned::to_owned))
}),
candidate_index: usage.candidate_index.or_else(|| {
existing
.as_ref()
.and_then(|existing| existing.routing_candidate_index())
}),
candidate_index: usage
.candidate_index
.or_else(|| existing.and_then(|existing| existing.routing_candidate_index())),
key_name: usage.key_name.or_else(|| {
existing.and_then(|existing| existing.routing_key_name().map(ToOwned::to_owned))
existing
.as_ref()
.and_then(|existing| existing.routing_key_name().map(ToOwned::to_owned))
}),
planner_kind: usage.planner_kind.or_else(|| {
existing.and_then(|existing| existing.routing_planner_kind().map(ToOwned::to_owned))
existing
.as_ref()
.and_then(|existing| existing.routing_planner_kind().map(ToOwned::to_owned))
}),
route_family: usage.route_family.or_else(|| {
existing.and_then(|existing| existing.routing_route_family().map(ToOwned::to_owned))
existing
.as_ref()
.and_then(|existing| existing.routing_route_family().map(ToOwned::to_owned))
}),
route_kind: usage.route_kind.or_else(|| {
existing.and_then(|existing| existing.routing_route_kind().map(ToOwned::to_owned))
existing
.as_ref()
.and_then(|existing| existing.routing_route_kind().map(ToOwned::to_owned))
}),
execution_path: usage.execution_path.or_else(|| {
existing
.as_ref()
.and_then(|existing| existing.routing_execution_path().map(ToOwned::to_owned))
}),
local_execution_runtime_miss_reason: usage.local_execution_runtime_miss_reason.or_else(
|| {
existing.and_then(|existing| {
existing.as_ref().and_then(|existing| {
existing
.routing_local_execution_runtime_miss_reason()
.map(ToOwned::to_owned)
@@ -2265,13 +2365,76 @@ impl UsageWriteRepository for InMemoryUsageReadRepository {
};
by_request_id.insert(stored.request_id.clone(), stored.clone());
if let Some(provider_catalog) = self.provider_catalog.as_ref() {
let before_contribution = existing
.as_ref()
.and_then(provider_api_key_usage_contribution);
let after_contribution = provider_api_key_usage_contribution(&stored);
match (before_contribution.as_ref(), after_contribution.as_ref()) {
(Some(before), Some(after)) if before.key_id == after.key_id => {
let delta = ProviderApiKeyUsageDelta::between(before, after);
provider_catalog.apply_usage_stats_delta(before.key_id.as_str(), &delta, None);
}
_ => {
if let Some(before) = before_contribution.as_ref() {
let delta = ProviderApiKeyUsageDelta::removal(before);
let recomputed_last_used_at_unix_secs = by_request_id
.values()
.filter_map(|item| {
item.provider_api_key_id
.as_deref()
.filter(|key_id| *key_id == before.key_id.as_str())
.map(|_| item.created_at_unix_ms)
})
.max();
provider_catalog.apply_usage_stats_delta(
before.key_id.as_str(),
&delta,
recomputed_last_used_at_unix_secs,
);
}
if let Some(after) = after_contribution.as_ref() {
let delta = ProviderApiKeyUsageDelta::addition(after);
provider_catalog.apply_usage_stats_delta(
after.key_id.as_str(),
&delta,
None,
);
}
}
}
}
Ok(stored)
}
async fn rebuild_provider_api_key_usage_stats(&self) -> Result<u64, DataLayerError> {
let Some(provider_catalog) = self.provider_catalog.as_ref() else {
return Ok(0);
};
let by_request_id = self.by_request_id.read().expect("usage repository lock");
let mut aggregates = BTreeMap::new();
for usage in by_request_id.values() {
let Some(contribution) = provider_api_key_usage_contribution(usage) else {
continue;
};
accumulate_provider_api_key_usage_contribution(&mut aggregates, contribution);
}
provider_catalog.rebuild_usage_stats(&aggregates);
Ok(aggregates.len() as u64)
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use super::InMemoryUsageReadRepository;
use crate::repository::provider_catalog::{
InMemoryProviderCatalogReadRepository, ProviderCatalogReadRepository,
ProviderCatalogWriteRepository, StoredProviderCatalogKey, StoredProviderCatalogProvider,
};
use crate::repository::usage::{
StoredProviderUsageWindow, StoredRequestUsageAudit, UpsertUsageRecord, UsageReadRepository,
UsageWriteRepository,
@@ -2321,6 +2484,78 @@ mod tests {
.expect("usage should build")
}
fn sample_upsert_usage_record(request_id: &str) -> UpsertUsageRecord {
UpsertUsageRecord {
request_id: request_id.to_string(),
user_id: None,
api_key_id: None,
username: None,
api_key_name: None,
provider_name: "OpenAI".to_string(),
model: "gpt-5".to_string(),
target_model: None,
provider_id: Some("provider-1".to_string()),
provider_endpoint_id: None,
provider_api_key_id: None,
request_type: None,
api_format: None,
api_family: None,
endpoint_kind: None,
endpoint_api_format: None,
provider_api_family: None,
provider_endpoint_kind: None,
has_format_conversion: Some(false),
is_stream: Some(false),
input_tokens: None,
output_tokens: None,
total_tokens: None,
cache_creation_input_tokens: None,
cache_creation_ephemeral_5m_input_tokens: None,
cache_creation_ephemeral_1h_input_tokens: None,
cache_read_input_tokens: None,
cache_creation_cost_usd: None,
cache_read_cost_usd: None,
output_price_per_1m: None,
total_cost_usd: None,
actual_total_cost_usd: None,
status_code: None,
error_message: None,
error_category: None,
response_time_ms: None,
first_byte_time_ms: None,
status: "pending".to_string(),
billing_status: "pending".to_string(),
request_headers: None,
request_body: None,
request_body_ref: None,
request_body_state: None,
provider_request_headers: None,
provider_request_body: None,
provider_request_body_ref: None,
provider_request_body_state: None,
response_headers: None,
response_body: None,
response_body_ref: None,
response_body_state: None,
client_response_headers: None,
client_response_body: None,
client_response_body_ref: None,
client_response_body_state: None,
candidate_id: None,
candidate_index: None,
key_name: None,
planner_kind: None,
route_family: None,
route_kind: None,
execution_path: None,
local_execution_runtime_miss_reason: None,
request_metadata: None,
finalized_at_unix_secs: None,
created_at_unix_ms: Some(1_700_000_000),
updated_at_unix_secs: 1_700_000_000,
}
}
#[tokio::test]
async fn finds_usage_by_request_id() {
let repository = InMemoryUsageReadRepository::seed(vec![
@@ -3719,4 +3954,203 @@ mod tests {
assert_eq!(usage.request_count, 2);
assert_eq!(usage.last_used_at_unix_secs, Some(2_500));
}
fn sample_provider_catalog_key(key_id: &str) -> StoredProviderCatalogKey {
StoredProviderCatalogKey::new(
key_id.to_string(),
"provider-1".to_string(),
"provider key".to_string(),
"api_key".to_string(),
None,
true,
)
.expect("provider key should build")
}
fn sample_provider_catalog_repository(
key_ids: &[&str],
) -> Arc<InMemoryProviderCatalogReadRepository> {
Arc::new(InMemoryProviderCatalogReadRepository::seed(
vec![StoredProviderCatalogProvider::new(
"provider-1".to_string(),
"OpenAI".to_string(),
None,
"openai".to_string(),
)
.expect("provider should build")],
Vec::new(),
key_ids
.iter()
.map(|key_id| sample_provider_catalog_key(key_id))
.collect(),
))
}
#[tokio::test]
async fn upsert_syncs_linked_provider_key_stats_without_double_counting_request_count() {
let provider_catalog = sample_provider_catalog_repository(&["provider-key-1"]);
let repository = InMemoryUsageReadRepository::default()
.with_provider_catalog_repository(Arc::clone(&provider_catalog));
repository
.upsert(UpsertUsageRecord {
provider_api_key_id: Some("provider-key-1".to_string()),
total_tokens: Some(100),
total_cost_usd: Some(0.5),
created_at_unix_ms: Some(1_711_100_000),
updated_at_unix_secs: 1_711_100_000,
..sample_upsert_usage_record("req-linked-1")
})
.await
.expect("pending upsert should succeed");
repository
.upsert(UpsertUsageRecord {
provider_api_key_id: Some("provider-key-1".to_string()),
status: "completed".to_string(),
billing_status: "settled".to_string(),
status_code: Some(200),
response_time_ms: Some(240),
total_tokens: Some(180),
total_cost_usd: Some(0.75),
created_at_unix_ms: Some(1_711_100_000),
updated_at_unix_secs: 1_711_100_010,
finalized_at_unix_secs: Some(1_711_100_011),
..sample_upsert_usage_record("req-linked-1")
})
.await
.expect("completed upsert should succeed");
let key = provider_catalog
.list_keys_by_ids(&["provider-key-1".to_string()])
.await
.expect("key list should succeed")
.into_iter()
.next()
.expect("provider key should exist");
assert_eq!(key.request_count, Some(1));
assert_eq!(key.success_count, Some(1));
assert_eq!(key.error_count, Some(0));
assert_eq!(key.total_tokens, 180);
assert_eq!(key.total_cost_usd, 0.75);
assert_eq!(key.total_response_time_ms, Some(240));
assert_eq!(key.last_used_at_unix_secs, Some(1_711_100_000));
}
#[tokio::test]
async fn upsert_moves_linked_provider_key_stats_when_key_assignment_changes() {
let provider_catalog =
sample_provider_catalog_repository(&["provider-key-a", "provider-key-b"]);
let repository = InMemoryUsageReadRepository::default()
.with_provider_catalog_repository(Arc::clone(&provider_catalog));
repository
.upsert(UpsertUsageRecord {
provider_api_key_id: Some("provider-key-a".to_string()),
status: "completed".to_string(),
billing_status: "settled".to_string(),
status_code: Some(200),
response_time_ms: Some(100),
total_tokens: Some(120),
total_cost_usd: Some(0.4),
created_at_unix_ms: Some(1_711_200_000),
updated_at_unix_secs: 1_711_200_000,
finalized_at_unix_secs: Some(1_711_200_001),
..sample_upsert_usage_record("req-move-1")
})
.await
.expect("first upsert should succeed");
repository
.upsert(UpsertUsageRecord {
provider_api_key_id: Some("provider-key-b".to_string()),
status: "completed".to_string(),
billing_status: "settled".to_string(),
status_code: Some(200),
response_time_ms: Some(150),
total_tokens: Some(140),
total_cost_usd: Some(0.6),
created_at_unix_ms: Some(1_711_200_000),
updated_at_unix_secs: 1_711_200_010,
finalized_at_unix_secs: Some(1_711_200_011),
..sample_upsert_usage_record("req-move-1")
})
.await
.expect("moved upsert should succeed");
let keys = provider_catalog
.list_keys_by_ids(&["provider-key-a".to_string(), "provider-key-b".to_string()])
.await
.expect("key list should succeed");
let key_a = keys
.iter()
.find(|key| key.id == "provider-key-a")
.expect("key a should exist");
let key_b = keys
.iter()
.find(|key| key.id == "provider-key-b")
.expect("key b should exist");
assert_eq!(key_a.request_count, Some(0));
assert_eq!(key_a.success_count, Some(0));
assert_eq!(key_a.total_tokens, 0);
assert_eq!(key_a.total_cost_usd, 0.0);
assert_eq!(key_a.total_response_time_ms, Some(0));
assert_eq!(key_a.last_used_at_unix_secs, None);
assert_eq!(key_b.request_count, Some(1));
assert_eq!(key_b.success_count, Some(1));
assert_eq!(key_b.total_tokens, 140);
assert_eq!(key_b.total_cost_usd, 0.6);
assert_eq!(key_b.total_response_time_ms, Some(150));
assert_eq!(key_b.last_used_at_unix_secs, Some(1_711_200_000));
}
#[tokio::test]
async fn rebuild_provider_key_usage_stats_resets_linked_catalog_to_current_usage() {
let provider_catalog = sample_provider_catalog_repository(&["provider-key-1"]);
let mut stale_key = provider_catalog
.list_keys_by_ids(&["provider-key-1".to_string()])
.await
.expect("key list should succeed")
.into_iter()
.next()
.expect("provider key should exist");
stale_key.request_count = Some(99);
stale_key.success_count = Some(88);
stale_key.error_count = Some(11);
stale_key.total_tokens = 9_999;
stale_key.total_cost_usd = 42.0;
stale_key.total_response_time_ms = Some(9_999);
stale_key.last_used_at_unix_secs = Some(9_999);
provider_catalog
.update_key(&stale_key)
.await
.expect("stale key should update");
let repository = InMemoryUsageReadRepository::seed(vec![
sample_usage("req-1", 1_711_300_000),
sample_usage("req-2", 1_711_300_250),
])
.with_provider_catalog_repository(Arc::clone(&provider_catalog));
let rebuilt = repository
.rebuild_provider_api_key_usage_stats()
.await
.expect("rebuild should succeed");
assert_eq!(rebuilt, 1);
let key = provider_catalog
.list_keys_by_ids(&["provider-key-1".to_string()])
.await
.expect("key list should succeed")
.into_iter()
.next()
.expect("provider key should exist");
assert_eq!(key.request_count, Some(2));
assert_eq!(key.success_count, Some(2));
assert_eq!(key.error_count, Some(0));
assert_eq!(key.total_tokens, 300);
assert_eq!(key.total_cost_usd, 0.24);
assert_eq!(key.total_response_time_ms, Some(840));
assert_eq!(key.last_used_at_unix_secs, Some(1_711_300_250));
}
}

View File

@@ -26,6 +26,85 @@ pub(crate) use aether_data_contracts::repository::usage::{
pub use memory::InMemoryUsageReadRepository;
pub use sql::SqlxUsageReadRepository;
#[derive(Debug, Clone, PartialEq, Default)]
pub(crate) struct ProviderApiKeyUsageContribution {
pub key_id: String,
pub request_count: i64,
pub success_count: i64,
pub error_count: i64,
pub total_tokens: i64,
pub total_cost_usd: f64,
pub total_response_time_ms: i64,
pub last_used_at_unix_secs: Option<u64>,
}
#[derive(Debug, Clone, PartialEq, Default)]
pub(crate) struct ProviderApiKeyUsageDelta {
pub request_count: i64,
pub success_count: i64,
pub error_count: i64,
pub total_tokens: i64,
pub total_cost_usd: f64,
pub total_response_time_ms: i64,
pub candidate_last_used_at_unix_secs: Option<u64>,
pub removed_last_used_at_unix_secs: Option<u64>,
}
impl ProviderApiKeyUsageDelta {
pub(crate) fn between(
before: &ProviderApiKeyUsageContribution,
after: &ProviderApiKeyUsageContribution,
) -> Self {
Self {
request_count: after.request_count - before.request_count,
success_count: after.success_count - before.success_count,
error_count: after.error_count - before.error_count,
total_tokens: after.total_tokens - before.total_tokens,
total_cost_usd: after.total_cost_usd - before.total_cost_usd,
total_response_time_ms: after.total_response_time_ms - before.total_response_time_ms,
candidate_last_used_at_unix_secs: after.last_used_at_unix_secs,
removed_last_used_at_unix_secs: None,
}
}
pub(crate) fn addition(after: &ProviderApiKeyUsageContribution) -> Self {
Self {
request_count: after.request_count,
success_count: after.success_count,
error_count: after.error_count,
total_tokens: after.total_tokens,
total_cost_usd: after.total_cost_usd,
total_response_time_ms: after.total_response_time_ms,
candidate_last_used_at_unix_secs: after.last_used_at_unix_secs,
removed_last_used_at_unix_secs: None,
}
}
pub(crate) fn removal(before: &ProviderApiKeyUsageContribution) -> Self {
Self {
request_count: -before.request_count,
success_count: -before.success_count,
error_count: -before.error_count,
total_tokens: -before.total_tokens,
total_cost_usd: -before.total_cost_usd,
total_response_time_ms: -before.total_response_time_ms,
candidate_last_used_at_unix_secs: None,
removed_last_used_at_unix_secs: before.last_used_at_unix_secs,
}
}
pub(crate) fn is_noop(&self) -> bool {
self.request_count == 0
&& self.success_count == 0
&& self.error_count == 0
&& self.total_tokens == 0
&& self.total_cost_usd == 0.0
&& self.total_response_time_ms == 0
&& self.candidate_last_used_at_unix_secs.is_none()
&& self.removed_last_used_at_unix_secs.is_none()
}
}
pub(crate) fn incoming_usage_can_recover_terminal_failure(
incoming_status: &str,
incoming_billing_status: &str,
@@ -57,11 +136,77 @@ pub(crate) fn strip_deprecated_usage_display_fields(
usage
}
pub(crate) fn provider_api_key_usage_is_success(
status: &str,
status_code: Option<u16>,
error_message: Option<&str>,
) -> bool {
matches!(
status,
"completed" | "success" | "ok" | "billed" | "settled"
) && status_code.is_none_or(|code| code < 400)
&& error_message.is_none_or(|value| value.trim().is_empty())
}
pub(crate) fn provider_api_key_usage_is_error(
status: &str,
status_code: Option<u16>,
error_message: Option<&str>,
) -> bool {
!matches!(status, "pending" | "streaming")
&& !provider_api_key_usage_is_success(status, status_code, error_message)
}
pub(crate) fn provider_api_key_usage_contribution(
usage: &StoredRequestUsageAudit,
) -> Option<ProviderApiKeyUsageContribution> {
let key_id = usage
.provider_api_key_id
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())?
.to_string();
let is_success = provider_api_key_usage_is_success(
usage.status.as_str(),
usage.status_code,
usage.error_message.as_deref(),
);
let is_error = provider_api_key_usage_is_error(
usage.status.as_str(),
usage.status_code,
usage.error_message.as_deref(),
);
Some(ProviderApiKeyUsageContribution {
key_id,
request_count: 1,
success_count: i64::from(is_success),
error_count: i64::from(is_error),
total_tokens: i64::try_from(usage.total_tokens).unwrap_or(i64::MAX),
total_cost_usd: if usage.total_cost_usd.is_finite() {
usage.total_cost_usd.max(0.0)
} else {
0.0
},
total_response_time_ms: if is_success {
usage
.response_time_ms
.and_then(|value| i64::try_from(value).ok())
.unwrap_or_default()
} else {
0
},
last_used_at_unix_secs: Some(usage.created_at_unix_ms),
})
}
#[cfg(test)]
mod tests {
use super::{
incoming_usage_can_recover_terminal_failure, strip_deprecated_usage_display_fields,
usage_can_recover_terminal_failure, UpsertUsageRecord,
incoming_usage_can_recover_terminal_failure, provider_api_key_usage_contribution,
provider_api_key_usage_is_error, provider_api_key_usage_is_success,
strip_deprecated_usage_display_fields, usage_can_recover_terminal_failure,
StoredRequestUsageAudit, UpsertUsageRecord,
};
#[test]
@@ -187,4 +332,98 @@ mod tests {
"failed", "void", "failed", "void"
));
}
#[test]
fn provider_key_usage_success_requires_clean_terminal_success() {
assert!(provider_api_key_usage_is_success(
"completed",
Some(200),
None
));
assert!(!provider_api_key_usage_is_success(
"completed",
Some(500),
None
));
assert!(!provider_api_key_usage_is_success(
"completed",
Some(200),
Some("boom")
));
assert!(!provider_api_key_usage_is_success(
"streaming",
Some(200),
None
));
}
#[test]
fn provider_key_usage_error_ignores_pending_states() {
assert!(provider_api_key_usage_is_error(
"failed",
Some(500),
Some("boom")
));
assert!(provider_api_key_usage_is_error(
"completed",
Some(200),
Some("boom")
));
assert!(!provider_api_key_usage_is_error("pending", None, None));
assert!(!provider_api_key_usage_is_error("streaming", None, None));
}
#[test]
fn provider_key_usage_contribution_tracks_success_response_time() {
let usage = StoredRequestUsageAudit::new(
"usage-1".to_string(),
"request-1".to_string(),
None,
None,
None,
None,
"OpenAI".to_string(),
"gpt-5".to_string(),
None,
Some("provider-1".to_string()),
None,
Some("provider-key-1".to_string()),
None,
None,
None,
None,
None,
None,
None,
false,
false,
12,
8,
20,
0.25,
0.25,
Some(200),
None,
None,
Some(120),
None,
"completed".to_string(),
"settled".to_string(),
123,
124,
Some(125),
)
.expect("usage should build");
let contribution =
provider_api_key_usage_contribution(&usage).expect("contribution should exist");
assert_eq!(contribution.key_id, "provider-key-1");
assert_eq!(contribution.request_count, 1);
assert_eq!(contribution.success_count, 1);
assert_eq!(contribution.error_count, 0);
assert_eq!(contribution.total_tokens, 20);
assert_eq!(contribution.total_cost_usd, 0.25);
assert_eq!(contribution.total_response_time_ms, 120);
assert_eq!(contribution.last_used_at_unix_secs, Some(123));
}
}

View File

@@ -26,7 +26,8 @@ use std::io::{Read, Write};
use uuid::Uuid;
use super::{
incoming_usage_can_recover_terminal_failure, strip_deprecated_usage_display_fields,
incoming_usage_can_recover_terminal_failure, provider_api_key_usage_contribution,
strip_deprecated_usage_display_fields, ProviderApiKeyUsageDelta,
StoredProviderApiKeyUsageSummary, StoredProviderUsageSummary, StoredRequestUsageAudit,
StoredUsageDailySummary, UpsertUsageRecord, UsageAuditListQuery, UsageDailyHeatmapQuery,
UsageReadRepository, UsageWriteRepository,
@@ -78,6 +79,9 @@ SET
WHERE request_id = $1
AND billing_status = 'void'
"#;
const LOCK_USAGE_REQUEST_ID_SQL: &str = r#"
SELECT pg_advisory_xact_lock(hashtext($1)::BIGINT)
"#;
const UPSERT_USAGE_HTTP_AUDIT_SQL: &str = r#"
INSERT INTO usage_http_audits (
request_id,
@@ -563,6 +567,112 @@ GROUP BY provider_api_key_id
ORDER BY provider_api_key_id ASC
"#;
const APPLY_PROVIDER_API_KEY_USAGE_DELTA_SQL: &str = r#"
UPDATE provider_api_keys
SET
request_count = GREATEST(COALESCE(request_count, 0) + $2, 0),
success_count = GREATEST(COALESCE(success_count, 0) + $3, 0),
error_count = GREATEST(COALESCE(error_count, 0) + $4, 0),
total_tokens = GREATEST(total_tokens + $5, 0),
total_cost_usd = CAST(
GREATEST(CAST(total_cost_usd AS DOUBLE PRECISION) + $6, 0) AS NUMERIC(20,8)
),
total_response_time_ms = GREATEST(COALESCE(total_response_time_ms, 0) + $7, 0),
last_used_at = CASE
WHEN $8::double precision IS NOT NULL THEN CASE
WHEN last_used_at IS NULL THEN TO_TIMESTAMP($8::double precision)
ELSE GREATEST(last_used_at, TO_TIMESTAMP($8::double precision))
END
WHEN $9::double precision IS NOT NULL
AND last_used_at IS NOT NULL
AND EXTRACT(EPOCH FROM last_used_at)::BIGINT = $9::BIGINT
THEN (
SELECT MAX(created_at)
FROM "usage"
WHERE provider_api_key_id = $1
)
ELSE last_used_at
END
WHERE id = $1
"#;
const RESET_PROVIDER_API_KEY_USAGE_STATS_SQL: &str = r#"
UPDATE provider_api_keys
SET
request_count = 0,
success_count = 0,
error_count = 0,
total_tokens = 0,
total_cost_usd = 0,
total_response_time_ms = 0,
last_used_at = NULL
"#;
const REBUILD_PROVIDER_API_KEY_USAGE_STATS_SQL: &str = r#"
WITH aggregated AS (
SELECT
provider_api_key_id,
COUNT(*)::INTEGER AS request_count,
COALESCE(SUM(
CASE
WHEN status IN ('completed', 'success', 'ok', 'billed', 'settled')
AND (status_code IS NULL OR status_code < 400)
AND NULLIF(BTRIM(error_message), '') IS NULL
THEN 1
ELSE 0
END
), 0)::INTEGER AS success_count,
COALESCE(SUM(
CASE
WHEN status NOT IN ('pending', 'streaming')
AND NOT (
status IN ('completed', 'success', 'ok', 'billed', 'settled')
AND (status_code IS NULL OR status_code < 400)
AND NULLIF(BTRIM(error_message), '') IS NULL
)
THEN 1
ELSE 0
END
), 0)::INTEGER AS error_count,
COALESCE(SUM(
GREATEST(
COALESCE(
total_tokens,
COALESCE(input_tokens, 0) + COALESCE(output_tokens, 0)
),
0
)::BIGINT
), 0)::BIGINT AS total_tokens,
COALESCE(SUM(COALESCE(total_cost_usd, 0)), 0)::NUMERIC(20,8) AS total_cost_usd,
COALESCE(SUM(
CASE
WHEN status IN ('completed', 'success', 'ok', 'billed', 'settled')
AND (status_code IS NULL OR status_code < 400)
AND NULLIF(BTRIM(error_message), '') IS NULL
AND response_time_ms IS NOT NULL
THEN GREATEST(response_time_ms, 0)
ELSE 0
END
), 0)::INTEGER AS total_response_time_ms,
MAX(created_at) AS last_used_at
FROM "usage"
WHERE provider_api_key_id IS NOT NULL
AND BTRIM(provider_api_key_id) <> ''
GROUP BY provider_api_key_id
)
UPDATE provider_api_keys
SET
request_count = aggregated.request_count,
success_count = aggregated.success_count,
error_count = aggregated.error_count,
total_tokens = aggregated.total_tokens,
total_cost_usd = aggregated.total_cost_usd,
total_response_time_ms = aggregated.total_response_time_ms,
last_used_at = aggregated.last_used_at
FROM aggregated
WHERE provider_api_keys.id = aggregated.provider_api_key_id
"#;
const LIST_USAGE_AUDITS_PREFIX: &str = r#"
SELECT
"usage".id,
@@ -3971,6 +4081,8 @@ WHERE "usage".created_at >= TO_TIMESTAMP($1::double precision)"#,
self.tx_runner
.run_read_write(|tx| {
Box::pin(async move {
lock_usage_request_id_in_tx(tx, &usage.request_id).await?;
if incoming_usage_can_recover_terminal_failure(
usage.status.as_str(),
usage.billing_status.as_str(),
@@ -3987,6 +4099,9 @@ WHERE "usage".created_at >= TO_TIMESTAMP($1::double precision)"#,
.map_postgres_err()?;
}
let previous_usage =
find_usage_by_request_id_in_tx(tx, &usage.request_id).await?;
let request_headers_json = json_bind_text(usage.request_headers.as_ref())?;
let request_body_storage =
prepare_usage_body_storage(usage.request_body.as_ref())?;
@@ -4292,11 +4407,66 @@ WHERE "usage".created_at >= TO_TIMESTAMP($1::double precision)"#,
routing_snapshot.local_execution_runtime_miss_reason.clone();
stored.output_price_per_1m = settlement_pricing_snapshot.output_price_per_1m;
stored.request_metadata = request_metadata_value;
let before_contribution = previous_usage
.as_ref()
.and_then(provider_api_key_usage_contribution);
let after_contribution = provider_api_key_usage_contribution(&stored);
match (before_contribution.as_ref(), after_contribution.as_ref()) {
(Some(before), Some(after)) if before.key_id == after.key_id => {
let delta = ProviderApiKeyUsageDelta::between(before, after);
apply_provider_api_key_usage_delta_in_tx(
tx,
before.key_id.as_str(),
&delta,
)
.await?;
}
_ => {
if let Some(before) = before_contribution.as_ref() {
let delta = ProviderApiKeyUsageDelta::removal(before);
apply_provider_api_key_usage_delta_in_tx(
tx,
before.key_id.as_str(),
&delta,
)
.await?;
}
if let Some(after) = after_contribution.as_ref() {
let delta = ProviderApiKeyUsageDelta::addition(after);
apply_provider_api_key_usage_delta_in_tx(
tx,
after.key_id.as_str(),
&delta,
)
.await?;
}
}
}
Ok(stored)
}) as BoxFuture<'_, Result<StoredRequestUsageAudit, DataLayerError>>
})
.await
}
pub async fn rebuild_provider_api_key_usage_stats(&self) -> Result<u64, DataLayerError> {
self.tx_runner
.run_read_write(|tx| {
Box::pin(async move {
sqlx::query(RESET_PROVIDER_API_KEY_USAGE_STATS_SQL)
.execute(&mut **tx)
.await
.map_postgres_err()?;
let rows_affected = sqlx::query(REBUILD_PROVIDER_API_KEY_USAGE_STATS_SQL)
.execute(&mut **tx)
.await
.map_postgres_err()?
.rows_affected();
Ok(rows_affected)
}) as BoxFuture<'_, Result<u64, DataLayerError>>
})
.await
}
}
#[async_trait]
@@ -4517,6 +4687,97 @@ impl UsageWriteRepository for SqlxUsageReadRepository {
) -> Result<StoredRequestUsageAudit, DataLayerError> {
Self::upsert(self, usage).await
}
async fn rebuild_provider_api_key_usage_stats(&self) -> Result<u64, DataLayerError> {
Self::rebuild_provider_api_key_usage_stats(self).await
}
}
async fn find_usage_by_request_id_in_tx(
tx: &mut sqlx::Transaction<'_, Postgres>,
request_id: &str,
) -> Result<Option<StoredRequestUsageAudit>, DataLayerError> {
sqlx::query(FIND_BY_REQUEST_ID_SQL)
.bind(request_id)
.fetch_optional(&mut **tx)
.await
.map_postgres_err()?
.map(|row| map_usage_row(&row, false))
.transpose()
}
async fn lock_usage_request_id_in_tx(
tx: &mut sqlx::Transaction<'_, Postgres>,
request_id: &str,
) -> Result<(), DataLayerError> {
sqlx::query(LOCK_USAGE_REQUEST_ID_SQL)
.bind(request_id)
.execute(&mut **tx)
.await
.map_postgres_err()?;
Ok(())
}
async fn apply_provider_api_key_usage_delta_in_tx(
tx: &mut sqlx::Transaction<'_, Postgres>,
key_id: &str,
delta: &ProviderApiKeyUsageDelta,
) -> Result<(), DataLayerError> {
if key_id.trim().is_empty() {
return Ok(());
}
if delta.is_noop() {
return Ok(());
}
let total_cost_usd_delta = if delta.total_cost_usd.is_finite() {
delta.total_cost_usd
} else {
0.0
};
sqlx::query(APPLY_PROVIDER_API_KEY_USAGE_DELTA_SQL)
.bind(key_id)
.bind(i32::try_from(delta.request_count).map_err(|_| {
DataLayerError::UnexpectedValue(format!(
"provider_api_keys.request_count delta exceeds i32: {}",
delta.request_count
))
})?)
.bind(i32::try_from(delta.success_count).map_err(|_| {
DataLayerError::UnexpectedValue(format!(
"provider_api_keys.success_count delta exceeds i32: {}",
delta.success_count
))
})?)
.bind(i32::try_from(delta.error_count).map_err(|_| {
DataLayerError::UnexpectedValue(format!(
"provider_api_keys.error_count delta exceeds i32: {}",
delta.error_count
))
})?)
.bind(delta.total_tokens)
.bind(total_cost_usd_delta)
.bind(i32::try_from(delta.total_response_time_ms).map_err(|_| {
DataLayerError::UnexpectedValue(format!(
"provider_api_keys.total_response_time_ms delta exceeds i32: {}",
delta.total_response_time_ms
))
})?)
.bind(
delta
.candidate_last_used_at_unix_secs
.map(|value| value as f64),
)
.bind(
delta
.removed_last_used_at_unix_secs
.map(|value| value as f64),
)
.execute(&mut **tx)
.await
.map_postgres_err()?;
Ok(())
}
// Build the usage read model from the split storage layout.
@@ -5848,6 +6109,26 @@ mod tests {
assert!(super::SUMMARIZE_USAGE_BY_PROVIDER_API_KEY_IDS_SQL.contains("ANY($1::TEXT[])"));
}
#[test]
fn usage_sql_serializes_request_id_upserts_before_reading_previous_usage() {
assert!(super::LOCK_USAGE_REQUEST_ID_SQL.contains("pg_advisory_xact_lock"));
assert!(super::LOCK_USAGE_REQUEST_ID_SQL.contains("hashtext($1)::BIGINT"));
assert!(include_str!("sql.rs")
.contains("lock_usage_request_id_in_tx(tx, &usage.request_id).await?;"));
}
#[test]
fn usage_sql_rebuild_matches_online_provider_key_usage_semantics() {
assert!(super::REBUILD_PROVIDER_API_KEY_USAGE_STATS_SQL
.contains("NULLIF(BTRIM(error_message), '') IS NULL"));
assert!(super::REBUILD_PROVIDER_API_KEY_USAGE_STATS_SQL.contains("COALESCE("));
assert!(super::REBUILD_PROVIDER_API_KEY_USAGE_STATS_SQL.contains("total_tokens,"));
assert!(super::REBUILD_PROVIDER_API_KEY_USAGE_STATS_SQL
.contains("COALESCE(input_tokens, 0) + COALESCE(output_tokens, 0)"));
assert!(super::REBUILD_PROVIDER_API_KEY_USAGE_STATS_SQL
.contains("AND BTRIM(provider_api_key_id) <> ''"));
}
#[test]
fn usage_sql_supports_recent_usage_audits_query() {
assert!(super::LIST_RECENT_USAGE_AUDITS_PREFIX.contains("FROM \"usage\""));