mirror of
https://github.com/zyycn/codex-proxy-rs.git
synced 2026-11-06 08:06:19 +00:00
fix(accounts): align quota ordering and summaries
Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
@@ -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、附加桶等事实,
|
||||
|
||||
@@ -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::<Vec<_>>();
|
||||
|
||||
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());
|
||||
|
||||
@@ -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' : ''
|
||||
}`,
|
||||
)
|
||||
|
||||
@@ -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(() => {
|
||||
<div :class="rootClass">
|
||||
<template v-if="mode === 'quota' && window">
|
||||
<div :class="headerClass">
|
||||
<span class="min-w-0" :class="labelClass">
|
||||
<span
|
||||
class="min-w-0"
|
||||
:class="labelClass"
|
||||
:title="isCompact || isMetric ? window.labelDisplay : undefined"
|
||||
>
|
||||
{{ window.labelDisplay }}
|
||||
</span>
|
||||
<span class="flex shrink-0 items-baseline justify-end gap-1.5 font-mono tabular-nums">
|
||||
<span
|
||||
v-if="isCompact"
|
||||
class="flex shrink-0 items-baseline justify-end gap-1.5 font-mono tabular-nums"
|
||||
>
|
||||
<span
|
||||
v-if="quotaLocalUsageVisible"
|
||||
class="text-right text-(--cp-text-muted)"
|
||||
:class="valueClass"
|
||||
:title="`本地 Token 用量:${quotaLocalUsageDisplay}`"
|
||||
>
|
||||
{{ quotaLocalUsageDisplay }}
|
||||
</span>
|
||||
<span
|
||||
v-if="quotaValueVisible"
|
||||
class="text-right"
|
||||
:class="[valueClass, percentTextClass]"
|
||||
:title="`额度已使用:${window.usedPercentDisplay}`"
|
||||
>
|
||||
{{ window.usedPercentDisplay }}
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
v-else
|
||||
class="flex shrink-0 items-baseline justify-end gap-1.5 font-mono tabular-nums"
|
||||
>
|
||||
<span
|
||||
v-if="quotaLocalUsageVisible"
|
||||
class="text-(--cp-text-muted)"
|
||||
|
||||
@@ -25,7 +25,7 @@ const quotaGroupOrder = new Map([
|
||||
['other', 2],
|
||||
])
|
||||
|
||||
const relaxedCellClass = 'py-3 align-middle'
|
||||
const relaxedCellClass = 'py-2 align-middle'
|
||||
|
||||
export const accountColumns = [
|
||||
{
|
||||
@@ -76,8 +76,8 @@ export const accountColumns = [
|
||||
key: 'planType',
|
||||
label: '套餐',
|
||||
sortable: true,
|
||||
flex: 0.8,
|
||||
minWidth: '112px',
|
||||
flex: 0.5,
|
||||
minWidth: '90px',
|
||||
cellClass: relaxedCellClass,
|
||||
},
|
||||
{
|
||||
@@ -96,7 +96,8 @@ export const accountColumns = [
|
||||
minWidth: '160px',
|
||||
format: (_value: unknown, row: AccountRow) => optionalAccountCell(row.usage.lastUsedAtDisplay),
|
||||
emptyText: '',
|
||||
cellClass: `${relaxedCellClass} text-(--cp-text-secondary)`,
|
||||
headerClass: '!pl-8',
|
||||
cellClass: `${relaxedCellClass} !pl-8 text-(--cp-text-secondary)`,
|
||||
},
|
||||
{
|
||||
key: 'accessTokenExpiresAtDisplay',
|
||||
@@ -212,7 +213,8 @@ function compareQuotaWindows(left: AccountQuotaWindow, right: AccountQuotaWindow
|
||||
if (secondsDifference !== 0)
|
||||
return secondsDifference
|
||||
|
||||
return left.key.localeCompare(right.key)
|
||||
// 同组同周期保留 Provider 的投影顺序,避免内部 key 把 additional 排到 core 前面。
|
||||
return 0
|
||||
}
|
||||
|
||||
function groupOrder(window: AccountQuotaWindow) {
|
||||
|
||||
Reference in New Issue
Block a user