- Updated markAccountUnavailable function to accept resetsAtMs for precise cooldown management.

- Added email backfill functionality for Codex OAuth connections to improve account information accuracy.
This commit is contained in:
decolua
2026-04-24 11:36:16 +07:00
parent fd8163e26e
commit 030fb34f88
15 changed files with 628 additions and 132 deletions

View File

@@ -121,6 +121,31 @@ export class CodexExecutor extends BaseExecutor {
return super.execute(args);
}
// Parse Codex usage_limit_reached to extract precise resetsAtMs; fallback to default otherwise
parseError(response, bodyText) {
if (response.status === 429 && bodyText) {
try {
const json = JSON.parse(bodyText);
const err = json?.error;
if (err?.type === "usage_limit_reached") {
const now = Date.now();
let resetsAtMs = null;
if (typeof err.resets_at === "number" && err.resets_at > 0) {
const ms = err.resets_at * 1000;
if (ms > now) resetsAtMs = ms;
}
if (!resetsAtMs && typeof err.resets_in_seconds === "number" && err.resets_in_seconds > 0) {
resetsAtMs = now + err.resets_in_seconds * 1000;
}
if (resetsAtMs) {
return { status: 429, message: err.message || bodyText, resetsAtMs };
}
}
} catch { /* fall through to default */ }
}
return super.parseError(response, bodyText);
}
/**
* Transform request before sending - inject default instructions if missing.
* Image fetching is handled separately in prefetchImages() so this stays sync.

View File

@@ -197,7 +197,7 @@ export async function handleChatCore({ body, modelInfo, credentials, log, onCred
// Provider returned error
if (!providerResponse.ok) {
trackPendingRequest(model, provider, connectionId, false, true);
const { statusCode, message } = await parseUpstreamError(providerResponse);
const { statusCode, message, resetsAtMs } = await parseUpstreamError(providerResponse, executor);
appendRequestLog({ model, provider, connectionId, status: `FAILED ${statusCode}` }).catch(() => {});
saveRequestDetail(buildRequestDetail({
provider, model, connectionId,
@@ -212,7 +212,7 @@ export async function handleChatCore({ body, modelInfo, credentials, log, onCred
const errMsg = formatProviderError(new Error(message), provider, model, statusCode);
console.log(`${COLORS.red}[ERROR] ${errMsg}${COLORS.reset}`);
reqLogger.logError(new Error(message), finalBody || translatedBody);
return createErrorResult(statusCode, errMsg);
return createErrorResult(statusCode, errMsg, resetsAtMs);
}
const sharedCtx = { provider, model, body, stream, translatedBody, finalBody, requestStartTime, connectionId, apiKey, clientRawRequest, onRequestSuccess };

View File

@@ -52,44 +52,55 @@ export async function writeStreamError(writer, statusCode, message) {
/**
* Parse upstream provider error response
* @param {Response} response - Fetch response from provider
* @returns {Promise<{statusCode: number, message: string}>}
* @param {object} [executor] - Optional executor with parseError() override for provider-specific parsing
* @returns {Promise<{statusCode: number, message: string, resetsAtMs?: number}>}
*/
export async function parseUpstreamError(response) {
let message = "";
export async function parseUpstreamError(response, executor = null) {
let bodyText = "";
try {
const text = await response.text();
try {
const json = JSON.parse(text);
message = json.error?.message || json.message || json.error || text;
} catch {
message = text;
}
bodyText = await response.text();
} catch {
message = `Upstream error: ${response.status}`;
bodyText = "";
}
// Let executor-specific parser extract provider-specific fields (e.g. codex resetsAtMs)
if (executor && typeof executor.parseError === "function") {
try {
const parsed = executor.parseError(response, bodyText);
if (parsed && typeof parsed === "object") {
const msg = parsed.message || DEFAULT_ERROR_MESSAGES[response.status] || `Upstream error: ${response.status}`;
return { statusCode: parsed.status || response.status, message: msg, resetsAtMs: parsed.resetsAtMs };
}
} catch { /* fall through to default parsing */ }
}
let message = "";
try {
const json = JSON.parse(bodyText);
message = json.error?.message || json.message || json.error || bodyText;
} catch {
message = bodyText;
}
const messageStr = typeof message === "string" ? message : JSON.stringify(message);
const finalMessage = messageStr || DEFAULT_ERROR_MESSAGES[response.status] || `Upstream error: ${response.status}`;
return {
statusCode: response.status,
message: finalMessage
};
return { statusCode: response.status, message: finalMessage };
}
/**
* Create error result for chatCore handler
* @param {number} statusCode - HTTP status code
* @param {string} message - Error message
* @returns {{ success: false, status: number, error: string, response: Response }}
* @param {number} [resetsAtMs] - Optional precise cooldown expiry (ms epoch) for provider-specific quota errors
* @returns {{ success: false, status: number, error: string, response: Response, resetsAtMs?: number }}
*/
export function createErrorResult(statusCode, message) {
export function createErrorResult(statusCode, message, resetsAtMs) {
return {
success: false,
status: statusCode,
error: message,
resetsAtMs,
response: errorResponse(statusCode, message)
};
}