From 7744e1b0b422dbbfdcb773bbf53a125ec80c00f1 Mon Sep 17 00:00:00 2001 From: Zyy Date: Sat, 15 Aug 2026 01:28:44 +0800 Subject: [PATCH] fix(accounts): preserve rolling window usage Co-authored-by: Codex --- .../src/model/provider_credentials.rs | 6 +-- backend/crates/gateway-admin/tests/main.rs | 46 +++++++++++++++++++ 2 files changed, 49 insertions(+), 3 deletions(-) diff --git a/backend/crates/gateway-admin/src/model/provider_credentials.rs b/backend/crates/gateway-admin/src/model/provider_credentials.rs index 9ae60961d..785039042 100644 --- a/backend/crates/gateway-admin/src/model/provider_credentials.rs +++ b/backend/crates/gateway-admin/src/model/provider_credentials.rs @@ -608,8 +608,9 @@ pub struct ProviderQuota { impl ProviderQuota { /// 返回账号展开面板使用的当前额度窗口用量。 /// - /// 只选择具备完整时间边界且可按账号归属的窗口;优先级与 Dashboard - /// 代表性额度窗口一致,同一优先级保持 Provider 投影顺序。 + /// Provider 已提供账号级本地用量时,固定窗口和无固定重置点的滚动窗口 + /// 都可作为代表用量;优先级与 Dashboard 代表性额度窗口一致, + /// 同一优先级保持 Provider 投影顺序。 #[must_use] pub fn representative_window_usage(&self) -> Option<&AccountUsage> { self.windows @@ -618,7 +619,6 @@ impl ProviderQuota { .filter(|(_, window)| { window.local_usage_attribution == QuotaLocalUsageAttribution::AccountWide && window.window_seconds.is_some() - && window.reset_at.is_some() && window.local_usage.is_some() }) .min_by_key(|(index, window)| (quota_usage_priority(window), *index)) diff --git a/backend/crates/gateway-admin/tests/main.rs b/backend/crates/gateway-admin/tests/main.rs index 87161f895..6c6091f93 100644 --- a/backend/crates/gateway-admin/tests/main.rs +++ b/backend/crates/gateway-admin/tests/main.rs @@ -6,6 +6,7 @@ use gateway_core::engine::credential::OpaqueProviderData; use gateway_admin::{ model::{ PageSize, Revision, + accounts::AccountUsage, auth::LoginCommand, client_keys::ClientKeyPageSize, observability::{DecimalAmount, RequestOutcome, TimeRange}, @@ -135,6 +136,51 @@ fn representative_quota_should_prefer_short_window_and_highest_usage() { assert_eq!(quota.representative_used_percent(), Some(45.0)); } +#[test] +fn representative_window_usage_should_accept_account_wide_rolling_window_without_reset() { + let usage = AccountUsage { + account_id: "acct_xai".to_owned(), + request_count: 5, + success_count: 5, + input_tokens: Some(100), + output_tokens: Some(20), + cached_tokens: Some(80), + cache_write_tokens: Some(0), + reasoning_tokens: Some(0), + image_input_tokens: Some(0), + image_output_tokens: Some(0), + image_request_count: 0, + image_request_failed_count: 0, + total_tokens: Some(120), + cost_coverage: Default::default(), + costs: Vec::new(), + last_used_at: Some(Utc::now()), + request_buckets: Vec::new(), + models: Vec::new(), + }; + let quota = ProviderQuota { + observed_at: None, + refresh_token_expires_at: None, + windows: vec![ProviderQuotaWindow { + key: "free-rolling-24h".to_owned(), + group: "shortTerm".to_owned(), + label: "日限额".to_owned(), + source: None, + local_usage_attribution: QuotaLocalUsageAttribution::AccountWide, + window_seconds: Some(86_400), + used_percent: None, + reset_at: None, + limit_reached: false, + local_usage: Some(usage.clone()), + provider_data: None, + }], + limit_reached: false, + provider_data: None, + }; + + assert_eq!(quota.representative_window_usage(), Some(&usage)); +} + #[test] fn exhausted_quota_should_project_full_usage_to_only_the_representative_window() { let mut quota = ProviderQuota {