refactor(errors): share api error payload parsing

This commit is contained in:
Vincent Koc 2026-03-20 09:29:38 -07:00
parent dbc9d3dd70
commit d3ffa1e4e7
3 changed files with 15 additions and 61 deletions

View File

@ -4,7 +4,9 @@ import {
BILLING_ERROR_USER_MESSAGE,
formatBillingErrorMessage,
formatAssistantErrorText,
getApiErrorPayloadFingerprint,
formatRawAssistantErrorForUi,
isRawApiErrorPayload,
} from "./pi-embedded-helpers.js";
import { makeAssistantMessageFixture } from "./test-helpers/assistant-message-fixtures.js";
@ -159,3 +161,14 @@ describe("formatRawAssistantErrorForUi", () => {
);
});
});
describe("raw API error payload helpers", () => {
it("recognizes provider-prefixed JSON payloads for observation fingerprints", () => {
const raw =
'Ollama API error: {"type":"error","error":{"type":"server_error","message":"Boom"},"request_id":"req_123"}';
expect(isRawApiErrorPayload(raw)).toBe(true);
expect(getApiErrorPayloadFingerprint(raw)).toContain("server_error");
expect(getApiErrorPayloadFingerprint(raw)).toContain("req_123");
});
});

View File

@ -5,6 +5,7 @@ import {
extractLeadingHttpStatus,
formatRawAssistantErrorForUi,
isCloudflareOrHtmlErrorPage,
parseApiErrorPayload,
} from "../../shared/assistant-error-format.js";
export {
extractLeadingHttpStatus,
@ -223,9 +224,6 @@ export function extractObservedOverflowTokenCount(errorMessage?: string): number
return undefined;
}
// Allow provider-wrapped API payloads such as "Ollama API error 400: {...}".
const ERROR_PAYLOAD_PREFIX_RE =
/^(?:error|(?:[a-z][\w-]*\s+)?api\s*error|apierror|openai\s*error|anthropic\s*error|gateway\s*error)(?:\s+\d{3})?[:\s-]+/i;
const FINAL_TAG_RE = /<\s*\/?\s*final\s*>/gi;
const ERROR_PREFIX_RE =
/^(?:error|(?:[a-z][\w-]*\s+)?api\s*error|openai\s*error|anthropic\s*error|gateway\s*error|request failed|failed|exception)(?:\s+\d{3})?[:\s-]+/i;
@ -482,63 +480,6 @@ function shouldRewriteContextOverflowText(raw: string): boolean {
);
}
type ErrorPayload = Record<string, unknown>;
function isErrorPayloadObject(payload: unknown): payload is ErrorPayload {
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
return false;
}
const record = payload as ErrorPayload;
if (record.type === "error") {
return true;
}
if (typeof record.request_id === "string" || typeof record.requestId === "string") {
return true;
}
if ("error" in record) {
const err = record.error;
if (err && typeof err === "object" && !Array.isArray(err)) {
const errRecord = err as ErrorPayload;
if (
typeof errRecord.message === "string" ||
typeof errRecord.type === "string" ||
typeof errRecord.code === "string"
) {
return true;
}
}
}
return false;
}
function parseApiErrorPayload(raw: string): ErrorPayload | null {
if (!raw) {
return null;
}
const trimmed = raw.trim();
if (!trimmed) {
return null;
}
const candidates = [trimmed];
if (ERROR_PAYLOAD_PREFIX_RE.test(trimmed)) {
candidates.push(trimmed.replace(ERROR_PAYLOAD_PREFIX_RE, "").trim());
}
for (const candidate of candidates) {
if (!candidate.startsWith("{") || !candidate.endsWith("}")) {
continue;
}
try {
const parsed = JSON.parse(candidate) as unknown;
if (isErrorPayloadObject(parsed)) {
return parsed;
}
} catch {
// ignore parse errors
}
}
return null;
}
export function getApiErrorPayloadFingerprint(raw?: string): string | null {
if (!raw) {
return null;

View File

@ -41,7 +41,7 @@ function isErrorPayloadObject(payload: unknown): payload is ErrorPayload {
return false;
}
function parseApiErrorPayload(raw: string): ErrorPayload | null {
export function parseApiErrorPayload(raw?: string): ErrorPayload | null {
if (!raw) {
return null;
}