/** * One classified error for every provider failure. * * Before this, a failure from any of the seven drivers reached the caller as an * opaque string, so nothing downstream could decide whether it was worth * retrying, whether it was the caller's fault, or whether the context window had * simply run out. Two drivers built that string by interpolating the response * body; three more inherited a vendor SDK error whose message IS the response * body. Either way, a credential the upstream echoed back landed in a message * that gets logged. * * So the contract here is deliberately narrow: * * - the message is built from the STATUS LINE, the classified `kind`, and the * provider's own complaint in `detail` — truncated and scrubbed of anything * credential-shaped. The raw body is never re-thrown and never attached as * `cause`; a `cause` survives every logger that serializes an error chain, * which defeats the point. * * The body used to be dropped entirely. That was over-corrected: a provider * rejecting a request names the exact offending field, and deleting that * sentence turned a one-line diagnosis into hypothesis elimination against a * live API — once at the cost of a day of production downtime, while the * wire had been saying `tools.0.custom.input_schema: … must match JSON * Schema draft 2020-12` the entire time. Scrubbing what looks like a * credential keeps the safety and returns the sentence. * - `retryAfterMs` is DATA. Nothing in this module sleeps, backs off or * retries. A retry loop inside a driver burns the turn's wall clock and hides * the failure from the layer that should decide. */ import type { ProviderErrorKind, ProviderRequestErrorInit } from '../types/provider/error.js'; export type { ProviderErrorInfo, ProviderErrorKind, ProviderRequestErrorInit, } from '../types/provider/error.js'; /** * A provider request that failed, classified. * * `name` is set explicitly rather than inherited, because the classifier is * matched structurally across a package boundary (a driver in one package throws * it; the runtime in another reads it) and `instanceof` is unreliable when two * copies of the SDK end up in one process. */ export declare class ProviderRequestError extends Error { readonly kind: ProviderErrorKind; readonly providerId: string; readonly providerCode?: string; readonly status?: number; readonly retryAfterMs?: number; /** * What the provider said was wrong, truncated and redacted. * * `ProviderRequestErrorInit` has declared this field all along and the * constructor never read it, so every caller that set it was writing to * nothing. That is not a cosmetic gap: a provider rejecting a request * usually names the exact offending field, and losing that sentence turns * a one-line diagnosis into hypothesis elimination against a live API. It * did — a tool schema in the wrong JSON Schema dialect cost a day of * production downtime while the wire had been saying * `tools.0.custom.input_schema: … must match JSON Schema draft 2020-12` * the whole time. * * See {@link vendorDetail} for what is kept and what is scrubbed. */ readonly detail?: string; constructor(init: ProviderRequestErrorInit); } /** * Credential shapes to scrub before a provider's words are kept. * * The original decision to discard the body outright was not paranoia — an * error body can echo the request, and a request can carry a key. The answer * is to scrub what looks like a credential rather than to throw away the * sentence that names the broken field. * * The pattern set itself now lives in `constants/secret-patterns.ts`, as * `LOG_SECRET_PATTERNS` — the union of this table and the narrower one * `runtime/query/guardrail-presets.ts` matches model OUTPUT against. A * false positive here only redacts a word out of a diagnostic `detail` * nobody reads as the credential itself, which is why this call site gets * the wider set and the output guardrail does not. */ /** * The provider's own account of what was wrong, safe to log. * * Prefers the structured `error.message` a JSON body carries, because that is * the field vendors put the actionable sentence in and it is bounded; falls * back to the raw text. Truncated, and every credential shape replaced. */ export declare function vendorDetail(body: unknown): string | undefined; export declare function redactSecrets(text: string): string; /** Is this a classified provider failure, whichever SDK copy threw it? */ export declare function isProviderRequestError(err: unknown): err is ProviderRequestError; /** * Did the caller's own AbortSignal terminate this request? * * Provider SDKs do not agree on the object they reject with: some preserve * `signal.reason`, while others replace it with `AbortError` or * `APIUserAbortError`. Reclassifying any of those as a network failure breaks * the runtime's Stop/cancel path, which depends on the abort escaping the * provider boundary. * * The signal must itself be aborted. That condition distinguishes a caller Stop * from an SDK timeout which may also use an `AbortError`-shaped rejection. */ export declare function isCallerAbortError(error: unknown, signal?: AbortSignal): boolean; /** Does this response body say the request did not fit the window? */ export declare function bodySaysContextOverflow(body: string | undefined | null): boolean; /** * `Retry-After` in milliseconds. The header is either delta-seconds or an * HTTP-date; both are specified, and vendors use both. * * Returns undefined for anything unparseable or for a date already in the past — * a negative delay is worse than none, because a caller would treat it as * "retry immediately" against a provider that just asked it to wait. */ export declare function parseRetryAfterMs(headerValue: string | null | undefined, now?: number): number | undefined; /** * Classify an HTTP failure. `body` is used ONLY to separate a context overflow * from an ordinary bad request, and is not retained. */ export declare function classifyProviderHttpStatus(status: number, body?: string | null): ProviderErrorKind; /** * Classify an error thrown by a vendor SDK and replace it. * * This exists because wrapping our OWN `!response.ok` throws is not enough. The * Anthropic, OpenAI and ollama clients each build their error message FROM the * response body, so a credential the upstream echoed back is already inside * `err.message` before our code sees it. Proven with a planted fake token on all * three. * * So the vendor error is read for its status and scanned for an overflow * signature, and then **dropped entirely** — not re-thrown, not wrapped, and not * attached as `cause`. A `cause` is exactly what a structured logger walks, so * keeping one for debuggability would reintroduce the leak it is meant to close. * * `utils/log/exception.ts` now walks a `cause` chain for whatever DOES carry * one, to put `exception.stacktrace` in a log record instead of just the * caught error's own message. That mapper existing is not a reason to start * attaching one here: every one of the six drivers in `packages/providers/*` * (openai, anthropic, ollama, bedrock, openrouter, lmstudio) throws through * this function specifically because none of them can otherwise avoid * handing a structured logger the credential a vendor SDK just echoed back. * Each driver's own `error-taxonomy.test.ts` asserts `'cause' in (err as * object)` is `false` on ITS OWN thrown error, not just on this function in * isolation — that is what would catch a driver that stops going through * `providerVendorError`/`providerHttpError` and attaches one directly. * `packages/sdk/src/provider/__tests__/errors.test.ts` pins the same thing * against this shared function directly. See "index". * * `name` is checked too, because AWS models its failures as distinct classes * (`ThrottlingException`, `ValidationException`, `AccessDeniedException`) rather * than as status codes. */ export declare function providerVendorError(input: { readonly providerId: string; readonly error: unknown; readonly retryAfter?: string | null; readonly now?: number; }): ProviderRequestError; /** * Build a classified error from a failed HTTP response. * * Callers pass the body they already read for classification; this function * does not return it, store it, or put it in the message. */ export declare function providerHttpError(input: { readonly providerId: string; readonly status: number; readonly body?: string | null; readonly retryAfter?: string | null; readonly now?: number; }): ProviderRequestError; //# sourceMappingURL=errors.d.ts.map