mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-18 21:02:50 +03:00
* fix(codex): fail the stream when the websocket closes before a terminal event * docs(changelog): add fragment for Codex websocket premature-close fix * chore(quality): rebaseline file-size baselines for #12737 test and executor growth * fix(codex): log websocket failures and harden premature-close tests Review follow-up: failController now logs the failure (code + message) via nextInput.log, and onclose surfaces the WS close code/reason in the log line (the public payload stays sanitized through the allowlist). Adds regression tests for the onerror-before-onclose sequence and a close with zero prior events. * chore(quality): re-measure the #12737 codex.ts file-size rebaseline after the release merge The PR's own annotation was written against base 1505 and the frozen cap was already at 1528 on release/v3.8.51 (which grew the file independently). With this PR's +13 lines the merged file measures 1530, so the frozen entry moves 1528->1530 (+2 of genuine PR growth; the remaining 11 lines fit the headroom the tip already had). No other frozen entry touched. --------- Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
112 lines
5.2 KiB
TypeScript
112 lines
5.2 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_closed", { 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 };
|
|
}
|