{"version":3,"file":"stream-failure.d.ts","sourceRoot":"","sources":["../../src/utils/stream-failure.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAGpD;;;;GAIG;AAEH,MAAM,MAAM,iBAAiB,GAC1B,SAAS,GACT,QAAQ,GACR,YAAY,GACZ,YAAY,GACZ,cAAc,GACd,MAAM,GACN,iBAAiB,GACjB,oBAAoB,GACpB,SAAS,CAAC;AAEb,MAAM,WAAW,iBAAiB;IACjC,IAAI,EAAE,iBAAiB,CAAC;IACxB,iFAAiF;IACjF,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uDAAuD;IACvD,GAAG,CAAC,EAAE,MAAM,CAAC;CACb;AAED,qBAAa,kBAAmB,SAAQ,KAAK;IAC5C,QAAQ,CAAC,IAAI,EAAE,iBAAiB,CAAC;IAEjC,YAAY,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,iBAAiB,EAInD;CACD;AAcD,4GAA4G;AAC5G,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,iBAAiB,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CASrF;AAED,wBAAgB,qBAAqB,CAAC,iBAAiB,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,iBAAiB,CAsBpG;AAED;;;;GAIG;AACH,wBAAgB,2BAA2B,CAC1C,aAAa,EAAE,MAAM,GAAG,SAAS,EACjC,KAAK,CAAC,EAAE,IAAI,CAAC,iBAAiB,EAAE,WAAW,CAAC,GAC1C,kBAAkB,CAWpB;AAID,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEtD;AA2DD;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,OAAO,GAAG,iBAAiB,CAE1E;AAED;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAOjE;AAID;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAClC,KAAK,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,EACpD,MAAM,EAAE,gBAAgB,EACxB,KAAK,EAAE,OAAO,GACZ,IAAI,CAsBN","sourcesContent":["import { getLogger } from \"../log.js\";\nimport type { AssistantMessage } from \"../types.js\";\nimport { appendAssistantMessageDiagnostic, extractDiagnosticError } from \"./diagnostics.js\";\n\n/**\n * Shared classification and reporting for provider stream failures, so no\n * provider collapses a specific cause (refusal, safety filter, overload, ...)\n * into a generic string before it is logged and persisted.\n */\n\nexport type StreamFailureKind =\n\t| \"refusal\"\n\t| \"safety\"\n\t| \"overloaded\"\n\t| \"rate_limit\"\n\t| \"server_error\"\n\t| \"auth\"\n\t| \"invalid_request\"\n\t| \"malformed_response\"\n\t| \"unknown\";\n\nexport interface StreamFailureInfo {\n\tkind: StreamFailureKind;\n\t/** Provider's own error/stop identifier, e.g. \"overloaded_error\" or \"SAFETY\". */\n\tproviderErrorType?: string;\n\tstatus?: number;\n\trequestId?: string;\n\t/** Truncated raw provider payload for post-mortems. */\n\traw?: string;\n}\n\nexport class StreamFailureError extends Error {\n\treadonly info: StreamFailureInfo;\n\n\tconstructor(message: string, info: StreamFailureInfo) {\n\t\tsuper(message);\n\t\tthis.name = \"StreamFailureError\";\n\t\tthis.info = info;\n\t}\n}\n\nconst KIND_MESSAGES: Record<StreamFailureKind, string> = {\n\trefusal: \"Model refused to respond\",\n\tsafety: \"Response blocked by provider safety filters\",\n\toverloaded: \"Provider overloaded\",\n\trate_limit: \"Provider rate limit exceeded\",\n\tserver_error: \"Provider server error\",\n\tauth: \"Provider authentication failed\",\n\tinvalid_request: \"Provider rejected the request\",\n\tmalformed_response: \"Provider returned a malformed response\",\n\tunknown: \"Provider stream failed\",\n};\n\n/** Build a user-facing message like \"Provider overloaded (overloaded_error, 529) [request_id: req_abc]\". */\nexport function streamFailureMessage(info: StreamFailureInfo, detail?: string): string {\n\tconst qualifiers = [info.providerErrorType, info.status !== undefined ? String(info.status) : undefined]\n\t\t.filter(Boolean)\n\t\t.join(\", \");\n\tlet message = KIND_MESSAGES[info.kind];\n\tif (qualifiers) message += ` (${qualifiers})`;\n\tif (detail) message += `: ${detail}`;\n\tif (info.requestId) message += ` [request_id: ${info.requestId}]`;\n\treturn message;\n}\n\nexport function classifyStreamFailure(providerErrorType?: string, status?: number): StreamFailureKind {\n\tconst type = providerErrorType?.toLowerCase() ?? \"\";\n\tif (type === \"refusal\") return \"refusal\";\n\tif (/sensitive|safety|prohibited_content|blocklist|spii|recitation|content.?filter|guardrail|flagged/.test(type)) {\n\t\treturn \"safety\";\n\t}\n\tif (type.includes(\"overloaded\") || status === 529) return \"overloaded\";\n\tif (type.includes(\"rate_limit\") || type.includes(\"throttl\") || status === 429) return \"rate_limit\";\n\tif (/authentication|permission|unauthorized/.test(type) || status === 401 || status === 403) return \"auth\";\n\tif (type.includes(\"invalid_request\") || type.includes(\"not_found_error\") || status === 400 || status === 404) {\n\t\treturn \"invalid_request\";\n\t}\n\tif (type.includes(\"malformed\")) return \"malformed_response\";\n\tif (\n\t\ttype.includes(\"api_error\") ||\n\t\ttype.includes(\"server_error\") ||\n\t\ttype.includes(\"unavailable\") ||\n\t\t(status !== undefined && status >= 500)\n\t) {\n\t\treturn \"server_error\";\n\t}\n\treturn \"unknown\";\n}\n\n/**\n * Failure for a stream that terminated with a provider stop/finish reason that\n * maps to \"error\" (e.g. Anthropic \"refusal\", Gemini \"SAFETY\"). Providers call\n * this instead of throwing a generic error, so the raw reason survives.\n */\nexport function streamFailureFromStopReason(\n\trawStopReason: string | undefined,\n\textra?: Pick<StreamFailureInfo, \"requestId\">,\n): StreamFailureError {\n\tconst info: StreamFailureInfo = {\n\t\tkind: rawStopReason ? classifyStreamFailure(rawStopReason) : \"unknown\",\n\t\tproviderErrorType: rawStopReason,\n\t\trequestId: extra?.requestId,\n\t};\n\tif (info.kind === \"unknown\" && /malformed/i.test(rawStopReason ?? \"\")) info.kind = \"malformed_response\";\n\tconst message = rawStopReason\n\t\t? streamFailureMessage(info)\n\t\t: streamFailureMessage(info, \"stream ended with an error and no stop reason\");\n\treturn new StreamFailureError(message, info);\n}\n\nconst MAX_RAW_LENGTH = 2000;\n\nexport function truncateRawPayload(raw: string): string {\n\treturn raw.length > MAX_RAW_LENGTH ? `${raw.slice(0, MAX_RAW_LENGTH)}…` : raw;\n}\n\nfunction extractStreamFailureParts(error: unknown): { info: StreamFailureInfo; detail?: string } {\n\tif (error instanceof StreamFailureError) return { info: error.info };\n\tif (!(error instanceof Error)) return { info: { kind: \"unknown\" } };\n\n\tconst err = error as Error & {\n\t\tstatus?: unknown;\n\t\tstatusCode?: unknown;\n\t\tcode?: unknown;\n\t\trequestID?: unknown;\n\t\trequest_id?: unknown;\n\t\theaders?: unknown;\n\t\terror?: unknown;\n\t\t$metadata?: { requestId?: unknown };\n\t};\n\n\tconst status =\n\t\ttypeof err.status === \"number\" ? err.status : typeof err.statusCode === \"number\" ? err.statusCode : undefined;\n\n\t// Error bodies come nested differently per SDK: Anthropic/OpenAI expose\n\t// `error.error = {type|code, message}` (sometimes doubly nested).\n\tlet body = err.error as { type?: unknown; code?: unknown; message?: unknown; error?: unknown } | undefined;\n\tif (body && typeof body === \"object\" && body.error && typeof body.error === \"object\") {\n\t\tbody = body.error as { type?: unknown; code?: unknown; message?: unknown };\n\t}\n\tconst bodyType = body && typeof body === \"object\" ? (body.type ?? body.code) : undefined;\n\tconst bodyMessage = body && typeof body === \"object\" ? body.message : undefined;\n\tconst providerErrorType =\n\t\ttypeof bodyType === \"string\"\n\t\t\t? bodyType\n\t\t\t: typeof err.code === \"string\"\n\t\t\t\t? err.code\n\t\t\t\t: err.name !== \"Error\" && err.name !== \"StreamFailureError\"\n\t\t\t\t\t? err.name\n\t\t\t\t\t: undefined;\n\n\tconst headers = err.headers;\n\tconst headerRequestId =\n\t\theaders && typeof (headers as Headers).get === \"function\"\n\t\t\t? ((headers as Headers).get(\"request-id\") ?? (headers as Headers).get(\"x-request-id\"))\n\t\t\t: headers && typeof headers === \"object\"\n\t\t\t\t? ((headers as Record<string, unknown>)[\"request-id\"] ??\n\t\t\t\t\t(headers as Record<string, unknown>)[\"x-request-id\"])\n\t\t\t\t: undefined;\n\tconst rawRequestId = err.requestID ?? err.request_id ?? err.$metadata?.requestId ?? headerRequestId;\n\tconst requestId = typeof rawRequestId === \"string\" ? rawRequestId : undefined;\n\n\treturn {\n\t\tinfo: {\n\t\t\tkind: classifyStreamFailure(providerErrorType ?? error.message, status),\n\t\t\tproviderErrorType,\n\t\t\tstatus,\n\t\t\trequestId,\n\t\t},\n\t\tdetail: typeof bodyMessage === \"string\" ? bodyMessage : undefined,\n\t};\n}\n\n/**\n * Best-effort extraction of structured failure info from any thrown value:\n * StreamFailureError, provider SDK errors (Anthropic/OpenAI APIError, AWS SDK\n * exceptions, Google ApiError), or plain errors.\n */\nexport function extractStreamFailureInfo(error: unknown): StreamFailureInfo {\n\treturn extractStreamFailureParts(error).info;\n}\n\n/**\n * User-facing message for a thrown stream error: a classified one-liner with\n * the provider's own short message, never the raw payload/trace. Unrecognized\n * errors pass through verbatim so their text (which downstream retry matching\n * may depend on) is preserved.\n */\nexport function formatStreamFailureMessage(error: unknown): string {\n\tif (error instanceof StreamFailureError) return error.message;\n\tconst { info, detail } = extractStreamFailureParts(error);\n\tif (info.kind === \"unknown\") {\n\t\treturn error instanceof Error ? error.message : JSON.stringify(error);\n\t}\n\treturn streamFailureMessage(info, detail);\n}\n\nconst log = getLogger(\"ai.provider\");\n\n/**\n * Record a terminal stream failure on the message (structured diagnostic that\n * persists to session JSONL) and emit one structured log line. Call from the\n * provider's terminal catch after stopReason/errorMessage are set; no-op for\n * user-initiated aborts.\n */\nexport function recordStreamFailure(\n\tmodel: { provider: string; id: string; api: string },\n\toutput: AssistantMessage,\n\terror: unknown,\n): void {\n\tif (output.stopReason !== \"error\") return;\n\tconst info = extractStreamFailureInfo(error);\n\tappendAssistantMessageDiagnostic(output, {\n\t\ttype: \"provider_stream_failure\",\n\t\ttimestamp: Date.now(),\n\t\terror: extractDiagnosticError(error),\n\t\tdetails: { ...info },\n\t});\n\tconst rawMessage = error instanceof Error ? error.message : String(error);\n\tlog.error(\"provider stream failure\", {\n\t\tprovider: model.provider,\n\t\tmodel: model.id,\n\t\tapi: model.api,\n\t\tkind: info.kind,\n\t\tproviderErrorType: info.providerErrorType,\n\t\tstatus: info.status,\n\t\trequestId: info.requestId,\n\t\tmessage: output.errorMessage,\n\t\t// errorMessage is user-facing and concise; keep the raw cause for debugging.\n\t\tcause: rawMessage === output.errorMessage ? undefined : truncateRawPayload(rawMessage),\n\t});\n}\n"]}