feat(cliproxy): expose codex weekly reset schedule in quota views

This commit is contained in:
Tam Nhu Tran
2026-02-15 09:48:39 +07:00
parent b40fe866ec
commit 4fc19c4390
8 changed files with 329 additions and 8 deletions
+1
View File
@@ -238,6 +238,7 @@ Re-creates symlinks for shared commands, skills, and settings.
```bash
ccs cliproxy doctor # Check quota status for all agy accounts
ccs cliproxy quota # Show agy/codex/gemini quotas (Codex: 5h + weekly reset schedule)
```
**Auto-Failover**: When an Antigravity account runs out of quota, CCS automatically switches to another account with remaining capacity. Shared GCP project accounts are excluded (pooled quota).
+96 -1
View File
@@ -10,7 +10,7 @@ import * as path from 'node:path';
import { getAuthDir } from './config-generator';
import { getProviderAccounts, getPausedDir } from './account-manager';
import { sanitizeEmail, isTokenExpired } from './auth-utils';
import type { CodexQuotaResult, CodexQuotaWindow } from './quota-types';
import type { CodexQuotaResult, CodexQuotaWindow, CodexCoreUsageSummary } from './quota-types';
/** ChatGPT backend API base URL */
const CODEX_API_BASE = 'https://chatgpt.com/backend-api';
@@ -57,6 +57,99 @@ interface CodexWindowData {
resetAfterSeconds?: number | null;
}
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';
}
/**
* Build explicit 5h + weekly usage summary from raw Codex windows.
* Falls back to shortest/longest reset windows if API labels change.
*/
export function buildCodexCoreUsageSummary(windows: CodexQuotaWindow[]): CodexCoreUsageSummary {
if (!windows || windows.length === 0) {
return { fiveHour: null, weekly: null };
}
let fiveHourWindow: CodexQuotaWindow | null = null;
let weeklyWindow: CodexQuotaWindow | null = null;
const nonCodeReviewWindows: CodexQuotaWindow[] = [];
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' &&
isFinite(w.resetAfterSeconds) &&
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;
}
}
const mapWindow = (window: CodexQuotaWindow | null): CodexCoreUsageSummary['fiveHour'] => {
if (!window) return null;
return {
label: window.label,
remainingPercent: window.remainingPercent,
resetAfterSeconds: window.resetAfterSeconds,
resetAt: window.resetAt,
};
};
return {
fiveHour: mapWindow(fiveHourWindow),
weekly: mapWindow(weeklyWindow),
};
}
/**
* Read auth data from Codex auth file
*/
@@ -295,6 +388,7 @@ export async function fetchCodexQuota(
const data = (await response.json()) as CodexUsageResponse;
const windows = buildCodexQuotaWindows(data);
const coreUsage = buildCodexCoreUsageSummary(windows);
// Extract plan type
const planTypeRaw = data.plan_type || data.planType;
@@ -311,6 +405,7 @@ export async function fetchCodexQuota(
return {
success: true,
windows,
coreUsage,
planType,
lastUpdated: Date.now(),
accountId,
+22
View File
@@ -27,6 +27,26 @@ export interface CodexQuotaWindow {
resetAt: string | null;
}
/** Core Codex usage window (5h/weekly) extracted from raw windows */
export interface CodexCoreUsageWindow {
/** Source window label */
label: string;
/** Percentage remaining (0-100) */
remainingPercent: number;
/** Seconds until quota resets, null if unknown */
resetAfterSeconds: number | null;
/** ISO timestamp when quota resets, null if unknown */
resetAt: string | null;
}
/** Core Codex usage summary with explicit 5h and weekly windows */
export interface CodexCoreUsageSummary {
/** Short-cycle usage limit window (typically 5h) */
fiveHour: CodexCoreUsageWindow | null;
/** Long-cycle usage limit window (typically weekly) */
weekly: CodexCoreUsageWindow | null;
}
/**
* Codex quota fetch result
*/
@@ -35,6 +55,8 @@ export interface CodexQuotaResult {
success: boolean;
/** Quota windows (primary, secondary, code review) */
windows: CodexQuotaWindow[];
/** Explicit core usage windows (5h + weekly) for easier reset display */
coreUsage?: CodexCoreUsageSummary;
/** Plan type: free, plus, team, or null if unknown */
planType: 'free' | 'plus' | 'team' | null;
/** Timestamp of fetch */
+1 -1
View File
@@ -54,7 +54,7 @@ export async function showHelp(): Promise<void> {
['default <account>', 'Set default account for rotation'],
['pause <account>', 'Pause account (skip in rotation)'],
['resume <account>', 'Resume paused account'],
['quota', 'Show quota status for all providers'],
['quota', 'Show quota status for all providers (Codex includes 5h + weekly reset)'],
['quota --provider <name>', 'Filter by provider (agy|codex|gemini)'],
],
],
+74 -4
View File
@@ -67,7 +67,13 @@ function formatResetTime(seconds: number): string {
if (seconds <= 0) return 'now';
if (seconds < 60) return `in ${seconds}s`;
if (seconds < 3600) return `in ${Math.round(seconds / 60)}m`;
return `in ${Math.round(seconds / 3600)}h`;
if (seconds < 86400) return `in ${Math.round(seconds / 3600)}h`;
const days = Math.floor(seconds / 86400);
const hours = Math.round((seconds % 86400) / 3600);
if (hours <= 0) return `in ${days}d`;
if (hours >= 24) return `in ${days + 1}d`;
return `in ${days}d ${hours}h`;
}
function formatResetTimeISO(isoTime: string): string {
@@ -78,6 +84,40 @@ function formatResetTimeISO(isoTime: string): string {
return formatResetTime(seconds);
}
function formatAbsoluteResetTime(isoTime: string): string | null {
if (!isoTime) return null;
const resetDate = new Date(isoTime);
if (isNaN(resetDate.getTime())) return null;
const date = resetDate.toLocaleDateString(undefined, {
month: '2-digit',
day: '2-digit',
});
const time = resetDate.toLocaleTimeString(undefined, {
hour: '2-digit',
minute: '2-digit',
});
return `${date} ${time}`;
}
function formatCodexWindowReset(
window: Pick<CodexQuotaResult['windows'][number], 'resetAfterSeconds' | 'resetAt'>
): string | null {
if (typeof window.resetAfterSeconds === 'number' && isFinite(window.resetAfterSeconds)) {
const relative = formatResetTime(Math.max(0, window.resetAfterSeconds));
if (window.resetAfterSeconds >= 86400 && window.resetAt) {
const absolute = formatAbsoluteResetTime(window.resetAt);
return absolute ? `${relative} (${absolute})` : relative;
}
return relative;
}
if (window.resetAt) {
return formatResetTimeISO(window.resetAt);
}
return null;
}
type CodexWindowKind =
| 'usage-5h'
| 'usage-weekly'
@@ -286,15 +326,45 @@ function displayCodexQuotaSection(results: { account: string; quota: CodexQuotaR
console.log(` ${statusIcon}${account}${defaultMark}${planBadge}`);
const coreUsageSummary = quota.coreUsage ?? {
fiveHour: fiveHourWindow
? {
label: fiveHourWindow.label,
remainingPercent: fiveHourWindow.remainingPercent,
resetAfterSeconds: fiveHourWindow.resetAfterSeconds,
resetAt: fiveHourWindow.resetAt,
}
: null,
weekly: weeklyWindow
? {
label: weeklyWindow.label,
remainingPercent: weeklyWindow.remainingPercent,
resetAfterSeconds: weeklyWindow.resetAfterSeconds,
resetAt: weeklyWindow.resetAt,
}
: null,
};
const resetParts: string[] = [];
const fiveHourReset = coreUsageSummary.fiveHour
? formatCodexWindowReset(coreUsageSummary.fiveHour)
: null;
const weeklyReset = coreUsageSummary.weekly
? formatCodexWindowReset(coreUsageSummary.weekly)
: null;
if (fiveHourReset) resetParts.push(`5h ${fiveHourReset}`);
if (weeklyReset) resetParts.push(`weekly ${weeklyReset}`);
if (resetParts.length > 0) {
console.log(` ${dim(`Reset schedule: ${resetParts.join(' | ')}`)}`);
}
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)}`)
: '';
const resetValue = formatCodexWindowReset(window);
const resetLabel = resetValue ? dim(` Resets ${resetValue}`) : '';
console.log(
` ${getCodexWindowDisplayLabel(window, orderedWindows).padEnd(24)} ${bar} ${window.remainingPercent.toFixed(0)}%${resetLabel}`
);
@@ -5,7 +5,10 @@
*/
import { describe, it, expect } from 'bun:test';
import { buildCodexQuotaWindows } from '../../../src/cliproxy/quota-fetcher-codex';
import {
buildCodexQuotaWindows,
buildCodexCoreUsageSummary,
} from '../../../src/cliproxy/quota-fetcher-codex';
describe('Codex Quota Fetcher', () => {
describe('buildCodexQuotaWindows', () => {
@@ -187,4 +190,69 @@ describe('Codex Quota Fetcher', () => {
expect(windows[0].remainingPercent).toBe(100);
});
});
describe('buildCodexCoreUsageSummary', () => {
it('extracts 5h and weekly windows from labeled usage windows', () => {
const windows = buildCodexQuotaWindows({
rate_limit: {
primary_window: {
used_percent: 35,
reset_after_seconds: 18000,
},
secondary_window: {
used_percent: 60,
reset_after_seconds: 604800,
},
},
});
const summary = buildCodexCoreUsageSummary(windows);
expect(summary.fiveHour?.label).toBe('Primary');
expect(summary.fiveHour?.remainingPercent).toBe(65);
expect(summary.fiveHour?.resetAfterSeconds).toBe(18000);
expect(summary.weekly?.label).toBe('Secondary');
expect(summary.weekly?.remainingPercent).toBe(40);
expect(summary.weekly?.resetAfterSeconds).toBe(604800);
});
it('falls back to shortest and longest reset windows when labels are unknown', () => {
const windows = [
{
label: 'Window A',
usedPercent: 20,
remainingPercent: 80,
resetAfterSeconds: 18000,
resetAt: '2026-02-15T15:00:00Z',
},
{
label: 'Window B',
usedPercent: 45,
remainingPercent: 55,
resetAfterSeconds: 604800,
resetAt: '2026-02-21T10:00:00Z',
},
{
label: 'Code Review (Primary)',
usedPercent: 10,
remainingPercent: 90,
resetAfterSeconds: 3600,
resetAt: '2026-02-15T11:00:00Z',
},
];
const summary = buildCodexCoreUsageSummary(windows);
expect(summary.fiveHour?.label).toBe('Window A');
expect(summary.fiveHour?.resetAfterSeconds).toBe(18000);
expect(summary.weekly?.label).toBe('Window B');
expect(summary.weekly?.resetAfterSeconds).toBe(604800);
});
it('returns null summaries when no windows are available', () => {
const summary = buildCodexCoreUsageSummary([]);
expect(summary.fiveHour).toBeNull();
expect(summary.weekly).toBeNull();
});
});
});
@@ -68,6 +68,8 @@ export function QuotaTooltipContent({ quota, resetTime }: QuotaTooltipContentPro
if (isCodexQuotaResult(quota)) {
const { fiveHourWindow, weeklyWindow, codeReviewWindows, unknownWindows } =
getCodexQuotaBreakdown(quota.windows);
const fiveHourResetAt = quota.coreUsage?.fiveHour?.resetAt ?? fiveHourWindow?.resetAt ?? null;
const weeklyResetAt = quota.coreUsage?.weekly?.resetAt ?? weeklyWindow?.resetAt ?? null;
const orderedWindows = [fiveHourWindow, weeklyWindow, ...codeReviewWindows, ...unknownWindows]
.filter((w): w is NonNullable<typeof w> => !!w)
.filter(
@@ -92,7 +94,11 @@ export function QuotaTooltipContent({ quota, resetTime }: QuotaTooltipContentPro
<span className="font-mono">{w.remainingPercent}%</span>
</div>
))}
<ResetTimeIndicator resetTime={resetTime} />
<CodexResetIndicators
fiveHourResetTime={fiveHourResetAt}
weeklyResetTime={weeklyResetAt}
fallbackResetTime={resetTime}
/>
</div>
);
}
@@ -132,3 +138,40 @@ function ResetTimeIndicator({ resetTime }: { resetTime: string | null }) {
</div>
);
}
function CodexResetIndicators({
fiveHourResetTime,
weeklyResetTime,
fallbackResetTime,
}: {
fiveHourResetTime: string | null;
weeklyResetTime: string | null;
fallbackResetTime: string | null;
}) {
const hasSpecificReset = !!fiveHourResetTime || !!weeklyResetTime;
if (!hasSpecificReset && !fallbackResetTime) return null;
return (
<div className="pt-1 border-t border-border/50 space-y-1">
{fiveHourResetTime && (
<div className="flex items-center gap-1.5">
<Clock className="w-3 h-3 text-blue-400" />
<span className="text-blue-400 font-medium">
5h resets {formatResetTime(fiveHourResetTime)}
</span>
</div>
)}
{weeklyResetTime && (
<div className="flex items-center gap-1.5">
<Clock className="w-3 h-3 text-indigo-400" />
<span className="text-indigo-400 font-medium">
Weekly resets {formatResetTime(weeklyResetTime)}
</span>
</div>
)}
{!hasSpecificReset && fallbackResetTime && (
<ResetTimeIndicator resetTime={fallbackResetTime} />
)}
</div>
);
}
+22
View File
@@ -183,12 +183,34 @@ export interface CodexQuotaWindow {
resetAt: string | null;
}
/** Core Codex usage window (5h/weekly) extracted from raw windows */
export interface CodexCoreUsageWindow {
/** Source window label */
label: string;
/** Percentage remaining (0-100) */
remainingPercent: number;
/** Seconds until quota resets, null if unknown */
resetAfterSeconds: number | null;
/** ISO timestamp when quota resets, null if unknown */
resetAt: string | null;
}
/** Core Codex usage summary with explicit 5h and weekly windows */
export interface CodexCoreUsageSummary {
/** Short-cycle usage limit window (typically 5h) */
fiveHour: CodexCoreUsageWindow | null;
/** Long-cycle usage limit window (typically weekly) */
weekly: CodexCoreUsageWindow | null;
}
/** Codex quota result */
export interface CodexQuotaResult {
/** Whether fetch succeeded */
success: boolean;
/** Quota windows (primary, secondary, code review) */
windows: CodexQuotaWindow[];
/** Explicit core usage windows (5h + weekly) for easier reset display */
coreUsage?: CodexCoreUsageSummary;
/** Plan type: free, plus, team, or null if unknown */
planType: 'free' | 'plus' | 'team' | null;
/** Timestamp of fetch */