From f370d355b645c4a4fccef1d56b32d48c956531e1 Mon Sep 17 00:00:00 2001 From: Zyy Date: Tue, 11 Aug 2026 17:21:23 +0800 Subject: [PATCH] fix(accounts): align quota ordering and summaries Co-authored-by: Codex --- .../openai/src/credential/quota/snapshot.rs | 14 ++++- .../openai/tests/credential/quota/snapshot.rs | 52 ++++++++++++++++++ .../components/AccountQuotaSummaryCell.vue | 2 +- .../components/AccountUsageWindow.vue | 54 +++++++++++++++---- frontend/src/views/accounts/constants.ts | 12 +++-- 5 files changed, 116 insertions(+), 18 deletions(-) diff --git a/backend/crates/providers/openai/src/credential/quota/snapshot.rs b/backend/crates/providers/openai/src/credential/quota/snapshot.rs index 7ce271234..74d7a5d05 100644 --- a/backend/crates/providers/openai/src/credential/quota/snapshot.rs +++ b/backend/crates/providers/openai/src/credential/quota/snapshot.rs @@ -334,7 +334,11 @@ pub(crate) fn parse_account_quota_snapshot( &mut windows, )?; } - windows.sort_by(|left, right| left.key.cmp(&right.key)); + windows.sort_by(|left, right| { + quota_source_order(left.source()) + .cmp("a_source_order(right.source())) + .then_with(|| left.key.cmp(&right.key)) + }); Ok(CodexAccountQuotaSnapshot { account_id, credential_revision, @@ -345,6 +349,14 @@ pub(crate) fn parse_account_quota_snapshot( }) } +fn quota_source_order(source: &str) -> u8 { + match source { + "codex" => 0, + "code_review" => 1, + _ => 2, + } +} + /// 账号状态只能由顶层 canonical Codex 额度桶的明确拒绝来改变。 /// /// `fact.exhausted()` 和窗口 `limit_reached` 都会聚合或继承 secondary、附加桶等事实, diff --git a/backend/crates/providers/openai/tests/credential/quota/snapshot.rs b/backend/crates/providers/openai/tests/credential/quota/snapshot.rs index a18a136e7..2b3851e13 100644 --- a/backend/crates/providers/openai/tests/credential/quota/snapshot.rs +++ b/backend/crates/providers/openai/tests/credential/quota/snapshot.rs @@ -364,6 +364,58 @@ async fn persisted_codex_alias_keeps_the_top_level_rate_limit_canonical() { ); } +#[tokio::test] +async fn persisted_quota_orders_core_window_before_additional_limit() { + let store = Arc::new(MemoryAccountStore::default()); + create_account(&store, "acct_quota_order").await; + let account = store.account("acct_quota_order").expect("account"); + let raw = json!({ + "rate_limit": { + "secondary_window": { + "used_percent": 42, + "reset_at": 1_900_604_800, + "limit_window_seconds": 604_800 + } + }, + "additional_rate_limits": [{ + "limit_name": "GPT-5.3-Codex-Spark", + "metered_feature": "gpt-5.3-codex-spark", + "rate_limit": { + "primary_window": { + "used_percent": 17, + "reset_at": 1_900_604_800, + "limit_window_seconds": 604_800 + } + } + }] + }); + store + .compare_and_swap_quota(QuotaObservation { + account_id: account.id().clone(), + expected_revision: account.revision(), + quota: Some(OpaqueProviderData::new( + raw.as_object().expect("quota object").clone(), + )), + observed_at: Some(SystemTime::now()), + limit_reached: None, + }) + .await + .expect("persist quota"); + + let snapshot = quota_service(&store) + .read_account(account.id()) + .await + .expect("read quota") + .expect("quota snapshot"); + let sources = snapshot + .windows() + .iter() + .map(|window| window.source()) + .collect::>(); + + assert_eq!(sources, ["codex", "gpt-5.3-codex-spark"]); +} + #[tokio::test] async fn code_review_limit_projects_as_independent_window_with_limit_name() { let store = Arc::new(MemoryAccountStore::default()); diff --git a/frontend/src/views/accounts/components/AccountQuotaSummaryCell.vue b/frontend/src/views/accounts/components/AccountQuotaSummaryCell.vue index 36f370df8..e93851492 100644 --- a/frontend/src/views/accounts/components/AccountQuotaSummaryCell.vue +++ b/frontend/src/views/accounts/components/AccountQuotaSummaryCell.vue @@ -13,7 +13,7 @@ const quotaWindows = computed(() => props.account.quota.windows) const visibleQuotaWindows = computed(() => visibleSummaryQuotaWindows(quotaWindows.value)) const summaryClass = computed( () => - `grid w-full max-w-40 min-w-0 gap-2 whitespace-normal py-0.5 ${ + `grid w-full min-w-0 gap-1.5 py-0.5 ${ visibleQuotaWindows.value.length <= 1 ? 'min-h-13 content-center' : '' }`, ) diff --git a/frontend/src/views/accounts/components/AccountUsageWindow.vue b/frontend/src/views/accounts/components/AccountUsageWindow.vue index 09c550475..b243075c6 100644 --- a/frontend/src/views/accounts/components/AccountUsageWindow.vue +++ b/frontend/src/views/accounts/components/AccountUsageWindow.vue @@ -66,25 +66,29 @@ function variantValue(metric: string, compact: string, detail: string) { const rootClass = computed(() => variantValue( - 'grid min-w-0 grid-rows-[11px_14px] gap-1', + 'grid min-w-0 grid-rows-[14px_14px] gap-1', 'flex min-w-0 flex-col', 'rounded-lg bg-(--cp-bg-subtle) p-2', ), ) const headerClass = computed(() => variantValue( - 'flex items-center justify-between gap-2 text-[11px] leading-none font-bold', - 'mb-1 flex items-center justify-between gap-2 text-[11px] leading-none font-bold', + 'flex items-center justify-between gap-2 text-[11px] leading-[14px] font-bold', + 'mb-1 flex items-baseline justify-between gap-2 text-[11px] leading-[14px] font-bold', 'flex items-center justify-between gap-3 text-[12px] font-bold', ), ) -const labelClass = computed(() => - isCompact.value || isMetric.value ? 'text-(--cp-text-muted)' : 'text-(--cp-text-secondary)', -) +const labelClass = computed(() => { + if (isMetric.value) + return 'truncate text-(--cp-text-muted)' + if (isCompact.value) + return 'truncate text-(--cp-text-muted)' + return 'text-(--cp-text-secondary)' +}) const valueClass = computed(() => variantValue( 'text-[11px] leading-none font-bold', - 'text-[10px] leading-none font-heavy', + 'text-[10px] leading-[14px] font-heavy', 'text-[12px] font-heavy', ), ) @@ -94,7 +98,7 @@ const trackOffsetClass = computed(() => const trackShapeClass = computed(() => variantValue( 'h-1.5 w-full overflow-hidden rounded-full', - 'h-1 w-full overflow-hidden rounded-full', + 'h-[3px] w-full overflow-hidden rounded-full', 'h-2 overflow-hidden rounded-full', ), ) @@ -104,7 +108,7 @@ const barStyle = computed(() => { return undefined return quotaWindowBarStyle( props.window, - variantValue('6px', '4px', '8px'), + variantValue('6px', '3px', '8px'), ) }) const barClass = computed(() => props.window ? quotaWindowBarClass(props.window) : undefined) @@ -167,10 +171,38 @@ const requestBars = computed(() => {