mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-17 06:17:09 +00:00
fix(codex): align quota display to 5h and weekly windows
This commit is contained in:
@@ -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
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user