Files
OmniRoute/open-sse/utils/codexPublicError.ts
Diego Rodrigues de Sa e Souza 2f6fdf16c7 fix(codex): close response failure boundary (#12444)
Validado em lote numa worktree combinada com os 14 PRs desta campanha de error-boundary sobre o tip de `release/v3.8.51`: `typecheck:core` limpo e **120/120** nos 23 arquivos de teste que os PRs trazem.

Um ponto que só apareceu no tree combinado: **#12465 e #12466 criam o mesmo arquivo novo** `open-sse/utils/streamReadiness.ts` (que não existe no tip) com desenhos divergentes de cancelamento — `cancelled` + `releaseLock` imediato num, `readInFlight`/`cancelRequested` com `cancelReader` fire-and-forget no outro. Adotei a versão do #12466, que difere e defere o release do lock para quando a leitura em voo termina, e validei a escolha rodando as suítes dos **dois** PRs contra ela: 21/21 no readiness compartilhado e 22/22 incluindo o boundary do Perplexity.
2026-09-03 20:59:14 -03:00

111 lines
5.1 KiB
TypeScript

import { sanitizeErrorMessage } from "./error.ts";
export const CODEX_PUBLIC_ERROR_MESSAGE = sanitizeErrorMessage("Codex provider request failed");
export interface CodexPublicError {
message: string;
type: string;
code: string;
}
interface CodexPublicErrorInput {
status?: number | null;
type?: unknown;
code?: unknown;
}
interface CodexPublicErrorRule {
type: string;
allowsStatus: (status: number) => boolean;
}
const exactStatuses =
(...statuses: number[]) =>
(status: number): boolean =>
statuses.includes(status);
const CODEX_PUBLIC_ERROR_RULES = new Map<string, CodexPublicErrorRule>([
["browser_stream_inconsistent", { type: "server_error", allowsStatus: exactStatuses(502) }],
["chatgpt_session_expired", { type: "authentication_error", allowsStatus: exactStatuses(401) }],
["chatgpt_submission_ambiguous", { type: "server_error", allowsStatus: exactStatuses(502) }],
["chatgpt_submitted_turn_failed", { type: "server_error", allowsStatus: exactStatuses(502) }],
["chatgpt_subscription_unavailable", { type: "server_error", allowsStatus: exactStatuses(503) }],
["client_cancelled", { type: "invalid_request_error", allowsStatus: exactStatuses(499) }],
["client_closed_request", { type: "invalid_request_error", allowsStatus: exactStatuses(499) }],
["codex_app_server_turn_failed", { type: "provider_error", allowsStatus: exactStatuses(502) }],
[
"compaction_control_unavailable",
{ type: "invalid_request_error", allowsStatus: exactStatuses(409) },
],
[
"compaction_handoff_failed",
{ type: "invalid_request_error", allowsStatus: exactStatuses(409) },
],
[
"compaction_source_unavailable",
{ type: "invalid_request_error", allowsStatus: exactStatuses(409) },
],
["connector_not_found", { type: "connector_error", allowsStatus: exactStatuses(424) }],
[
"context_length_exceeded",
{ type: "invalid_request_error", allowsStatus: exactStatuses(400, 413) },
],
["insufficient_quota", { type: "insufficient_quota", allowsStatus: exactStatuses(429) }],
["invalid_api_key", { type: "authentication_error", allowsStatus: exactStatuses(401) }],
["invalid_output_schema", { type: "invalid_request_error", allowsStatus: exactStatuses(400) }],
["invalid_request_error", { type: "invalid_request_error", allowsStatus: exactStatuses(400) }],
["multipart_protocol_violation", { type: "server_error", allowsStatus: exactStatuses(502) }],
["origin_rejected", { type: "invalid_request_error", allowsStatus: exactStatuses(403) }],
["permission_denied", { type: "permission_error", allowsStatus: exactStatuses(403) }],
["prompt_attachment_integrity", { type: "server_error", allowsStatus: exactStatuses(502) }],
["rate_limit_exceeded", { type: "rate_limit_error", allowsStatus: exactStatuses(429) }],
["server_is_overloaded", { type: "server_error", allowsStatus: exactStatuses(503) }],
[
"structured_output_validation_failed",
{ type: "server_error", allowsStatus: exactStatuses(502) },
],
["subscription_required", { type: "permission_error", allowsStatus: exactStatuses(403) }],
[
"upstream_server_error",
{
type: "server_error",
allowsStatus: (status) => status >= 500 && status <= 599 && status !== 503,
},
],
[
"upstream_websocket_connect_failed",
{ type: "provider_error", allowsStatus: exactStatuses(502) },
],
["upstream_websocket_error", { type: "provider_error", allowsStatus: exactStatuses(502) }],
["usage_limit_reached", { type: "rate_limit_error", allowsStatus: exactStatuses(429) }],
]);
function defaultPublicClassification(status: number): Pick<CodexPublicError, "type" | "code"> {
if (status === 429) return { type: "rate_limit_error", code: "rate_limit_exceeded" };
if (status === 401) return { type: "authentication_error", code: "invalid_api_key" };
if (status === 403) return { type: "permission_error", code: "permission_denied" };
if (status === 499) return { type: "invalid_request_error", code: "client_closed_request" };
if (status === 503) return { type: "server_error", code: "server_is_overloaded" };
if (status >= 500) return { type: "server_error", code: "upstream_server_error" };
return { type: "invalid_request_error", code: "invalid_request_error" };
}
/**
* Project an internally classified Codex failure onto its public Responses contract.
*
* Upstream message, code, and type fields are untrusted. The public message is fixed,
* while code/type retain only closed, protocol-level identifiers already produced by
* OmniRoute. Everything else falls back to the HTTP status classification.
*/
export function projectCodexPublicError(input: CodexPublicErrorInput): CodexPublicError {
const status =
typeof input.status === "number" && Number.isInteger(input.status) ? input.status : 502;
const fallback = defaultPublicClassification(status);
const rule =
typeof input.code === "string" ? CODEX_PUBLIC_ERROR_RULES.get(input.code) : undefined;
if (!rule || !rule.allowsStatus(status)) {
return { message: CODEX_PUBLIC_ERROR_MESSAGE, ...fallback };
}
return { message: CODEX_PUBLIC_ERROR_MESSAGE, type: rule.type, code: input.code as string };
}