fix(codex): align quota display to 5h and weekly windows

This commit is contained in:
Tam Nhu Tran
2026-02-14 05:57:06 +07:00
parent 6c2083949f
commit f8af5a8c3c
6 changed files with 335 additions and 23 deletions
+101 -4
View File
@@ -78,6 +78,93 @@ function formatResetTimeISO(isoTime: string): string {
return formatResetTime(seconds);
}
type CodexWindowKind =
| 'usage-5h'
| 'usage-weekly'
| 'code-review-5h'
| 'code-review-weekly'
| 'code-review'
| 'unknown';
function getCodexWindowKind(label: string): CodexWindowKind {
const lower = (label || '').toLowerCase();
const isCodeReview = lower.includes('code review') || lower.includes('code_review');
const isPrimary = lower.includes('primary');
const isSecondary = lower.includes('secondary');
if (isCodeReview) {
if (isPrimary) return 'code-review-5h';
if (isSecondary) return 'code-review-weekly';
return 'code-review';
}
if (isPrimary) return 'usage-5h';
if (isSecondary) return 'usage-weekly';
return 'unknown';
}
function getCodexWindowDisplayLabel(label: string): string {
switch (getCodexWindowKind(label)) {
case 'usage-5h':
return '5h usage limit';
case 'usage-weekly':
return 'Weekly usage limit';
case 'code-review-5h':
return 'Code review (5h)';
case 'code-review-weekly':
return 'Code review (weekly)';
case 'code-review':
return 'Code review';
case 'unknown':
return label;
}
}
function getCodexCoreUsageWindows(windows: CodexQuotaResult['windows']): {
fiveHourWindow: CodexQuotaResult['windows'][number] | null;
weeklyWindow: CodexQuotaResult['windows'][number] | null;
} {
let fiveHourWindow: CodexQuotaResult['windows'][number] | null = null;
let weeklyWindow: CodexQuotaResult['windows'][number] | null = null;
const nonCodeReviewWindows: CodexQuotaResult['windows'] = [];
for (const window of windows) {
const kind = getCodexWindowKind(window.label);
if (kind === 'usage-5h') {
if (!fiveHourWindow) fiveHourWindow = window;
nonCodeReviewWindows.push(window);
continue;
}
if (kind === 'usage-weekly') {
if (!weeklyWindow) weeklyWindow = window;
nonCodeReviewWindows.push(window);
continue;
}
if (kind === 'unknown') {
nonCodeReviewWindows.push(window);
}
}
if ((!fiveHourWindow || !weeklyWindow) && nonCodeReviewWindows.length > 0) {
const withReset = nonCodeReviewWindows
.filter((w) => typeof w.resetAfterSeconds === 'number' && w.resetAfterSeconds >= 0)
.sort((a, b) => (a.resetAfterSeconds || 0) - (b.resetAfterSeconds || 0));
if (!fiveHourWindow) {
fiveHourWindow = withReset[0] || nonCodeReviewWindows[0] || null;
}
if (!weeklyWindow) {
weeklyWindow =
withReset.length > 1
? withReset[withReset.length - 1]
: nonCodeReviewWindows.find((w) => w !== fiveHourWindow) || null;
}
}
return { fiveHourWindow, weeklyWindow };
}
function displayAntigravityQuotaSection(
quotaResult: Awaited<ReturnType<typeof fetchAllProviderQuotas>>
): void {
@@ -143,22 +230,32 @@ function displayCodexQuotaSection(results: { account: string; quota: CodexQuotaR
continue;
}
const { fiveHourWindow, weeklyWindow } = getCodexCoreUsageWindows(quota.windows);
const coreUsageWindows = [fiveHourWindow, weeklyWindow].filter(
(w, index, arr): w is NonNullable<typeof w> => !!w && arr.indexOf(w) === index
);
const statusWindows = coreUsageWindows.length > 0 ? coreUsageWindows : quota.windows;
const avgQuota =
quota.windows.length > 0
? quota.windows.reduce((sum, w) => sum + w.remainingPercent, 0) / quota.windows.length
statusWindows.length > 0
? statusWindows.reduce((sum, w) => sum + w.remainingPercent, 0) / statusWindows.length
: 0;
const statusIcon = avgQuota > 50 ? ok('') : avgQuota > 10 ? warn('') : fail('');
const planBadge = quota.planType ? color(` [${quota.planType}]`, 'info') : '';
console.log(` ${statusIcon}${account}${defaultMark}${planBadge}`);
for (const window of quota.windows) {
const orderedWindows = [fiveHourWindow, weeklyWindow, ...quota.windows].filter(
(w, index, arr): w is NonNullable<typeof w> => !!w && arr.indexOf(w) === index
);
for (const window of orderedWindows) {
const bar = formatQuotaBar(window.remainingPercent);
const resetLabel = window.resetAfterSeconds
? dim(` Resets ${formatResetTime(window.resetAfterSeconds)}`)
: '';
console.log(
` ${window.label.padEnd(24)} ${bar} ${window.remainingPercent.toFixed(0)}%${resetLabel}`
` ${getCodexWindowDisplayLabel(window.label).padEnd(24)} ${bar} ${window.remainingPercent.toFixed(0)}%${resetLabel}`
);
}
console.log('');
@@ -2,7 +2,13 @@
* Account Card Component for Flow Visualization
*/
import { cn, getProviderMinQuota, getProviderResetTime } from '@/lib/utils';
import {
cn,
getCodexQuotaBreakdown,
getProviderMinQuota,
getProviderResetTime,
isCodexQuotaResult,
} from '@/lib/utils';
import { PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context';
import { GripVertical, Loader2, Pause, Play, KeyRound } from 'lucide-react';
import { useAccountQuota, QUOTA_SUPPORTED_PROVIDERS } from '@/hooks/use-cliproxy-stats';
@@ -96,6 +102,14 @@ export function AccountCard({
// Use shared helper for provider-specific minimum quota
const minQuota = getProviderMinQuota(account.provider, quota);
const resetTime = getProviderResetTime(account.provider, quota);
const codexBreakdown =
account.provider === 'codex' && quota && isCodexQuotaResult(quota)
? getCodexQuotaBreakdown(quota.windows)
: null;
const codexQuotaRows = [
{ label: '5h', value: codexBreakdown?.fiveHourWindow?.remainingPercent ?? null },
{ label: 'Wk', value: codexBreakdown?.weeklyWindow?.remainingPercent ?? null },
].filter((row): row is { label: string; value: number } => row.value !== null);
// Tier badge (AGY only) - show P for Pro, U for Ultra
const showTierBadge =
@@ -229,6 +243,15 @@ export function AccountCard({
{minQuota}%
</span>
</div>
{account.provider === 'codex' && codexQuotaRows.length > 0 && (
<div className="flex items-center justify-between text-[7px] text-muted-foreground/70">
{codexQuotaRows.map((row) => (
<span key={row.label}>
{row.label} {row.value}%
</span>
))}
</div>
)}
<div className="w-full bg-muted dark:bg-zinc-800/50 h-1 rounded-full overflow-hidden">
<div
className={cn(
@@ -30,7 +30,13 @@ import {
Check,
KeyRound,
} from 'lucide-react';
import { cn, getProviderMinQuota, getProviderResetTime } from '@/lib/utils';
import {
cn,
getCodexQuotaBreakdown,
getProviderMinQuota,
getProviderResetTime,
isCodexQuotaResult,
} from '@/lib/utils';
import { PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context';
import { useAccountQuota, useCliproxyStats } from '@/hooks/use-cliproxy-stats';
import { QuotaTooltipContent } from '@/components/shared/quota-tooltip-content';
@@ -115,6 +121,14 @@ export function AccountItem({
// Use shared utility functions for provider-specific quota handling
const minQuota = getProviderMinQuota(account.provider, quota);
const nextReset = getProviderResetTime(account.provider, quota);
const codexBreakdown =
account.provider === 'codex' && quota && isCodexQuotaResult(quota)
? getCodexQuotaBreakdown(quota.windows)
: null;
const codexQuotaRows = [
{ label: '5h', value: codexBreakdown?.fiveHourWindow?.remainingPercent ?? null },
{ label: 'Weekly', value: codexBreakdown?.weeklyWindow?.remainingPercent ?? null },
].filter((row): row is { label: string; value: number } => row.value !== null);
return (
<div
@@ -337,14 +351,34 @@ export function AccountItem({
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center gap-2">
<Progress
value={Math.max(0, Math.min(100, minQuota))}
className="h-2 flex-1"
indicatorClassName={getQuotaColor(minQuota)}
/>
<span className="text-xs font-medium w-10 text-right">{minQuota}%</span>
</div>
{account.provider === 'codex' && codexQuotaRows.length > 0 ? (
<div className="space-y-1.5">
{codexQuotaRows.map((row) => (
<div key={row.label} className="flex items-center gap-2">
<span className="w-10 text-[10px] text-muted-foreground">
{row.label}
</span>
<Progress
value={Math.max(0, Math.min(100, row.value))}
className="h-2 flex-1"
indicatorClassName={getQuotaColor(row.value)}
/>
<span className="text-xs font-medium w-10 text-right">
{row.value}%
</span>
</div>
))}
</div>
) : (
<div className="flex items-center gap-2">
<Progress
value={Math.max(0, Math.min(100, minQuota))}
className="h-2 flex-1"
indicatorClassName={getQuotaColor(minQuota)}
/>
<span className="text-xs font-medium w-10 text-right">{minQuota}%</span>
</div>
)}
</TooltipTrigger>
<TooltipContent side="bottom" className="max-w-xs">
{quota && <QuotaTooltipContent quota={quota} resetTime={nextReset} />}
@@ -7,6 +7,8 @@ import { Clock } from 'lucide-react';
import {
cn,
formatResetTime,
getCodexQuotaBreakdown,
getCodexWindowDisplayLabel,
getModelsWithTiers,
groupModelsByTier,
isAgyQuotaResult,
@@ -64,13 +66,29 @@ export function QuotaTooltipContent({ quota, resetTime }: QuotaTooltipContentPro
// Codex provider tooltip
if (isCodexQuotaResult(quota)) {
const { fiveHourWindow, weeklyWindow, codeReviewWindows, unknownWindows } =
getCodexQuotaBreakdown(quota.windows);
const orderedWindows = [fiveHourWindow, weeklyWindow, ...codeReviewWindows, ...unknownWindows]
.filter((w): w is NonNullable<typeof w> => !!w)
.filter(
(w, index, arr) =>
arr.findIndex(
(candidate) => candidate.label === w.label && candidate.resetAt === w.resetAt
) === index
);
return (
<div className="text-xs space-y-1">
<p className="font-medium">Rate Limits:</p>
{quota.planType && <p className="text-muted-foreground">Plan: {quota.planType}</p>}
{quota.windows.map((w) => (
<div key={w.label} className="flex justify-between gap-4">
<span className={cn(w.remainingPercent < 20 && 'text-red-500')}>{w.label}</span>
{orderedWindows.map((w, index) => (
<div
key={`${w.label}-${w.resetAt ?? 'no-reset'}-${index}`}
className="flex justify-between gap-4"
>
<span className={cn(w.remainingPercent < 20 && 'text-red-500')}>
{getCodexWindowDisplayLabel(w.label)}
</span>
<span className="font-mono">{w.remainingPercent}%</span>
</div>
))}
+140 -2
View File
@@ -306,12 +306,144 @@ export function groupModelsByTier(models: TieredModel[]): Map<ModelTier, TieredM
return groups;
}
export type CodexWindowKind =
| 'usage-5h'
| 'usage-weekly'
| 'code-review-5h'
| 'code-review-weekly'
| 'code-review'
| 'unknown';
/**
* Map raw Codex API window labels into semantic buckets.
*/
export function getCodexWindowKind(label: string): CodexWindowKind {
const lower = (label || '').toLowerCase();
const isCodeReview = lower.includes('code review') || lower.includes('code_review');
const isPrimary = lower.includes('primary');
const isSecondary = lower.includes('secondary');
if (isCodeReview) {
if (isPrimary) return 'code-review-5h';
if (isSecondary) return 'code-review-weekly';
return 'code-review';
}
if (isPrimary) return 'usage-5h';
if (isSecondary) return 'usage-weekly';
return 'unknown';
}
/**
* Convert raw Codex window labels into user-facing labels.
*/
export function getCodexWindowDisplayLabel(label: string): string {
switch (getCodexWindowKind(label)) {
case 'usage-5h':
return '5h usage limit';
case 'usage-weekly':
return 'Weekly usage limit';
case 'code-review-5h':
return 'Code review (5h)';
case 'code-review-weekly':
return 'Code review (weekly)';
case 'code-review':
return 'Code review';
case 'unknown':
return label;
}
}
export interface CodexQuotaBreakdown {
fiveHourWindow: CodexQuotaWindow | null;
weeklyWindow: CodexQuotaWindow | null;
codeReviewWindows: CodexQuotaWindow[];
unknownWindows: CodexQuotaWindow[];
}
/**
* Break down Codex windows into core usage windows (5h + weekly) and auxiliary windows.
*/
export function getCodexQuotaBreakdown(windows: CodexQuotaWindow[]): CodexQuotaBreakdown {
if (!windows || windows.length === 0) {
return {
fiveHourWindow: null,
weeklyWindow: null,
codeReviewWindows: [],
unknownWindows: [],
};
}
let fiveHourWindow: CodexQuotaWindow | null = null;
let weeklyWindow: CodexQuotaWindow | null = null;
const codeReviewWindows: CodexQuotaWindow[] = [];
const unknownWindows: CodexQuotaWindow[] = [];
const nonCodeReviewWindows: CodexQuotaWindow[] = [];
for (const window of windows) {
const kind = getCodexWindowKind(window.label);
switch (kind) {
case 'usage-5h':
if (!fiveHourWindow) fiveHourWindow = window;
nonCodeReviewWindows.push(window);
break;
case 'usage-weekly':
if (!weeklyWindow) weeklyWindow = window;
nonCodeReviewWindows.push(window);
break;
case 'code-review-5h':
case 'code-review-weekly':
case 'code-review':
codeReviewWindows.push(window);
break;
case 'unknown':
unknownWindows.push(window);
nonCodeReviewWindows.push(window);
break;
}
}
// Fallback for API label changes: infer 5h/weekly from reset horizon when explicit labels are absent.
if ((!fiveHourWindow || !weeklyWindow) && nonCodeReviewWindows.length > 0) {
const withReset = nonCodeReviewWindows
.filter((w) => typeof w.resetAfterSeconds === 'number' && w.resetAfterSeconds >= 0)
.sort((a, b) => (a.resetAfterSeconds || 0) - (b.resetAfterSeconds || 0));
if (!fiveHourWindow) {
fiveHourWindow = withReset[0] || nonCodeReviewWindows[0] || null;
}
if (!weeklyWindow) {
weeklyWindow =
withReset.length > 1
? withReset[withReset.length - 1]
: nonCodeReviewWindows.find((w) => w !== fiveHourWindow) || null;
}
}
return {
fiveHourWindow,
weeklyWindow,
codeReviewWindows,
unknownWindows,
};
}
/**
* Get minimum remaining percentage across Codex rate limit windows
*/
export function getMinCodexQuota(windows: CodexQuotaWindow[]): number | null {
if (!windows || windows.length === 0) return null;
const percentages = windows.map((w) => w.remainingPercent);
const { fiveHourWindow, weeklyWindow } = getCodexQuotaBreakdown(windows);
const usageWindows = [fiveHourWindow, weeklyWindow].filter(
(w, index, arr): w is CodexQuotaWindow => !!w && arr.indexOf(w) === index
);
// Primary account quota should be driven by core usage windows, not code-review windows.
const sourceWindows = usageWindows.length > 0 ? usageWindows : windows;
const percentages = sourceWindows.map((w) => w.remainingPercent);
return Math.min(...percentages);
}
@@ -320,7 +452,13 @@ export function getMinCodexQuota(windows: CodexQuotaWindow[]): number | null {
*/
export function getCodexResetTime(windows: CodexQuotaWindow[]): string | null {
if (!windows || windows.length === 0) return null;
const resets = windows.map((w) => w.resetAt).filter((t): t is string => t !== null);
const { fiveHourWindow, weeklyWindow } = getCodexQuotaBreakdown(windows);
const usageWindows = [fiveHourWindow, weeklyWindow].filter(
(w, index, arr): w is CodexQuotaWindow => !!w && arr.indexOf(w) === index
);
const sourceWindows = usageWindows.length > 0 ? usageWindows : windows;
const resets = sourceWindows.map((w) => w.resetAt).filter((t): t is string => t !== null);
if (resets.length === 0) return null;
return resets.sort()[0];
}
+6 -4
View File
@@ -386,7 +386,8 @@ describe('getMinCodexQuota', () => {
resetAt: '2026-01-30T14:30:32Z',
},
];
expect(getMinCodexQuota(windows)).toBe(8.7);
// Core account quota should ignore code-review windows.
expect(getMinCodexQuota(windows)).toBe(21.1);
});
});
});
@@ -439,10 +440,10 @@ describe('getCodexResetTime', () => {
usedPercent: 50,
remainingPercent: 50,
resetAfterSeconds: 1800,
resetAt: '2026-01-30T16:00:00Z',
resetAt: '2026-01-30T09:00:00Z',
},
];
// Should return earliest (alphabetically sorted)
// Should ignore code-review reset when choosing main account reset.
expect(getCodexResetTime(windows)).toBe('2026-01-30T10:00:00Z');
});
});
@@ -516,9 +517,10 @@ describe('getCodexResetTime', () => {
usedPercent: 75,
remainingPercent: 25,
resetAfterSeconds: 5400,
resetAt: '2026-01-30T18:45:00Z',
resetAt: '2026-01-30T08:15:00Z',
},
];
// Code-review windows should not drive the main account reset.
expect(getCodexResetTime(windows)).toBe('2026-01-30T09:30:00Z');
});
});