Files
OmniRoute/open-sse/utils/upstreamErrorPassthrough.ts
Diego Rodrigues de Sa e Souza 5be61dbcd6 feat(sse): relay upstream 4xx error bodies verbatim on the Anthropic request path (#8622)
* feat(sse): selective upstream 4xx error passthrough util

* feat(sse): relay upstream 4xx error bodies verbatim on the Anthropic request path

createErrorResult() gains an opt-in 7th param opts.passthrough; when set and
the upstream body is eligible (per shouldPassthroughUpstreamError), the
returned response body is the upstream 4xx body verbatim instead of the
sanitized wrapper. Internal classification fields (error/rawMessage/
errorType/errorCode) are never affected.

Wired into chatCore.ts's 7 upstream-error createErrorResult call sites
(model_unavailable / context_overflow / generic upstream-error branches),
gated on sourceFormat === FORMATS.CLAUDE so only /v1/messages requests get
the verbatim body — OpenAI-format paths are unchanged.

* test(quality): register upstream-error-passthrough in stryker tap.testFiles

`tests/unit/upstream-error-passthrough.test.ts` covers `open-sse/utils/error.ts`,
an instrumented module, so `check:mutation-test-coverage --strict` requires it in
`tap.testFiles` — the gate flagged the drift on this PR.
2026-07-26 16:30:52 -03:00

39 lines
1.7 KiB
TypeScript

/**
* Selective upstream 4xx error passthrough (Claude Code auto-recover contract).
*
* Claude Code matches the upstream error WORDING to auto-disable capabilities
* (thinking / output_config) for the rest of the conversation. Wrapping the body
* via buildErrorBody() truncates the message and breaks that recovery. For
* upstream-originated 4xx errors the body is the provider's public API message —
* not our internals — so it is safe and required to relay it verbatim.
* OmniRoute-generated errors MUST keep using buildErrorBody() (Hard Rule #12).
*/
const PASSTHROUGH_MIN = 400;
const PASSTHROUGH_MAX = 499;
// 401/403/407: auth-adjacent — our own credential context may leak via provider
// echoes; keep those sanitized. 400/404/408/413/422/429 carry the capability and
// quota wording the client needs.
const EXCLUDED_STATUSES = new Set([401, 403, 407]);
const INTERNAL_LEAK_RE = /\sat\s\/|node_modules|omniroute\//i;
export function shouldPassthroughUpstreamError(statusCode: number, upstreamBody: unknown): boolean {
if (statusCode < PASSTHROUGH_MIN || statusCode > PASSTHROUGH_MAX) return false;
if (EXCLUDED_STATUSES.has(statusCode)) return false;
if (!upstreamBody || typeof upstreamBody !== "object") return false;
const text = JSON.stringify(upstreamBody);
if (INTERNAL_LEAK_RE.test(text)) return false;
return true;
}
export function buildPassthroughErrorResponse(
statusCode: number,
upstreamBody: unknown,
headers?: Record<string, string>
): Response | null {
if (!shouldPassthroughUpstreamError(statusCode, upstreamBody)) return null;
return new Response(JSON.stringify(upstreamBody), {
status: statusCode,
headers: { "Content-Type": "application/json", ...(headers || {}) },
});
}