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

This commit is contained in:
Tam Nhu Tran committed 2026-02-15 09:48:39 +07:00
1 parent b40fe866ec
commit 4fc19c4390
8 files changed
+329 -8

No files matched your search

+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}`
);