fix(xai): preserve quota exhaustion state

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
zyycn
2026-07-30 03:10:35 +08:00
co-authored by Codex
parent acf574129f
commit cae2c8656a
8 changed files with 46 additions and 32 deletions
@@ -461,7 +461,9 @@ pub struct RequestMetricPoint {
/// 账号池容量统计。
///
/// 配额耗尽单列为可恢复状态;`unavailable` 只包含过期、禁用和封禁账号。
/// 首页概览的 `unavailable` 表示不可参与调度,包含配额耗尽及过期、禁用和封禁账号。
///
/// `quota_exhausted` 仍单列,以便状态详情展示和诊断。
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct AccountPoolMetrics {
pub total: u64,
@@ -2234,9 +2234,6 @@ fn dashboard_view(result: domain::DashboardResult, kind: TrendKind) -> Dashboard
account_usage,
recent_requests,
} = observation;
// 概览卡只有可用和不可用两桶:额度耗尽可恢复,保留在可用;不可用只含终态账号。
let available_accounts =
dashboard_available_account_count(provider_accounts.total, provider_accounts.unavailable);
let mut account_usage_views = Vec::with_capacity(account_usage.len());
let mut credential_usage_views = Vec::with_capacity(account_usage.len());
for credential in account_usage {
@@ -2285,8 +2282,8 @@ fn dashboard_view(result: domain::DashboardResult, kind: TrendKind) -> Dashboard
credentials: DashboardCredentialsCardView {
total: format_compact_number(provider_accounts.total),
total_value: provider_accounts.total,
available: format_compact_number(available_accounts),
available_value: available_accounts,
available: format_compact_number(provider_accounts.active),
available_value: provider_accounts.active,
unavailable: format_compact_number(provider_accounts.unavailable),
unavailable_value: provider_accounts.unavailable,
},
@@ -2342,10 +2339,6 @@ fn dashboard_view(result: domain::DashboardResult, kind: TrendKind) -> Dashboard
}
}
fn dashboard_available_account_count(total: u64, unavailable: u64) -> u64 {
total.saturating_sub(unavailable)
}
fn usage_summary_view(summary: domain::UsageSummary) -> UsageSummaryView {
let overview = summary.overview;
UsageSummaryView {
@@ -261,7 +261,7 @@ mod response {
}
#[tokio::test]
async fn dashboard_summary_should_count_quota_exhausted_accounts_as_available_in_headline_card() {
async fn dashboard_summary_should_include_quota_exhaustion_in_unavailable_headline_count() {
use axum::{
body::{Body, to_bytes},
http::{Request, StatusCode, header},
@@ -288,7 +288,7 @@ async fn dashboard_summary_should_count_quota_exhausted_accounts_as_available_in
provider_accounts: AccountPoolMetrics {
total: 807,
enabled: 804,
unavailable: 7,
unavailable: 792,
active: 15,
expired: 2,
quota_exhausted: 785,
@@ -328,8 +328,8 @@ async fn dashboard_summary_should_count_quota_exhausted_accounts_as_available_in
),
(
&serde_json::json!(807),
&serde_json::json!(800),
&serde_json::json!(7)
&serde_json::json!(15),
&serde_json::json!(792)
),
);
}
@@ -2013,8 +2013,7 @@ async fn provider_account_metrics(
)
select count(*)::bigint as total,
count(*) filter (where enabled)::bigint as enabled,
count(*) filter (where status in ('expired', 'disabled', 'banned'))::bigint
as unavailable,
count(*) filter (where status <> 'active')::bigint as unavailable,
count(*) filter (where status = 'active')::bigint as active,
count(*) filter (where status = 'expired')::bigint as expired,
count(*) filter (where status = 'quota_exhausted')::bigint as quota_exhausted,
@@ -169,12 +169,14 @@ async fn dashboard_account_metrics_should_partition_account_statuses() {
metrics.disabled,
metrics.banned,
),
(6, 5, 2, 3, 1, 1, 1, 1),
(6, 5, 2, 4, 1, 1, 1, 1),
);
// 首页按调度可用性聚合:配额耗尽也属于不可用。
assert_eq!(
metrics.total,
metrics.active + metrics.quota_exhausted + metrics.unavailable
metrics.unavailable,
metrics.quota_exhausted + metrics.expired + metrics.disabled + metrics.banned
);
assert_eq!(metrics.total, metrics.active + metrics.unavailable);
database.close().await;
}
@@ -469,7 +469,7 @@ impl GrokCredentialQuotaService {
if quota_refresh_may_update_state(&loaded.account, &current.account)
&& let Some(availability) = quota_refresh_availability(
current.account.availability(),
quota_is_exhausted(snapshot.billing()),
authoritative_quota_exhaustion(snapshot.billing()),
)
{
self.repository
@@ -1329,6 +1329,13 @@ fn quota_is_exhausted(billing: &GrokBillingPresentation) -> bool {
)
}
/// `None` means the billing document cannot prove either quota state.
fn authoritative_quota_exhaustion(billing: &GrokBillingPresentation) -> Option<bool> {
billing
.has_authoritative_quota()
.then(|| quota_is_exhausted(billing))
}
fn quota_refresh_may_update_state(before: &ProviderAccount, current: &ProviderAccount) -> bool {
current.enabled()
&& current
@@ -1341,22 +1348,26 @@ fn quota_refresh_may_update_state(before: &ProviderAccount, current: &ProviderAc
fn quota_refresh_availability(
current: AccountAvailability,
exhausted: bool,
exhausted: Option<bool>,
) -> Option<GrokCredentialAvailability> {
match current {
AccountAvailability::Invalid
| AccountAvailability::Expired
| AccountAvailability::Banned => None,
AccountAvailability::QuotaExhausted => {
(!exhausted).then_some(GrokCredentialAvailability::Ready)
matches!(exhausted, Some(false)).then_some(GrokCredentialAvailability::Ready)
}
AccountAvailability::Cooldown | AccountAvailability::Unknown => {
exhausted.map(|exhausted| {
if exhausted {
GrokCredentialAvailability::QuotaExhausted
} else {
GrokCredentialAvailability::Ready
}
})
}
AccountAvailability::Cooldown | AccountAvailability::Unknown => Some(if exhausted {
GrokCredentialAvailability::QuotaExhausted
} else {
GrokCredentialAvailability::Ready
}),
AccountAvailability::Ready => {
exhausted.then_some(GrokCredentialAvailability::QuotaExhausted)
matches!(exhausted, Some(true)).then_some(GrokCredentialAvailability::QuotaExhausted)
}
}
}
@@ -874,17 +874,24 @@ async fn quota_projection_preserves_unknown_period_for_dynamic_duration_fallback
}
#[tokio::test]
async fn weekly_period_without_reported_allowance_is_not_authoritative_quota() {
let (_, repository) = repository_with_accounts(&[("free-quota", "subject-free-quota")]).await;
async fn non_authoritative_quota_refresh_does_not_clear_existing_quota_exhaustion() {
let (store, repository) =
repository_with_accounts(&[("free-quota", "subject-free-quota")]).await;
let id = account_id("free-quota");
set_account_state(&store, &id, AccountAvailability::QuotaExhausted, None).await;
let transport = QueueBillingTransport::success(
br#"{"config":{"currentPeriod":{"type":"USAGE_PERIOD_TYPE_WEEKLY","start":"2026-07-15T00:00:00Z","end":"2026-07-22T00:00:00Z"},"onDemandCap":{"val":0},"onDemandUsed":{"val":0},"prepaidBalance":{"val":0}}}"#,
);
let snapshot = crate::support::grok_quota_service(repository, transport)
.refresh_account(&account_id("free-quota"))
.refresh_account(&id)
.await
.expect("refresh Free quota");
assert!(!snapshot.billing().has_authoritative_quota());
assert_eq!(
store.account(&id).expect("account").availability(),
AccountAvailability::QuotaExhausted
);
}
#[tokio::test]
@@ -33,7 +33,7 @@ const overviewItems = computed(() => [
icon: Gauge,
},
{
label: '不可用账号',
label: '异常账号',
value: formatCount(props.summary.unavailable),
caption: '过期 / 禁用 / 封禁',
tone: 'danger',