Merge pull request #554 from kaitranntt/fix/553-codex-quota-display

fix(codex): align quota display to 5h and weekly windows
This commit is contained in:
Kai (Tam Nhu) Tran
2026-02-14 06:32:33 +07:00
committed by GitHub
8 changed files with 623 additions and 120 deletions
+115 -93
View File
@@ -14,6 +14,8 @@ import type { CodexQuotaResult, CodexQuotaWindow } from './quota-types';
/** ChatGPT backend API base URL */
const CODEX_API_BASE = 'https://chatgpt.com/backend-api';
const CODEX_QUOTA_TIMEOUT_MS = 12000;
const CODEX_QUOTA_MAX_ATTEMPTS = 2;
/**
* User agent matching Codex CLI for API compatibility.
@@ -222,114 +224,134 @@ export async function fetchCodexQuota(
}
const url = `${CODEX_API_BASE}/wham/usage`;
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000);
let lastErrorMsg = 'Unknown error';
try {
const response = await fetch(url, {
method: 'GET',
signal: controller.signal,
headers: {
Authorization: `Bearer ${authData.accessToken}`,
'ChatGPT-Account-Id': authData.accountId,
'User-Agent': USER_AGENT,
},
});
for (let attempt = 1; attempt <= CODEX_QUOTA_MAX_ATTEMPTS; attempt++) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), CODEX_QUOTA_TIMEOUT_MS);
clearTimeout(timeoutId);
try {
const response = await fetch(url, {
method: 'GET',
signal: controller.signal,
headers: {
Authorization: `Bearer ${authData.accessToken}`,
'ChatGPT-Account-Id': authData.accountId,
'User-Agent': USER_AGENT,
},
});
if (verbose) console.error(`[i] Codex API status: ${response.status}`);
clearTimeout(timeoutId);
if (verbose) console.error(`[i] Codex API status: ${response.status} (attempt ${attempt})`);
if (response.status === 401) {
return {
success: false,
windows: [],
planType: null,
lastUpdated: Date.now(),
error: 'Token expired or invalid',
accountId,
needsReauth: true,
};
}
if (response.status === 403) {
// 403 = account lacks API access (not same as quota exhausted)
// Keep success=false with isForbidden flag for UI to show distinct "403" badge
return {
success: false,
windows: [],
planType: null,
lastUpdated: Date.now(),
error: '403 Forbidden - No quota API access',
accountId,
isForbidden: true,
};
}
if (response.status === 429) {
return {
success: false,
windows: [],
planType: null,
lastUpdated: Date.now(),
error: 'Rate limited - try again later',
accountId,
};
}
if (!response.ok) {
return {
success: false,
windows: [],
planType: null,
lastUpdated: Date.now(),
error: `API error: ${response.status}`,
accountId,
};
}
const data = (await response.json()) as CodexUsageResponse;
const windows = buildCodexQuotaWindows(data);
// Extract plan type
const planTypeRaw = data.plan_type || data.planType;
let planType: 'free' | 'plus' | 'team' | null = null;
if (planTypeRaw) {
const normalized = planTypeRaw.toLowerCase();
if (normalized === 'free') planType = 'free';
else if (normalized === 'plus') planType = 'plus';
else if (normalized === 'team') planType = 'team';
}
if (verbose) console.error(`[i] Codex windows found: ${windows.length}`);
if (response.status === 401) {
return {
success: false,
windows: [],
planType: null,
success: true,
windows,
planType,
lastUpdated: Date.now(),
error: 'Token expired or invalid',
accountId,
needsReauth: true,
};
}
if (response.status === 403) {
// 403 = account lacks API access (not same as quota exhausted)
// Keep success=false with isForbidden flag for UI to show distinct "403" badge
return {
success: false,
windows: [],
planType: null,
lastUpdated: Date.now(),
error: '403 Forbidden - No quota API access',
accountId,
isForbidden: true,
};
}
if (response.status === 429) {
return {
success: false,
windows: [],
planType: null,
lastUpdated: Date.now(),
error: 'Rate limited - try again later',
accountId,
};
}
if (!response.ok) {
return {
success: false,
windows: [],
planType: null,
lastUpdated: Date.now(),
error: `API error: ${response.status}`,
accountId,
};
}
const data = (await response.json()) as CodexUsageResponse;
const windows = buildCodexQuotaWindows(data);
// Extract plan type
const planTypeRaw = data.plan_type || data.planType;
let planType: 'free' | 'plus' | 'team' | null = null;
if (planTypeRaw) {
const normalized = planTypeRaw.toLowerCase();
if (normalized === 'free') planType = 'free';
else if (normalized === 'plus') planType = 'plus';
else if (normalized === 'team') planType = 'team';
}
if (verbose) console.error(`[i] Codex windows found: ${windows.length}`);
return {
success: true,
windows,
planType,
lastUpdated: Date.now(),
accountId,
};
} catch (err) {
clearTimeout(timeoutId);
const errorMsg =
err instanceof Error && err.name === 'AbortError'
} catch (err) {
clearTimeout(timeoutId);
const isAbortError = err instanceof Error && err.name === 'AbortError';
lastErrorMsg = isAbortError
? 'Request timeout'
: err instanceof Error
? err.message
: 'Unknown error';
if (verbose) console.error(`[!] Codex quota error: ${errorMsg}`);
if (verbose) {
console.error(`[!] Codex quota error (attempt ${attempt}): ${lastErrorMsg}`);
}
return {
success: false,
windows: [],
planType: null,
lastUpdated: Date.now(),
error: errorMsg,
accountId,
};
// Retry timeout once; other failures return immediately.
if (isAbortError && attempt < CODEX_QUOTA_MAX_ATTEMPTS) {
continue;
}
return {
success: false,
windows: [],
planType: null,
lastUpdated: Date.now(),
error: lastErrorMsg,
accountId,
};
}
}
return {
success: false,
windows: [],
planType: null,
lastUpdated: Date.now(),
error: lastErrorMsg,
accountId,
};
}
/**
+142 -4
View File
@@ -78,6 +78,134 @@ 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';
}
type CodexWindowSummary = Pick<CodexQuotaResult['windows'][number], 'label' | 'resetAfterSeconds'>;
function inferCodeReviewCadence(
window: CodexWindowSummary,
allWindows: CodexWindowSummary[]
): '5h' | 'weekly' | null {
const kind = getCodexWindowKind(window.label);
if (kind === 'code-review-weekly') return 'weekly';
const reset = window.resetAfterSeconds;
if (typeof reset !== 'number' || !isFinite(reset) || reset <= 0) return null;
const usage5h = allWindows.find(
(w) =>
getCodexWindowKind(w.label) === 'usage-5h' &&
typeof w.resetAfterSeconds === 'number' &&
isFinite(w.resetAfterSeconds) &&
w.resetAfterSeconds > 0
);
const usageWeekly = allWindows.find(
(w) =>
getCodexWindowKind(w.label) === 'usage-weekly' &&
typeof w.resetAfterSeconds === 'number' &&
isFinite(w.resetAfterSeconds) &&
w.resetAfterSeconds > 0
);
if (!usage5h || !usageWeekly) return null;
const diffTo5h = Math.abs(reset - (usage5h.resetAfterSeconds as number));
const diffToWeekly = Math.abs(reset - (usageWeekly.resetAfterSeconds as number));
return diffToWeekly <= diffTo5h ? 'weekly' : '5h';
}
function getCodexWindowDisplayLabel(
window: CodexWindowSummary,
allWindows: CodexWindowSummary[] = []
): string {
const context = allWindows.length > 0 ? allWindows : [window];
switch (getCodexWindowKind(window.label)) {
case 'usage-5h':
return '5h usage limit';
case 'usage-weekly':
return 'Weekly usage limit';
case 'code-review-5h':
case 'code-review-weekly':
case 'code-review': {
const inferred = inferCodeReviewCadence(window, context);
if (inferred === '5h') return 'Code review (5h)';
if (inferred === 'weekly') return 'Code review (weekly)';
return 'Code review';
}
case 'unknown':
return window.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 +271,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, orderedWindows).padEnd(24)} ${bar} ${window.remainingPercent.toFixed(0)}%${resetLabel}`
);
}
console.log('');
+35 -4
View File
@@ -46,6 +46,37 @@ import { loadOrCreateUnifiedConfig } from '../../config/unified-config-loader';
const router = Router();
/**
* Cache only stable failures; avoid pinning transient network failures (timeouts, 429s).
*/
function shouldCacheCodexQuotaResult(result: CodexQuotaResult): boolean {
if (result.success) return true;
if (result.needsReauth || result.isForbidden) return true;
const msg = (result.error || '').toLowerCase();
if (!msg) return false;
if (msg.includes('timeout')) return false;
if (msg.includes('rate limited')) return false;
if (msg.includes('api error: 5')) return false;
if (msg.includes('fetch failed')) return false;
return false;
}
function shouldCacheGeminiQuotaResult(result: GeminiCliQuotaResult): boolean {
if (result.success) return true;
if (result.needsReauth) return true;
const msg = (result.error || '').toLowerCase();
if (!msg) return false;
if (msg.includes('timeout')) return false;
if (msg.includes('rate limited')) return false;
if (msg.includes('api error: 5')) return false;
if (msg.includes('fetch failed')) return false;
return false;
}
/** Get configured backend from config */
function getConfiguredBackend() {
try {
@@ -548,8 +579,8 @@ router.get('/quota/codex/:accountId', async (req: Request, res: Response): Promi
// Fetch from external API
const result = await fetchCodexQuota(accountId);
// Cache successful results (don't cache errors that need reauth)
if (result.success || !result.needsReauth) {
// Cache successful and stable failure states; skip transient network failures.
if (shouldCacheCodexQuotaResult(result)) {
setCachedQuota('codex', accountId, result);
}
@@ -589,8 +620,8 @@ router.get('/quota/gemini/:accountId', async (req: Request, res: Response): Prom
// Fetch from external API
const result = await fetchGeminiCliQuota(accountId);
// Cache successful results (don't cache errors that need reauth)
if (result.success || !result.needsReauth) {
// Cache successful and stable failure states; skip transient network failures.
if (shouldCacheGeminiQuotaResult(result)) {
setCachedQuota('gemini', accountId, result);
}
@@ -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, orderedWindows)}
</span>
<span className="font-mono">{w.remainingPercent}%</span>
</div>
))}
+187 -2
View File
@@ -306,12 +306,191 @@ 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';
}
type CodexWindowSummary = Pick<CodexQuotaWindow, 'label' | 'resetAfterSeconds'>;
/**
* Infer code-review window cadence by comparing against usage windows.
* This keeps labels stable as countdown values decrease over time.
*/
function inferCodeReviewCadence(
window: CodexWindowSummary,
allWindows: CodexWindowSummary[]
): '5h' | 'weekly' | null {
const kind = getCodexWindowKind(window.label);
if (kind === 'code-review-weekly') return 'weekly';
const reset = window.resetAfterSeconds;
if (typeof reset !== 'number' || !isFinite(reset) || reset <= 0) return null;
const usage5h = allWindows.find(
(w) =>
getCodexWindowKind(w.label) === 'usage-5h' &&
typeof w.resetAfterSeconds === 'number' &&
isFinite(w.resetAfterSeconds) &&
w.resetAfterSeconds > 0
);
const usageWeekly = allWindows.find(
(w) =>
getCodexWindowKind(w.label) === 'usage-weekly' &&
typeof w.resetAfterSeconds === 'number' &&
isFinite(w.resetAfterSeconds) &&
w.resetAfterSeconds > 0
);
if (!usage5h || !usageWeekly) return null;
const diffTo5h = Math.abs(reset - (usage5h.resetAfterSeconds as number));
const diffToWeekly = Math.abs(reset - (usageWeekly.resetAfterSeconds as number));
return diffToWeekly <= diffTo5h ? 'weekly' : '5h';
}
export function getCodexWindowDisplayLabel(
labelOrWindow: string | CodexWindowSummary,
allWindows: CodexWindowSummary[] = []
): string {
const label = typeof labelOrWindow === 'string' ? labelOrWindow : labelOrWindow.label;
const currentWindow: CodexWindowSummary =
typeof labelOrWindow === 'string'
? { label, resetAfterSeconds: null }
: { label, resetAfterSeconds: labelOrWindow.resetAfterSeconds };
const context = allWindows.length > 0 ? allWindows : [currentWindow];
switch (getCodexWindowKind(label)) {
case 'usage-5h':
return '5h usage limit';
case 'usage-weekly':
return 'Weekly usage limit';
case 'code-review-5h':
case 'code-review-weekly':
case 'code-review': {
const inferred = inferCodeReviewCadence(currentWindow, context);
if (inferred === '5h') return 'Code review (5h)';
if (inferred === 'weekly') return 'Code review (weekly)';
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 +499,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];
}
+56 -4
View File
@@ -9,6 +9,7 @@ import {
getEarliestResetTime,
getMinCodexQuota,
getCodexResetTime,
getCodexWindowDisplayLabel,
getMinGeminiQuota,
getGeminiResetTime,
getProviderMinQuota,
@@ -386,7 +387,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 +441,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,14 +518,64 @@ 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');
});
});
});
describe('getCodexWindowDisplayLabel', () => {
it('labels code review primary as weekly when it matches usage weekly window', () => {
const windows: Array<{ label: string; resetAfterSeconds: number | null }> = [
{ label: 'Primary', resetAfterSeconds: 18000 },
{ label: 'Secondary', resetAfterSeconds: 604800 },
{ label: 'Code Review (Primary)', resetAfterSeconds: 600000 },
];
expect(
getCodexWindowDisplayLabel(
{
label: 'Code Review (Primary)',
resetAfterSeconds: 600000,
},
windows
)
).toBe('Code review (weekly)');
});
it('labels code review primary as 5h when it matches usage 5h window', () => {
const windows: Array<{ label: string; resetAfterSeconds: number | null }> = [
{ label: 'Primary', resetAfterSeconds: 18000 },
{ label: 'Secondary', resetAfterSeconds: 604800 },
{ label: 'Code Review (Primary)', resetAfterSeconds: 17000 },
];
expect(
getCodexWindowDisplayLabel(
{
label: 'Code Review (Primary)',
resetAfterSeconds: 17000,
},
windows
)
).toBe('Code review (5h)');
});
it('keeps secondary usage label as weekly by default', () => {
expect(getCodexWindowDisplayLabel('Secondary')).toBe('Weekly usage limit');
});
it('falls back to generic code review label when cadence cannot be inferred', () => {
expect(
getCodexWindowDisplayLabel({
label: 'Code Review (Primary)',
resetAfterSeconds: 604800,
})
).toBe('Code review');
});
});
// ==================== Gemini Quota Functions ====================
describe('getMinGeminiQuota', () => {