Merge origin/main into main

Integrate upstream updates while preserving the local analytics dashboards and schema-only migration changes.

Combine user account analysis with upstream user/group usage statistics in separate tabs, retain all migration versions, and keep the deleted audit document removed.

Validation: gateway all-target cargo check, frontend type check and 57 focused tests, 48 migration tests, schema composition checks, and diff whitespace checks.
This commit is contained in:
elky
2026-10-02 11:57:18 +08:00
343 changed files with 27929 additions and 2549 deletions
@@ -1592,6 +1592,7 @@ fn pending_migrations_from_applied_skips_versions_already_applied() {
20260921010000,
20260921020000,
20260921020100,
20260923000000,
20261001000000,
]
);
@@ -2563,12 +2564,14 @@ INSERT INTO public.stats_daily_api_key (
.expect("API key daily aggregate fixtures should be inserted");
let leaderboard_query = UsageLeaderboardQuery {
provider_names: None,
created_from_unix_secs: u64::try_from(stats_day.timestamp())
.expect("historical stats day should be nonnegative"),
created_until_unix_secs: u64::try_from((stats_day + chrono::Duration::days(1)).timestamp())
.expect("historical stats end should be nonnegative"),
group_by: UsageLeaderboardGroupBy::ApiKey,
user_id: Some("leaderboard-owner".to_string()),
user_ids: None,
provider_name: None,
model: None,
};
@@ -107,7 +107,14 @@ WHERE version=20260919000000;
.iter()
.map(|migration| migration.version)
.collect::<Vec<_>>(),
vec![DIRTY_EVENTS, 20260921010000, 20260921020000, 20260921020100, 20261001000000]
vec![
DIRTY_EVENTS,
20260921010000,
20260921020000,
20260921020100,
20260923000000,
20261001000000,
]
);
assert_eq!(
rows_snapshot(&pool, "_sqlx_migrations").await,
@@ -346,6 +346,16 @@ fn key_auth_channel_matches(row: &StoredMinimalCandidateSelectionRow, api_format
"openai:chat" | "openai:responses" | "claude:messages" | "openai:image"
)
}
"xai" => {
matches!(auth_type.as_str(), "oauth" | "bearer" | "api_key")
&& matches!(
api_format.as_str(),
"openai:responses"
| "openai:responses:compact"
| "openai:image"
| "openai:video"
)
}
"windsurf" => {
matches!(auth_type.as_str(), "oauth" | "api_key" | "bearer")
&& api_format == "openai:chat"
@@ -591,6 +601,59 @@ mod tests {
assert_eq!(rows[0].global_model_name, "grok-4.20-0309-non-reasoning");
}
#[tokio::test]
async fn includes_xai_oauth_rows_for_responses_models() {
let mut row = sample_row("provider-xai", "openai:responses", "grok-4", 10);
row.provider_type = "xai".to_string();
row.provider_name = "xai".to_string();
row.key_auth_type = "oauth".to_string();
row.key_api_formats = Some(vec![
"openai:responses".to_string(),
"openai:responses:compact".to_string(),
]);
let repository = InMemoryMinimalCandidateSelectionReadRepository::seed(vec![row]);
let rows = repository
.list_for_exact_api_format("openai:responses")
.await
.expect("list should succeed");
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].provider_type, "xai");
assert_eq!(rows[0].global_model_name, "grok-4");
}
#[tokio::test]
async fn includes_xai_oauth_rows_for_image_and_video_models() {
let mut image = sample_row("provider-xai", "openai:image", "grok-imagine-image", 10);
image.provider_type = "xai".to_string();
image.provider_name = "xai".to_string();
image.key_auth_type = "oauth".to_string();
image.key_api_formats = Some(vec!["openai:image".to_string(), "openai:video".to_string()]);
let mut video = image.clone();
video.endpoint_id = "endpoint-video".to_string();
video.endpoint_api_format = "openai:video".to_string();
video.global_model_name = "grok-imagine-video".to_string();
video.model_provider_model_name = "grok-imagine-video".to_string();
let repository = InMemoryMinimalCandidateSelectionReadRepository::seed(vec![image, video]);
let image_rows = repository
.list_for_exact_api_format("openai:image")
.await
.expect("list should succeed");
assert_eq!(image_rows.len(), 1);
assert_eq!(image_rows[0].global_model_name, "grok-imagine-image");
let video_rows = repository
.list_for_exact_api_format("openai:video")
.await
.expect("list should succeed");
assert_eq!(video_rows.len(), 1);
assert_eq!(video_rows[0].global_model_name, "grok-imagine-video");
}
#[tokio::test]
async fn requested_model_filter_respects_endpoint_scoped_default_mapping() {
let mut selected = sample_row("provider-1", "openai:chat", "deepseek-v4-pro", 10);
@@ -600,6 +600,22 @@ fn usage_matches_summary_query(
return false;
}
}
if let Some(user_ids) = query.user_ids.as_deref() {
if !item
.user_id
.as_ref()
.is_some_and(|user_id| user_ids.contains(user_id))
{
return false;
}
}
if query
.provider_names
.as_ref()
.is_some_and(|names| !names.contains(&item.provider_name))
{
return false;
}
if let Some(provider_name) = query.provider_name.as_deref() {
if item.provider_name != provider_name {
return false;
@@ -627,6 +643,22 @@ fn usage_matches_time_series_query(
return false;
}
}
if let Some(user_ids) = query.user_ids.as_deref() {
if !item
.user_id
.as_ref()
.is_some_and(|user_id| user_ids.contains(user_id))
{
return false;
}
}
if query
.provider_names
.as_ref()
.is_some_and(|names| !names.contains(&item.provider_name))
{
return false;
}
if let Some(provider_name) = query.provider_name.as_deref() {
if item.provider_name != provider_name {
return false;
@@ -998,6 +1030,22 @@ fn usage_matches_leaderboard_query(
return false;
}
}
if let Some(user_ids) = query.user_ids.as_deref() {
if !item
.user_id
.as_ref()
.is_some_and(|user_id| user_ids.contains(user_id))
{
return false;
}
}
if query
.provider_names
.as_ref()
.is_some_and(|names| !names.contains(&item.provider_name))
{
return false;
}
if let Some(provider_name) = query.provider_name.as_deref() {
if item.provider_name != provider_name {
return false;
@@ -18,7 +18,7 @@ use aether_data_contracts::repository::usage::{
UsageAuditAggregationQuery, UsageAuditKeywordSearchQuery, UsageAuditListQuery,
UsageAuditSummaryQuery, UsageBodyCaptureState, UsageBodyField, UsageDashboardSummaryQuery,
UsageLeaderboardGroupBy, UsageLeaderboardQuery, UsageProviderPerformanceQuery,
UsageTimeSeriesGranularity,
UsageTimeSeriesGranularity, UsageTimeSeriesQuery,
};
use serde_json::json;
@@ -937,6 +937,7 @@ async fn unmetered_session_audit_counts_lifecycle_without_token_or_cost_contribu
let summary = repository
.summarize_usage_audits(&UsageAuditSummaryQuery {
provider_names: None,
created_from_unix_secs: 0,
created_until_unix_secs: 1_000,
..UsageAuditSummaryQuery::default()
@@ -2693,10 +2694,12 @@ async fn dashboard_and_leaderboard_total_tokens_use_effective_cache_aware_tokens
let leaderboard = repository
.summarize_usage_leaderboard(&UsageLeaderboardQuery {
provider_names: None,
created_from_unix_secs: 1_711_000_000,
created_until_unix_secs: 1_711_000_001,
group_by: UsageLeaderboardGroupBy::User,
user_id: None,
user_ids: None,
provider_name: None,
model: None,
})
@@ -2706,6 +2709,140 @@ async fn dashboard_and_leaderboard_total_tokens_use_effective_cache_aware_tokens
assert_eq!(leaderboard[0].total_tokens, 120);
}
#[tokio::test]
async fn usage_analytics_filters_by_multiple_user_ids() {
let user_one = sample_usage("req-user-1", 1_711_000_000);
let mut user_two = sample_usage("req-user-2", 1_711_000_000);
user_two.user_id = Some("user-2".to_string());
let mut user_three = sample_usage("req-user-3", 1_711_000_000);
user_three.user_id = Some("user-3".to_string());
let repository = InMemoryUsageReadRepository::seed(vec![user_one, user_two, user_three]);
let scoped_user_ids = vec!["user-1".to_string(), "user-2".to_string()];
let summary = repository
.summarize_usage_audits(&UsageAuditSummaryQuery {
provider_names: None,
created_from_unix_secs: 1_711_000_000,
created_until_unix_secs: 1_711_000_001,
user_ids: Some(scoped_user_ids.clone()),
..Default::default()
})
.await
.expect("summary should filter by multiple users");
assert_eq!(summary.total_requests, 2);
let buckets = repository
.summarize_usage_time_series(&UsageTimeSeriesQuery {
provider_names: None,
created_from_unix_secs: 1_711_000_000,
created_until_unix_secs: 1_711_000_001,
granularity: UsageTimeSeriesGranularity::Day,
tz_offset_minutes: 0,
user_id: None,
user_ids: Some(scoped_user_ids.clone()),
provider_name: None,
model: None,
})
.await
.expect("time series should filter by multiple users");
assert_eq!(
buckets
.iter()
.map(|bucket| bucket.total_requests)
.sum::<u64>(),
2
);
let leaderboard = repository
.summarize_usage_leaderboard(&UsageLeaderboardQuery {
provider_names: None,
created_from_unix_secs: 1_711_000_000,
created_until_unix_secs: 1_711_000_001,
group_by: UsageLeaderboardGroupBy::User,
user_id: None,
user_ids: Some(scoped_user_ids),
provider_name: None,
model: None,
})
.await
.expect("leaderboard should filter by multiple users");
assert_eq!(leaderboard.len(), 2);
assert!(leaderboard.iter().all(|item| item.group_key != "user-3"));
}
#[tokio::test]
async fn usage_analytics_intersects_provider_allowlist_and_user_scope() {
let mut a = sample_usage("allowed", 1_711_000_000);
a.provider_name = "Gemini".to_string();
a.user_id = Some("user-1".to_string());
let mut b = a.clone();
b.request_id = "other-provider".to_string();
b.provider_name = "Other".to_string();
let mut c = a.clone();
c.request_id = "other-user".to_string();
c.user_id = Some("user-2".to_string());
let repository = InMemoryUsageReadRepository::seed(vec![a, b, c]);
for (names, provider, count) in [
(
Some(vec!["Gemini".to_string(), "Gemini".to_string()]),
None,
1,
),
(Some(vec![]), None, 0),
(
Some(vec!["Gemini".to_string()]),
Some("Other".to_string()),
0,
),
(None, None, 2),
] {
let summary = repository
.summarize_usage_audits(&UsageAuditSummaryQuery {
created_from_unix_secs: 1_711_000_000,
created_until_unix_secs: 1_711_000_001,
user_ids: Some(vec!["user-1".to_string()]),
provider_names: names.clone(),
provider_name: provider.clone(),
..Default::default()
})
.await
.unwrap();
assert_eq!(summary.total_requests, count);
let buckets = repository
.summarize_usage_time_series(&UsageTimeSeriesQuery {
created_from_unix_secs: 1_711_000_000,
created_until_unix_secs: 1_711_000_001,
user_id: None,
user_ids: Some(vec!["user-1".to_string()]),
provider_names: names.clone(),
provider_name: provider.clone(),
model: None,
granularity: UsageTimeSeriesGranularity::Day,
tz_offset_minutes: 0,
})
.await
.unwrap();
assert_eq!(
buckets.iter().map(|row| row.total_requests).sum::<u64>(),
count
);
let rows = repository
.summarize_usage_leaderboard(&UsageLeaderboardQuery {
created_from_unix_secs: 1_711_000_000,
created_until_unix_secs: 1_711_000_001,
user_id: None,
user_ids: Some(vec!["user-1".to_string()]),
provider_names: names,
provider_name: provider,
model: None,
group_by: UsageLeaderboardGroupBy::User,
})
.await
.unwrap();
assert_eq!(rows.iter().map(|row| row.request_count).sum::<u64>(), count);
}
}
#[tokio::test]
async fn summarizes_provider_api_key_last_used_at_in_seconds() {
let repository = InMemoryUsageReadRepository::seed(vec![
@@ -2328,8 +2328,13 @@ impl WalletWriteRepository for InMemoryWalletRepository {
async fn adjust_wallet_balance(
&self,
_input: AdjustWalletBalanceInput,
) -> Result<Option<(StoredWalletSnapshot, super::StoredAdminWalletTransaction)>, DataLayerError>
{
) -> Result<
Option<(
StoredWalletSnapshot,
Option<super::StoredAdminWalletTransaction>,
)>,
DataLayerError,
> {
Ok(None)
}
@@ -16,27 +16,30 @@ pub use aether_data_contracts::repository::wallet::{
wallet_recharge_order_is_checkout_placeholder,
wallet_recharge_order_is_reclaimable_placeholder, wallet_recharge_replay_matches,
wallet_recharge_response_is_checkout_placeholder, wallet_refund_proof_is_success,
AdjustWalletBalanceInput, AdminPaymentCallbackRecord, AdminPaymentOrderListQuery,
AdminRedeemCodeBatchListQuery, AdminRedeemCodeListQuery, AdminWalletLedgerQuery,
AdminWalletListQuery, AdminWalletPaymentOrderRecord, AdminWalletRefundRecord,
AdminWalletRefundRequestListQuery, AdminWalletTransactionRecord, CanonicalWalletRefundFields,
CompareAndSwapPaymentOrderStripeClientSecretInput, CompleteAdminWalletRefundInput,
CreateAdminRedeemCodeBatchInput, CreateAdminRedeemCodeBatchResult,
CreateManualWalletRechargeInput, CreatePlanPurchaseOrderInput, CreatePlanPurchaseOrderOutcome,
CreateWalletRechargeOrderInput, CreateWalletRechargeOrderOutcome,
CreateWalletRefundRequestInput, CreateWalletRefundRequestOutcome,
CreatedAdminRedeemCodePlaintext, CreditAdminPaymentOrderInput, DeleteAdminRedeemCodeBatchInput,
AdjustWalletBalanceInBatchInput, AdjustWalletBalanceInput, AdminPaymentCallbackRecord,
AdminPaymentOrderListQuery, AdminRedeemCodeBatchListQuery, AdminRedeemCodeListQuery,
AdminUserWalletBalanceBatchContext, AdminUserWalletBalanceBatchUserOutcome,
AdminWalletLedgerQuery, AdminWalletListQuery, AdminWalletPaymentOrderRecord,
AdminWalletRefundRecord, AdminWalletRefundRequestListQuery, AdminWalletTransactionRecord,
CanonicalWalletRefundFields, CompareAndSwapPaymentOrderStripeClientSecretInput,
CompleteAdminWalletRefundInput, CreateAdminRedeemCodeBatchInput,
CreateAdminRedeemCodeBatchResult, CreateManualWalletRechargeInput,
CreatePlanPurchaseOrderInput, CreatePlanPurchaseOrderOutcome, CreateWalletRechargeOrderInput,
CreateWalletRechargeOrderOutcome, CreateWalletRefundRequestInput,
CreateWalletRefundRequestOutcome, CreatedAdminRedeemCodePlaintext,
CreditAdminPaymentOrderInput, DeleteAdminRedeemCodeBatchInput,
DisableAdminRedeemCodeBatchInput, DisableAdminRedeemCodeInput, FailAdminWalletRefundInput,
FailWalletRechargeCheckoutInput, InitializeAuthWalletOutcome, ProcessAdminWalletRefundInput,
ProcessPaymentCallbackInput, ProcessPaymentCallbackOutcome, ReclaimWalletRechargeCheckoutInput,
RedeemWalletCodeInput, RedeemWalletCodeOutcome, StoredAdminPaymentCallback,
StoredAdminPaymentCallbackPage, StoredAdminPaymentOrder, StoredAdminPaymentOrderPage,
StoredAdminRedeemCode, StoredAdminRedeemCodeBatch, StoredAdminRedeemCodeBatchPage,
StoredAdminRedeemCodePage, StoredAdminWalletLedgerItem, StoredAdminWalletLedgerPage,
StoredAdminWalletListItem, StoredAdminWalletListPage, StoredAdminWalletRefund,
StoredAdminWalletRefundPage, StoredAdminWalletRefundRequestItem,
StoredAdminWalletRefundRequestPage, StoredAdminWalletTransaction,
StoredAdminWalletTransactionPage, StoredWalletDailyUsageLedger,
FailWalletRechargeCheckoutInput, InitializeAuthWalletOutcome,
PrepareAdminUserWalletBalanceBatchInput, PrepareAdminUserWalletBalanceBatchOutcome,
ProcessAdminWalletRefundInput, ProcessPaymentCallbackInput, ProcessPaymentCallbackOutcome,
ReclaimWalletRechargeCheckoutInput, RedeemWalletCodeInput, RedeemWalletCodeOutcome,
StoredAdminPaymentCallback, StoredAdminPaymentCallbackPage, StoredAdminPaymentOrder,
StoredAdminPaymentOrderPage, StoredAdminRedeemCode, StoredAdminRedeemCodeBatch,
StoredAdminRedeemCodeBatchPage, StoredAdminRedeemCodePage, StoredAdminUserWalletBalanceBatch,
StoredAdminWalletLedgerItem, StoredAdminWalletLedgerPage, StoredAdminWalletListItem,
StoredAdminWalletListPage, StoredAdminWalletRefund, StoredAdminWalletRefundPage,
StoredAdminWalletRefundRequestItem, StoredAdminWalletRefundRequestPage,
StoredAdminWalletTransaction, StoredAdminWalletTransactionPage, StoredWalletDailyUsageLedger,
StoredWalletDailyUsageLedgerPage, StoredWalletSnapshot, UpdateAdminWalletRefundGatewayInput,
UpdateWalletRechargeCheckoutInput, WalletLookupKey, WalletMutationOutcome,
WalletReadRepository, WalletReadSeed, WalletReadSnapshot, WalletRepository,