feat(gateway/data): 新增 usage 关键词搜索、缓存命中摘要、结算成本摘要及 dashboard 聚合查询能力

- 新增 UsageAuditKeywordSearchQuery,支持多关键词、用户名、API Key 交叉过滤
- 新增 UsageCacheHitSummaryQuery/StoredUsageCacheHitSummary,统计请求缓存命中率
- 新增 UsageSettledCostSummaryQuery/StoredUsageSettledCostSummary,汇总结算成本
- 新增 UsageDashboardSummaryQuery、UsageBreakdownSummaryQuery 等 dashboard 聚合类型
- SQL 层实现对应查询方法,含 list_by_ids、list_usage_audits_by_keyword_search、count_usage_audits_by_keyword_search
- 将上述能力通过 GatewayDataState / AdminAppState 暴露给 handler 层
- user_me_usage 及 dashboard_filters 切换为新查询接口,移除旧的内存过滤逻辑
- 同步更新 memory 层及测试
This commit is contained in:
fawney19
2026-04-17 00:58:46 +08:00
parent 6e5af5ef70
commit 1e0bc61526
48 changed files with 5241 additions and 1162 deletions

View File

@@ -1,7 +1,11 @@
use aether_data::repository::auth::StoredAuthApiKeySnapshot;
use aether_data_contracts::repository::{
provider_catalog::StoredProviderCatalogProvider,
usage::{StoredRequestUsageAudit, StoredUsageLeaderboardSummary, StoredUsageTimeSeriesBucket},
usage::{
StoredRequestUsageAudit, StoredUsageCostSavingsSummary, StoredUsageErrorDistributionRow,
StoredUsageLeaderboardSummary, StoredUsagePerformancePercentilesRow,
StoredUsageTimeSeriesBucket,
},
};
use axum::{
body::Body,
@@ -927,6 +931,62 @@ pub fn build_admin_stats_error_distribution_response(
.into_response()
}
pub fn build_admin_stats_error_distribution_response_from_summaries(
rows: &[StoredUsageErrorDistributionRow],
) -> Response<Body> {
let mut distribution: std::collections::BTreeMap<String, u64> =
std::collections::BTreeMap::new();
let mut trend: std::collections::BTreeMap<String, std::collections::BTreeMap<String, u64>> =
std::collections::BTreeMap::new();
for row in rows {
*distribution.entry(row.error_category.clone()).or_default() += row.count;
*trend
.entry(row.date.clone())
.or_default()
.entry(row.error_category.clone())
.or_default() += row.count;
}
let mut distribution_items: Vec<_> = distribution
.into_iter()
.map(|(category, count)| json!({ "category": category, "count": count }))
.collect();
distribution_items.sort_by(|left, right| {
let left_count = left
.get("count")
.and_then(serde_json::Value::as_u64)
.unwrap_or(0);
let right_count = right
.get("count")
.and_then(serde_json::Value::as_u64)
.unwrap_or(0);
right_count.cmp(&left_count).then_with(|| {
left.get("category")
.and_then(serde_json::Value::as_str)
.cmp(&right.get("category").and_then(serde_json::Value::as_str))
})
});
let trend_items: Vec<_> = trend
.into_iter()
.map(|(date, categories)| {
let total: u64 = categories.values().copied().sum();
json!({
"date": date,
"total": total,
"categories": categories,
})
})
.collect();
Json(json!({
"distribution": distribution_items,
"trend": trend_items,
}))
.into_response()
}
pub fn build_admin_stats_performance_percentiles_response(
time_range: &AdminStatsTimeRange,
usage: &[StoredRequestUsageAudit],
@@ -974,6 +1034,33 @@ pub fn build_admin_stats_performance_percentiles_response(
Json(serde_json::Value::Array(payload)).into_response()
}
pub fn build_admin_stats_performance_percentiles_response_from_summaries(
time_range: &AdminStatsTimeRange,
rows: &[StoredUsagePerformancePercentilesRow],
) -> Response<Body> {
let by_day: std::collections::BTreeMap<String, &StoredUsagePerformancePercentilesRow> =
rows.iter().map(|row| (row.date.clone(), row)).collect();
let payload: Vec<_> = time_range
.local_date_strings()
.into_iter()
.map(|date| {
let row = by_day.get(&date).copied();
json!({
"date": date,
"p50_response_time_ms": row.and_then(|value| value.p50_response_time_ms),
"p90_response_time_ms": row.and_then(|value| value.p90_response_time_ms),
"p99_response_time_ms": row.and_then(|value| value.p99_response_time_ms),
"p50_first_byte_time_ms": row.and_then(|value| value.p50_first_byte_time_ms),
"p90_first_byte_time_ms": row.and_then(|value| value.p90_first_byte_time_ms),
"p99_first_byte_time_ms": row.and_then(|value| value.p99_first_byte_time_ms),
})
})
.collect();
Json(serde_json::Value::Array(payload)).into_response()
}
pub fn build_admin_stats_time_series_response(
time_range: &AdminStatsTimeRange,
granularity: AdminStatsGranularity,
@@ -1199,6 +1286,53 @@ pub fn build_admin_stats_cost_forecast_response(
.into_response()
}
pub fn build_admin_stats_cost_forecast_response_from_summaries(
time_range: &AdminStatsTimeRange,
forecast_days: u32,
buckets: &[StoredUsageTimeSeriesBucket],
) -> Response<Body> {
let history: Vec<AdminStatsForecastPoint> =
build_daily_time_series_buckets_from_summaries(time_range, buckets)
.into_iter()
.map(|(date, bucket)| AdminStatsForecastPoint {
date,
total_cost: bucket.total_cost,
})
.collect();
let values: Vec<f64> = history.iter().map(|item| item.total_cost).collect();
let (slope, intercept) = linear_regression(&values);
let last_date = history
.last()
.map(|item| item.date)
.unwrap_or(time_range.end_date);
let forecast: Vec<_> = (0..forecast_days)
.map(|index| {
let idx = values.len() + index as usize;
let predicted = (slope * idx as f64 + intercept).max(0.0);
json!({
"date": last_date
.checked_add_signed(chrono::Duration::days(i64::from(index + 1)))
.unwrap_or(last_date)
.to_string(),
"total_cost": round_to(predicted, 4),
})
})
.collect();
Json(json!({
"history": history.into_iter().map(|item| json!({
"date": item.date.to_string(),
"total_cost": round_to(item.total_cost, 6),
})).collect::<Vec<_>>(),
"forecast": forecast,
"slope": round_to(slope, 6),
"intercept": round_to(intercept, 6),
"start_date": time_range.start_date.to_string(),
"end_date": time_range.end_date.to_string(),
}))
.into_response()
}
pub fn build_admin_stats_cost_savings_response(
usage: &[StoredRequestUsageAudit],
) -> Response<Body> {
@@ -1228,6 +1362,27 @@ pub fn build_admin_stats_cost_savings_response(
.into_response()
}
pub fn build_admin_stats_cost_savings_response_from_summary(
summary: &StoredUsageCostSavingsSummary,
) -> Response<Body> {
let estimated_full_cost =
if summary.estimated_full_cost_usd <= 0.0 && summary.cache_read_cost_usd > 0.0 {
summary.cache_read_cost_usd * 10.0
} else {
summary.estimated_full_cost_usd
};
let cache_savings = estimated_full_cost - summary.cache_read_cost_usd;
Json(json!({
"cache_read_tokens": summary.cache_read_tokens,
"cache_read_cost": round_to(summary.cache_read_cost_usd, 6),
"cache_creation_cost": round_to(summary.cache_creation_cost_usd, 6),
"estimated_full_cost": round_to(estimated_full_cost, 6),
"cache_savings": round_to(cache_savings, 6),
}))
.into_response()
}
fn unix_secs_to_rfc3339(unix_secs: u64) -> Option<String> {
let timestamp = chrono::DateTime::<Utc>::from_timestamp(i64::try_from(unix_secs).ok()?, 0)?;
Some(timestamp.to_rfc3339())

View File

@@ -3,10 +3,21 @@ mod types;
pub use types::{
parse_usage_body_ref, usage_body_ref, StoredProviderApiKeyUsageSummary,
StoredProviderUsageSummary, StoredProviderUsageWindow, StoredRequestUsageAudit,
StoredUsageAuditAggregation, StoredUsageAuditSummary, StoredUsageDailySummary,
StoredUsageLeaderboardSummary, StoredUsageTimeSeriesBucket, UpsertUsageRecord,
UsageAuditAggregationGroupBy, UsageAuditAggregationQuery, UsageAuditListQuery,
UsageAuditSummaryQuery, UsageBodyField, UsageDailyHeatmapQuery, UsageLeaderboardGroupBy,
UsageLeaderboardQuery, UsageReadRepository, UsageRepository, UsageTimeSeriesGranularity,
UsageTimeSeriesQuery, UsageWriteRepository,
StoredUsageAuditAggregation, StoredUsageAuditSummary, StoredUsageBreakdownSummaryRow,
StoredUsageCacheAffinityHitSummary, StoredUsageCacheAffinityIntervalRow,
StoredUsageCacheHitSummary, StoredUsageCostSavingsSummary, StoredUsageDailySummary,
StoredUsageDashboardDailyBreakdownRow, StoredUsageDashboardProviderCount,
StoredUsageDashboardSummary, StoredUsageErrorDistributionRow, StoredUsageLeaderboardSummary,
StoredUsagePerformancePercentilesRow, StoredUsageSettledCostSummary,
StoredUsageTimeSeriesBucket, UpsertUsageRecord, UsageAuditAggregationGroupBy,
UsageAuditAggregationQuery, UsageAuditKeywordSearchQuery, UsageAuditListQuery,
UsageAuditSummaryQuery, UsageBodyField, UsageBreakdownGroupBy, UsageBreakdownSummaryQuery,
UsageCacheAffinityHitSummaryQuery, UsageCacheAffinityIntervalGroupBy,
UsageCacheAffinityIntervalQuery, UsageCacheHitSummaryQuery, UsageCostSavingsSummaryQuery,
UsageDailyHeatmapQuery, UsageDashboardDailyBreakdownQuery, UsageDashboardProviderCountsQuery,
UsageDashboardSummaryQuery, UsageErrorDistributionQuery, UsageLeaderboardGroupBy,
UsageLeaderboardQuery, UsageMonitoringErrorCountQuery, UsageMonitoringErrorListQuery,
UsagePerformancePercentilesQuery, UsageReadRepository, UsageRepository,
UsageSettledCostSummaryQuery, UsageTimeSeriesGranularity, UsageTimeSeriesQuery,
UsageWriteRepository,
};

View File

@@ -489,6 +489,29 @@ pub struct UsageAuditListQuery {
pub newest_first: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
pub struct UsageAuditKeywordSearchQuery {
pub created_from_unix_secs: Option<u64>,
pub created_until_unix_secs: Option<u64>,
pub user_id: Option<String>,
pub provider_name: Option<String>,
pub model: Option<String>,
pub api_format: Option<String>,
pub statuses: Option<Vec<String>>,
pub is_stream: Option<bool>,
pub error_only: bool,
pub keywords: Vec<String>,
pub matched_user_ids_by_keyword: Vec<Vec<String>>,
pub auth_user_reader_available: bool,
pub matched_api_key_ids_by_keyword: Vec<Vec<String>>,
pub auth_api_key_reader_available: bool,
pub username_keyword: Option<String>,
pub matched_user_ids_for_username: Vec<String>,
pub limit: Option<usize>,
pub offset: Option<usize>,
pub newest_first: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum UsageAuditAggregationGroupBy {
@@ -553,6 +576,244 @@ pub struct StoredUsageAuditSummary {
pub error_requests: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
pub struct UsageCacheHitSummaryQuery {
pub created_from_unix_secs: u64,
pub created_until_unix_secs: u64,
pub user_id: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
pub struct StoredUsageCacheHitSummary {
pub total_requests: u64,
pub cache_hit_requests: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
pub struct UsageSettledCostSummaryQuery {
pub created_from_unix_secs: u64,
pub created_until_unix_secs: u64,
pub user_id: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
pub struct StoredUsageSettledCostSummary {
pub total_cost_usd: f64,
pub total_requests: u64,
pub input_tokens: u64,
pub output_tokens: u64,
pub cache_creation_tokens: u64,
pub cache_read_tokens: u64,
pub first_finalized_at_unix_secs: Option<u64>,
pub last_finalized_at_unix_secs: Option<u64>,
}
#[derive(Debug, Clone, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
pub struct UsageCacheAffinityHitSummaryQuery {
pub created_from_unix_secs: u64,
pub created_until_unix_secs: u64,
pub user_id: Option<String>,
pub api_key_id: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
pub struct StoredUsageCacheAffinityHitSummary {
pub total_requests: u64,
pub requests_with_cache_hit: u64,
pub input_tokens: u64,
pub cache_read_tokens: u64,
pub cache_creation_tokens: u64,
pub total_input_context: u64,
pub cache_read_cost_usd: f64,
pub cache_creation_cost_usd: f64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum UsageCacheAffinityIntervalGroupBy {
#[default]
User,
ApiKey,
}
#[derive(Debug, Clone, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
pub struct UsageCacheAffinityIntervalQuery {
pub created_from_unix_secs: u64,
pub created_until_unix_secs: u64,
pub group_by: UsageCacheAffinityIntervalGroupBy,
pub user_id: Option<String>,
pub api_key_id: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
pub struct StoredUsageCacheAffinityIntervalRow {
pub group_id: String,
pub username: Option<String>,
pub model: String,
pub created_at_unix_secs: u64,
pub interval_minutes: f64,
}
#[derive(Debug, Clone, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
pub struct UsageDashboardSummaryQuery {
pub created_from_unix_secs: u64,
pub created_until_unix_secs: u64,
pub user_id: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
pub struct StoredUsageDashboardSummary {
pub total_requests: u64,
pub input_tokens: u64,
pub effective_input_tokens: u64,
pub output_tokens: u64,
pub total_tokens: u64,
pub cache_creation_tokens: u64,
pub cache_read_tokens: u64,
pub total_input_context: u64,
pub cache_creation_cost_usd: f64,
pub cache_read_cost_usd: f64,
pub total_cost_usd: f64,
pub actual_total_cost_usd: f64,
pub error_requests: u64,
pub response_time_sum_ms: f64,
pub response_time_samples: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct UsageDashboardDailyBreakdownQuery {
pub created_from_unix_secs: u64,
pub created_until_unix_secs: u64,
pub tz_offset_minutes: i32,
pub user_id: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
pub struct StoredUsageDashboardDailyBreakdownRow {
pub date: String,
pub model: String,
pub provider: String,
pub requests: u64,
pub total_tokens: u64,
pub total_cost_usd: f64,
pub response_time_sum_ms: f64,
pub response_time_samples: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct UsageDashboardProviderCountsQuery {
pub created_from_unix_secs: u64,
pub created_until_unix_secs: u64,
pub user_id: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
pub struct StoredUsageDashboardProviderCount {
pub provider_name: String,
pub request_count: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum UsageBreakdownGroupBy {
#[default]
Model,
Provider,
ApiFormat,
}
#[derive(Debug, Clone, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
pub struct UsageBreakdownSummaryQuery {
pub created_from_unix_secs: u64,
pub created_until_unix_secs: u64,
pub user_id: Option<String>,
pub group_by: UsageBreakdownGroupBy,
}
#[derive(Debug, Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
pub struct StoredUsageBreakdownSummaryRow {
pub group_key: String,
pub request_count: u64,
pub input_tokens: u64,
pub total_tokens: u64,
pub output_tokens: u64,
pub effective_input_tokens: u64,
pub total_input_context: u64,
pub cache_creation_tokens: u64,
pub cache_creation_ephemeral_5m_tokens: u64,
pub cache_creation_ephemeral_1h_tokens: u64,
pub cache_read_tokens: u64,
pub total_cost_usd: f64,
pub actual_total_cost_usd: f64,
pub success_count: u64,
pub response_time_sum_ms: f64,
pub response_time_samples: u64,
pub overall_response_time_sum_ms: f64,
pub overall_response_time_samples: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
pub struct UsageMonitoringErrorCountQuery {
pub created_from_unix_secs: u64,
pub created_until_unix_secs: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
pub struct UsageMonitoringErrorListQuery {
pub created_from_unix_secs: u64,
pub created_until_unix_secs: u64,
pub limit: Option<usize>,
}
#[derive(Debug, Clone, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
pub struct UsageErrorDistributionQuery {
pub created_from_unix_secs: u64,
pub created_until_unix_secs: u64,
pub tz_offset_minutes: i32,
}
#[derive(Debug, Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
pub struct StoredUsageErrorDistributionRow {
pub date: String,
pub error_category: String,
pub count: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
pub struct UsagePerformancePercentilesQuery {
pub created_from_unix_secs: u64,
pub created_until_unix_secs: u64,
pub tz_offset_minutes: i32,
}
#[derive(Debug, Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
pub struct StoredUsagePerformancePercentilesRow {
pub date: String,
pub p50_response_time_ms: Option<u64>,
pub p90_response_time_ms: Option<u64>,
pub p99_response_time_ms: Option<u64>,
pub p50_first_byte_time_ms: Option<u64>,
pub p90_first_byte_time_ms: Option<u64>,
pub p99_first_byte_time_ms: Option<u64>,
}
#[derive(Debug, Clone, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
pub struct UsageCostSavingsSummaryQuery {
pub created_from_unix_secs: u64,
pub created_until_unix_secs: u64,
pub user_id: Option<String>,
pub provider_name: Option<String>,
pub model: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
pub struct StoredUsageCostSavingsSummary {
pub cache_read_tokens: u64,
pub cache_read_cost_usd: f64,
pub cache_creation_cost_usd: f64,
pub estimated_full_cost_usd: f64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum UsageTimeSeriesGranularity {
@@ -694,6 +955,11 @@ pub trait UsageReadRepository: Send + Sync {
id: &str,
) -> Result<Option<StoredRequestUsageAudit>, crate::DataLayerError>;
async fn list_by_ids(
&self,
ids: &[String],
) -> Result<Vec<StoredRequestUsageAudit>, crate::DataLayerError>;
async fn find_by_request_id(
&self,
request_id: &str,
@@ -714,6 +980,16 @@ pub trait UsageReadRepository: Send + Sync {
query: &UsageAuditListQuery,
) -> Result<u64, crate::DataLayerError>;
async fn list_usage_audits_by_keyword_search(
&self,
query: &UsageAuditKeywordSearchQuery,
) -> Result<Vec<StoredRequestUsageAudit>, crate::DataLayerError>;
async fn count_usage_audits_by_keyword_search(
&self,
query: &UsageAuditKeywordSearchQuery,
) -> Result<u64, crate::DataLayerError>;
async fn aggregate_usage_audits(
&self,
query: &UsageAuditAggregationQuery,
@@ -724,6 +1000,71 @@ pub trait UsageReadRepository: Send + Sync {
query: &UsageAuditSummaryQuery,
) -> Result<StoredUsageAuditSummary, crate::DataLayerError>;
async fn summarize_usage_cache_hit_summary(
&self,
query: &UsageCacheHitSummaryQuery,
) -> Result<StoredUsageCacheHitSummary, crate::DataLayerError>;
async fn summarize_usage_settled_cost(
&self,
query: &UsageSettledCostSummaryQuery,
) -> Result<StoredUsageSettledCostSummary, crate::DataLayerError>;
async fn summarize_usage_cache_affinity_hit_summary(
&self,
query: &UsageCacheAffinityHitSummaryQuery,
) -> Result<StoredUsageCacheAffinityHitSummary, crate::DataLayerError>;
async fn list_usage_cache_affinity_intervals(
&self,
query: &UsageCacheAffinityIntervalQuery,
) -> Result<Vec<StoredUsageCacheAffinityIntervalRow>, crate::DataLayerError>;
async fn summarize_dashboard_usage(
&self,
query: &UsageDashboardSummaryQuery,
) -> Result<StoredUsageDashboardSummary, crate::DataLayerError>;
async fn list_dashboard_daily_breakdown(
&self,
query: &UsageDashboardDailyBreakdownQuery,
) -> Result<Vec<StoredUsageDashboardDailyBreakdownRow>, crate::DataLayerError>;
async fn summarize_dashboard_provider_counts(
&self,
query: &UsageDashboardProviderCountsQuery,
) -> Result<Vec<StoredUsageDashboardProviderCount>, crate::DataLayerError>;
async fn summarize_usage_breakdown(
&self,
query: &UsageBreakdownSummaryQuery,
) -> Result<Vec<StoredUsageBreakdownSummaryRow>, crate::DataLayerError>;
async fn count_monitoring_usage_errors(
&self,
query: &UsageMonitoringErrorCountQuery,
) -> Result<u64, crate::DataLayerError>;
async fn list_monitoring_usage_errors(
&self,
query: &UsageMonitoringErrorListQuery,
) -> Result<Vec<StoredRequestUsageAudit>, crate::DataLayerError>;
async fn summarize_usage_error_distribution(
&self,
query: &UsageErrorDistributionQuery,
) -> Result<Vec<StoredUsageErrorDistributionRow>, crate::DataLayerError>;
async fn summarize_usage_performance_percentiles(
&self,
query: &UsagePerformancePercentilesQuery,
) -> Result<Vec<StoredUsagePerformancePercentilesRow>, crate::DataLayerError>;
async fn summarize_usage_cost_savings(
&self,
query: &UsageCostSavingsSummaryQuery,
) -> Result<StoredUsageCostSavingsSummary, crate::DataLayerError>;
async fn summarize_usage_time_series(
&self,
query: &UsageTimeSeriesQuery,

View File

@@ -188,6 +188,34 @@ impl AuthApiKeyReadRepository for InMemoryAuthApiKeySnapshotRepository {
.collect())
}
async fn list_export_api_keys_by_name_search(
&self,
name_search: &str,
) -> Result<Vec<StoredAuthApiKeyExportRecord>, DataLayerError> {
let name_search = name_search.trim().to_ascii_lowercase();
if name_search.is_empty() {
return Ok(Vec::new());
}
let index = self
.index
.read()
.expect("auth api key snapshot repository lock");
Ok(index
.export_by_api_key_id
.values()
.filter(|record| {
record
.name
.as_deref()
.unwrap_or_default()
.to_ascii_lowercase()
.contains(&name_search)
})
.cloned()
.collect())
}
async fn list_export_standalone_api_keys_page(
&self,
query: &StandaloneApiKeyExportListQuery,

View File

@@ -182,6 +182,30 @@ WHERE api_keys.id = ANY($1::TEXT[])
ORDER BY api_keys.id ASC
"#;
const LIST_EXPORT_BY_NAME_SEARCH_SQL: &str = r#"
SELECT
api_keys.user_id,
api_keys.id AS api_key_id,
api_keys.key_hash,
api_keys.key_encrypted,
api_keys.name,
api_keys.allowed_providers,
api_keys.allowed_api_formats,
api_keys.allowed_models,
api_keys.rate_limit,
api_keys.concurrent_limit,
api_keys.force_capabilities,
api_keys.is_active,
CAST(EXTRACT(EPOCH FROM api_keys.expires_at) AS BIGINT) AS expires_at_unix_secs,
api_keys.auto_delete_on_expiry,
api_keys.total_requests,
COALESCE(CAST(api_keys.total_cost_usd AS DOUBLE PRECISION), 0) AS total_cost_usd,
api_keys.is_standalone
FROM api_keys
WHERE LOWER(COALESCE(api_keys.name, '')) LIKE $1
ORDER BY api_keys.id ASC
"#;
const LIST_EXPORT_STANDALONE_SQL: &str = r#"
SELECT
api_keys.user_id,
@@ -751,6 +775,24 @@ impl SqlxAuthApiKeySnapshotReadRepository {
.await
}
pub async fn list_export_api_keys_by_name_search(
&self,
name_search: &str,
) -> Result<Vec<StoredAuthApiKeyExportRecord>, DataLayerError> {
let name_search = name_search.trim();
if name_search.is_empty() {
return Ok(Vec::new());
}
Self::collect_query_rows(
sqlx::query(LIST_EXPORT_BY_NAME_SEARCH_SQL)
.bind(format!("%{}%", name_search.to_ascii_lowercase()))
.fetch(&self.pool),
map_auth_api_key_export_row,
)
.await
}
pub async fn summarize_export_api_keys_by_user_ids(
&self,
user_ids: &[String],
@@ -886,6 +928,13 @@ impl AuthApiKeyReadRepository for SqlxAuthApiKeySnapshotReadRepository {
Self::list_export_api_keys_by_ids(self, api_key_ids).await
}
async fn list_export_api_keys_by_name_search(
&self,
name_search: &str,
) -> Result<Vec<StoredAuthApiKeyExportRecord>, DataLayerError> {
Self::list_export_api_keys_by_name_search(self, name_search).await
}
async fn list_export_standalone_api_keys_page(
&self,
query: &StandaloneApiKeyExportListQuery,

View File

@@ -447,6 +447,11 @@ pub trait AuthApiKeyReadRepository: Send + Sync {
api_key_ids: &[String],
) -> Result<Vec<StoredAuthApiKeyExportRecord>, crate::DataLayerError>;
async fn list_export_api_keys_by_name_search(
&self,
name_search: &str,
) -> Result<Vec<StoredAuthApiKeyExportRecord>, crate::DataLayerError>;
async fn list_export_standalone_api_keys_page(
&self,
query: &StandaloneApiKeyExportListQuery,

File diff suppressed because it is too large Load Diff

View File

@@ -5,11 +5,23 @@ mod sql;
pub(crate) use aether_data_contracts::repository::usage::{
StoredProviderApiKeyUsageSummary, StoredProviderUsageSummary, StoredProviderUsageWindow,
StoredRequestUsageAudit, StoredUsageAuditAggregation, StoredUsageAuditSummary,
StoredUsageDailySummary, StoredUsageLeaderboardSummary, StoredUsageTimeSeriesBucket,
UpsertUsageRecord, UsageAuditAggregationGroupBy, UsageAuditAggregationQuery,
UsageAuditListQuery, UsageAuditSummaryQuery, UsageDailyHeatmapQuery, UsageLeaderboardGroupBy,
UsageLeaderboardQuery, UsageReadRepository, UsageRepository, UsageTimeSeriesGranularity,
UsageTimeSeriesQuery, UsageWriteRepository,
StoredUsageBreakdownSummaryRow, StoredUsageCacheAffinityHitSummary,
StoredUsageCacheAffinityIntervalRow, StoredUsageCacheHitSummary, StoredUsageCostSavingsSummary,
StoredUsageDailySummary, StoredUsageDashboardDailyBreakdownRow,
StoredUsageDashboardProviderCount, StoredUsageDashboardSummary,
StoredUsageErrorDistributionRow, StoredUsageLeaderboardSummary,
StoredUsagePerformancePercentilesRow, StoredUsageSettledCostSummary,
StoredUsageTimeSeriesBucket, UpsertUsageRecord, UsageAuditAggregationGroupBy,
UsageAuditAggregationQuery, UsageAuditKeywordSearchQuery, UsageAuditListQuery,
UsageAuditSummaryQuery, UsageBreakdownGroupBy, UsageBreakdownSummaryQuery,
UsageCacheAffinityHitSummaryQuery, UsageCacheAffinityIntervalGroupBy,
UsageCacheAffinityIntervalQuery, UsageCacheHitSummaryQuery, UsageCostSavingsSummaryQuery,
UsageDailyHeatmapQuery, UsageDashboardDailyBreakdownQuery, UsageDashboardProviderCountsQuery,
UsageDashboardSummaryQuery, UsageErrorDistributionQuery, UsageLeaderboardGroupBy,
UsageLeaderboardQuery, UsageMonitoringErrorCountQuery, UsageMonitoringErrorListQuery,
UsagePerformancePercentilesQuery, UsageReadRepository, UsageRepository,
UsageSettledCostSummaryQuery, UsageTimeSeriesGranularity, UsageTimeSeriesQuery,
UsageWriteRepository,
};
pub use memory::InMemoryUsageReadRepository;
pub use sql::SqlxUsageReadRepository;

File diff suppressed because it is too large Load Diff

View File

@@ -95,6 +95,29 @@ impl UserReadRepository for InMemoryUserReadRepository {
.collect())
}
async fn list_users_by_username_search(
&self,
username_search: &str,
) -> Result<Vec<StoredUserSummary>, DataLayerError> {
let username_search = username_search.trim().to_ascii_lowercase();
if username_search.is_empty() {
return Ok(Vec::new());
}
Ok(self
.by_id
.read()
.expect("user repository lock")
.values()
.filter(|user| {
user.username
.to_ascii_lowercase()
.contains(&username_search)
})
.cloned()
.collect())
}
async fn list_non_admin_export_users(
&self,
) -> Result<Vec<StoredUserExportRow>, DataLayerError> {

View File

@@ -21,6 +21,20 @@ WHERE id = ANY($1::text[])
ORDER BY id ASC
"#;
const LIST_USERS_BY_USERNAME_SEARCH_SQL: &str = r#"
SELECT
id,
username,
email,
role::text AS role,
is_active,
is_deleted
FROM users
WHERE is_deleted IS FALSE
AND LOWER(username) LIKE $1
ORDER BY id ASC
"#;
const LIST_NON_ADMIN_EXPORT_USERS_SQL: &str = r#"
SELECT
id,
@@ -199,6 +213,24 @@ impl SqlxUserReadRepository {
.await
}
pub async fn list_users_by_username_search(
&self,
username_search: &str,
) -> Result<Vec<StoredUserSummary>, DataLayerError> {
let username_search = username_search.trim();
if username_search.is_empty() {
return Ok(Vec::new());
}
collect_query_rows(
sqlx::query(LIST_USERS_BY_USERNAME_SEARCH_SQL)
.bind(format!("%{}%", username_search.to_ascii_lowercase()))
.fetch(&self.pool),
map_user_row,
)
.await
}
pub async fn list_non_admin_export_users(
&self,
) -> Result<Vec<StoredUserExportRow>, DataLayerError> {
@@ -383,6 +415,13 @@ impl UserReadRepository for SqlxUserReadRepository {
self.list_users_by_ids(user_ids).await
}
async fn list_users_by_username_search(
&self,
username_search: &str,
) -> Result<Vec<StoredUserSummary>, DataLayerError> {
self.list_users_by_username_search(username_search).await
}
async fn list_non_admin_export_users(
&self,
) -> Result<Vec<StoredUserExportRow>, DataLayerError> {

View File

@@ -393,6 +393,11 @@ pub trait UserReadRepository: Send + Sync {
user_ids: &[String],
) -> Result<Vec<StoredUserSummary>, crate::DataLayerError>;
async fn list_users_by_username_search(
&self,
username_search: &str,
) -> Result<Vec<StoredUserSummary>, crate::DataLayerError>;
async fn list_export_users(&self) -> Result<Vec<StoredUserExportRow>, crate::DataLayerError>;
async fn list_export_users_page(