fix(codex): reduce quota timeout flakes in dashboard

This commit is contained in:
Tam Nhu Tran
2026-02-14 06:23:31 +07:00
parent 1d2ee827fe
commit b3d9dce6e1
2 changed files with 150 additions and 97 deletions
+28 -6
View File
@@ -14,6 +14,8 @@ import type { CodexQuotaResult, CodexQuotaWindow } from './quota-types';
/** ChatGPT backend API base URL */ /** ChatGPT backend API base URL */
const CODEX_API_BASE = 'https://chatgpt.com/backend-api'; 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. * User agent matching Codex CLI for API compatibility.
@@ -222,8 +224,11 @@ export async function fetchCodexQuota(
} }
const url = `${CODEX_API_BASE}/wham/usage`; const url = `${CODEX_API_BASE}/wham/usage`;
let lastErrorMsg = 'Unknown error';
for (let attempt = 1; attempt <= CODEX_QUOTA_MAX_ATTEMPTS; attempt++) {
const controller = new AbortController(); const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000); const timeoutId = setTimeout(() => controller.abort(), CODEX_QUOTA_TIMEOUT_MS);
try { try {
const response = await fetch(url, { const response = await fetch(url, {
@@ -238,7 +243,7 @@ export async function fetchCodexQuota(
clearTimeout(timeoutId); clearTimeout(timeoutId);
if (verbose) console.error(`[i] Codex API status: ${response.status}`); if (verbose) console.error(`[i] Codex API status: ${response.status} (attempt ${attempt})`);
if (response.status === 401) { if (response.status === 401) {
return { return {
@@ -312,26 +317,43 @@ export async function fetchCodexQuota(
}; };
} catch (err) { } catch (err) {
clearTimeout(timeoutId); clearTimeout(timeoutId);
const errorMsg = const isAbortError = err instanceof Error && err.name === 'AbortError';
err instanceof Error && err.name === 'AbortError' lastErrorMsg = isAbortError
? 'Request timeout' ? 'Request timeout'
: err instanceof Error : err instanceof Error
? err.message ? err.message
: 'Unknown error'; : 'Unknown error';
if (verbose) console.error(`[!] Codex quota error: ${errorMsg}`); if (verbose) {
console.error(`[!] Codex quota error (attempt ${attempt}): ${lastErrorMsg}`);
}
// Retry timeout once; other failures return immediately.
if (isAbortError && attempt < CODEX_QUOTA_MAX_ATTEMPTS) {
continue;
}
return { return {
success: false, success: false,
windows: [], windows: [],
planType: null, planType: null,
lastUpdated: Date.now(), lastUpdated: Date.now(),
error: errorMsg, error: lastErrorMsg,
accountId, accountId,
}; };
} }
} }
return {
success: false,
windows: [],
planType: null,
lastUpdated: Date.now(),
error: lastErrorMsg,
accountId,
};
}
/** /**
* Fetch quota for all Codex accounts * Fetch quota for all Codex accounts
* *
+35 -4
View File
@@ -46,6 +46,37 @@ import { loadOrCreateUnifiedConfig } from '../../config/unified-config-loader';
const router = Router(); 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 */ /** Get configured backend from config */
function getConfiguredBackend() { function getConfiguredBackend() {
try { try {
@@ -548,8 +579,8 @@ router.get('/quota/codex/:accountId', async (req: Request, res: Response): Promi
// Fetch from external API // Fetch from external API
const result = await fetchCodexQuota(accountId); const result = await fetchCodexQuota(accountId);
// Cache successful results (don't cache errors that need reauth) // Cache successful and stable failure states; skip transient network failures.
if (result.success || !result.needsReauth) { if (shouldCacheCodexQuotaResult(result)) {
setCachedQuota('codex', accountId, result); setCachedQuota('codex', accountId, result);
} }
@@ -589,8 +620,8 @@ router.get('/quota/gemini/:accountId', async (req: Request, res: Response): Prom
// Fetch from external API // Fetch from external API
const result = await fetchGeminiCliQuota(accountId); const result = await fetchGeminiCliQuota(accountId);
// Cache successful results (don't cache errors that need reauth) // Cache successful and stable failure states; skip transient network failures.
if (result.success || !result.needsReauth) { if (shouldCacheGeminiQuotaResult(result)) {
setCachedQuota('gemini', accountId, result); setCachedQuota('gemini', accountId, result);
} }