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
+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,
};
}
/**
+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);
}