UNPKG

agentfootprint

Version:

The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.

755 lines • 35.7 kB
"use strict"; /** * InvokeModelGatewayProvider — Anthropic models behind a gateway that speaks the * Bedrock InvokeModel wire over plain HTTPS with an API-key header. * * Pattern: Adapter (GoF). Zero peer dependencies — uses `fetch`. * Role: Outer ring. Translates `LLMRequest`/`LLMResponse` to and from the * gateway's wire. Knows nothing about agents, skills or routing. * * ─── Why this is not `bedrock()` ──────────────────────────────────── * * `bedrock()` drives the AWS SDK: SigV4 credentials against the CONVERSE * operation. Many organisations put the models behind their own gateway * instead — one that authenticates with an API-key header and forwards the * INVOKE operation. Neither the auth nor the operation matches, and * `providerFromEnv()` has no base URL for this wire, so until this adapter an * app had to write its own (the field case that prompted it was ~450 lines). * * ─── The wire ─────────────────────────────────────────────────────── * * POST {baseUrl}/model/{modelId}/invoke complete() * POST {baseUrl}/model/{modelId}/invoke-with-response-stream stream() * headers: content-type: application/json, {apiKeyHeader}: <key> * * The body is Anthropic's Messages API with two differences: the literal * `anthropic_version: "bedrock-2023-05-31"`, and NO `model` field — the model * is in the path. Everything else (`system` as a top-level field, content * blocks, `tools` with `input_schema`, `tool_choice`) is the shape * `anthropicMessagesWire.ts` owns. * * The stream answers SSE (`data: {…}` lines, with or without `event:` lines): * the gateway has already decoded AWS's binary event-stream framing, so there * is no SDK and no event-stream parser here. * * ─── Retries are NOT here ─────────────────────────────────────────── * * This adapter makes ONE attempt per call. Every failure it raises is an * {@link InvokeModelGatewayError} carrying the two fields `withRetry`'s default * predicate reads: `status` on an HTTP refusal (429 and 5xx retried, other 4xx * not) and `retryable: false` on every failure that asking again cannot mend — * a refusal raised before any request, or a 2xx answer it could not read (a * re-send may run and bill the model again). So the default policy retries a * 429, a 5xx, a network failure and a timeout, and nothing else. Compose: * * withRetry(invokeModelGateway({ ... })) * * A retry loop inside an adapter is a second policy nobody can see or tune. * * ─── What this adapter DOES own ───────────────────────────────────── * * - The stated wait. A throttled gateway says how long to wait — a * `Retry-After` header when it feels like it, and often only in the body * ("Rate limit is exceeded. Try again in 4 seconds."). The wording is this * wire's, so it is read HERE and declared as `retryAfterMs` (header first); * `withRetry` waits max(its schedule, that), capped by its `maxDelayMs`. * - The deadline. `timeoutMs` bounds the wait for the response headers, for a * complete() body, and for EACH stream read (the first chunk and every gap * between chunks — a stream that is still talking is never cut off), and * for the response body to close. A miss * aborts the request and raises `reason: 'timeout'`, retryable; the * caller's `req.signal` still wins. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.InvokeModelGatewayProvider = exports.invokeModelGateway = exports.InvokeModelGatewayError = exports.INVOKE_MODEL_ANTHROPIC_VERSION = void 0; const contextWindow_js_1 = require("../../provider-contract/contextWindow.js"); const anthropicMessagesWire_js_1 = require("./anthropicMessagesWire.js"); const wireManifest_js_1 = require("./wireManifest.js"); const anthropicThinkingWire_js_1 = require("./anthropicThinkingWire.js"); const anthropicModels_js_1 = require("./anthropicModels.js"); const anthropicThinkingReplay_js_1 = require("./anthropicThinkingReplay.js"); const AnthropicThinkingHandler_js_1 = require("../../thinking/AnthropicThinkingHandler.js"); const retryAfter_js_1 = require("./retryAfter.js"); const linearText_js_1 = require("../../lib/linearText.js"); const abort_js_1 = require("../../lib/abort.js"); /** The literal this wire requires in the body in place of a version header. */ exports.INVOKE_MODEL_ANTHROPIC_VERSION = 'bedrock-2023-05-31'; const PROVIDER_NAME = 'invoke-model-gateway'; const DEFAULT_MAX_TOKENS = 4096; /** How much of a refusal body an error carries — enough for the gateway's reason. */ const BODY_EXCERPT_CHARS = 400; /** * Every failure `invokeModelGateway()` raises, told in words that name the * model and the fix. `reason` is the discriminator. * * `retryable` is `true` for a 429, a 5xx, a network failure and a timeout, and `false` * for every other reason, and `withRetry`'s default predicate honours the * `false` — so a refusal raised before any request (`no-key`, `no-model`, * `invalid-options`) is never repeated, and a 2xx answer it could not read * (`unreadable-response`) is never re-sent to a model that may already have * run. `status` is set ONLY for `'http-status'`. The key is never in the * message. */ class InvokeModelGatewayError extends Error { name = 'InvokeModelGatewayError'; reason; /** The model id the call was for, when one was resolved. */ modelId; /** The HTTP status, for `'http-status'` only. */ status; /** * Whether asking again can mend it: a 429, a 5xx, a network failure, or a * timeout. `withRetry`'s default predicate reads the `false`. */ retryable; /** The gateway's `Retry-After` header, in seconds, when it sent a number. */ retryAfterSeconds; /** * The wait the gateway STATED, in ms: its `Retry-After` header (seconds or * an HTTP-date) or, when there is none, its body's "try again in N * seconds". `withRetry` waits at least this long (capped by `maxDelayMs`). */ retryAfterMs; /** The first 400 characters of the gateway's refusal body. */ bodyExcerpt; /** For `'malformed-tool-args'`: the tool whose arguments did not parse. */ toolName; constructor(init) { super(`[${PROVIDER_NAME}] ${init.message}`); this.reason = init.reason; if (init.modelId !== undefined) this.modelId = init.modelId; if (init.status !== undefined) this.status = init.status; this.retryable = isTransient(init.reason, init.status); if (init.retryAfterSeconds !== undefined) this.retryAfterSeconds = init.retryAfterSeconds; if (init.retryAfterMs !== undefined) this.retryAfterMs = init.retryAfterMs; if (init.bodyExcerpt !== undefined) this.bodyExcerpt = init.bodyExcerpt; if (init.toolName !== undefined) this.toolName = init.toolName; if (init.cause !== undefined) this.cause = init.cause; } } exports.InvokeModelGatewayError = InvokeModelGatewayError; /** * The ONE owner of which failures are worth asking again: the gateway throttled * or failed (429, 5xx), or no answer came back (network, timeout). Everything * else is either refused before a request (nothing to repeat) or an answer * already given. */ function isTransient(reason, status) { if (reason === 'network' || reason === 'timeout') return true; if (reason !== 'http-status' || status === undefined) return false; return status === 429 || status >= 500; } // ─── Adapter ──────────────────────────────────────────────────────── /** * An `LLMProvider` for Anthropic models behind a gateway that speaks the * Bedrock InvokeModel wire with an API-key header. * * @example * ```ts * import { Agent } from 'agentfootprint'; * import { invokeModelGateway } from 'agentfootprint/providers'; * import { withRetry } from 'agentfootprint/resilience'; * * const provider = withRetry( * invokeModelGateway({ * baseUrl: 'https://llm-gateway.example.com/bedrock', * apiKeyHeader: 'api-key', * apiKey: process.env.GATEWAY_KEY!, * model: 'us.anthropic.claude-haiku-4-5-20251001-v1:0', * }), * ); * const agent = Agent.create({ provider, model: 'invoke-model-gateway' }).build(); * ``` */ function invokeModelGateway(options) { const baseUrl = checkedBaseUrl(options.baseUrl); const apiKeyHeader = checkedHeader(options.apiKeyHeader); checkKeySource(options.apiKey); const timeoutMs = checkedTimeout(options.timeoutMs); const defaultMaxTokens = options.defaultMaxTokens ?? DEFAULT_MAX_TOKENS; const fetchImpl = options.fetch ?? ((input, init) => fetch(input, init)); /** The ONE mode function: declared below and used for every body. */ const modeOf = (0, anthropicThinkingWire_js_1.thinkingModeWith)(options.thinkingMode); /** POST one request; a non-2xx becomes a typed error before anything is read. */ async function post(req, operation, deadline, hooks) { const modelId = modelIdFor(req, options.model); // Built BEFORE the key is read: a request the model cannot take is // refused with nothing looked up and nothing sent. const preparation = (0, anthropicThinkingReplay_js_1.prepareAnthropicReplay)(req, modelId, PROVIDER_NAME); const body = buildInvokeBody(preparation.request, modelId, modeOf, defaultMaxTokens, options.parallelToolCalls); const bindingFactory = (0, anthropicThinkingReplay_js_1.thinkingReplayBinding)(body, modelId); // Capture and send the same body even if caller-owned nested values change // while an asynchronous key source is resolving. const serializedBody = JSON.stringify(body); if (preparation.omissions.length > 0) hooks?.onReplayPrepared?.(preparation); // A key source can take a while; a call cancelled meanwhile sends nothing // (lib/abort.ts, law 1). const key = await (0, abort_js_1.untilAborted)(keyFor(options.apiKey, modelId), req.signal); (0, abort_js_1.throwIfAborted)(req.signal); let response; try { response = await deadline.within(fetchImpl(`${baseUrl}/model/${encodeURIComponent(modelId)}/${operation}`, { method: 'POST', headers: { 'content-type': 'application/json', [apiKeyHeader]: key }, body: serializedBody, ...(deadline.signal && { signal: deadline.signal }), }), 'the response'); } catch (err) { throw networkError(err, modelId, req.signal); } if (!response.ok) throw await deadline.within(statusError(response, modelId), 'the response'); return { response, body, bindingFactory, omissions: preparation.omissions }; } return { name: PROVIDER_NAME, // `carriesInMessages` is deliberately ABSENT. The default is // ['user', 'assistant'] (DEFAULT_CARRIES_IN_MESSAGES), which is exactly // this wire: `system` rides the top-level field and a `role: 'system'` // message is dropped by `toAnthropicMessages`. Declaring more would let a // system-role injection vanish between the recording and the request // instead of being refused at run start, by name. // // `tool_choice: { type: 'tool', name }` is part of the Anthropic body this // wire forwards, and a field deployment verified the gateway honours it. carriesForcedToolChoice: true, supportsForcedToolChoice: (model) => (0, anthropicModels_js_1.anthropicTakesForcedToolChoice)(gatewayModelOf(model, options.model)), // `promptCaching` is deliberately ABSENT, for the same reason the line // above is present: a capability is declared where it is true of the // ENDPOINT. This adapter builds the body `anthropic()` builds, so it // writes `cache_control` wherever markers point — but no deployment has // verified that a gateway forwards the field to InvokeModel, and until // one does the agent sends it none. An operator who has verified theirs // declares it on the instance: // { ...invokeModelGateway(opts), promptCaching: { mode: 'breakpoints', maxBreakpoints: 4, reportsUsage: true } } // // The thinking handler IS declared: the response is Anthropic's, parsed // by `fromAnthropicResponse`, which passes thinking blocks through as // `rawThinking`. Matched by name until now, this adapter never had one — // so its signed blocks were never normalized for the echo back. thinkingHandler: AnthropicThinkingHandler_js_1.anthropicThinkingHandler, // Which thinking request each model takes (anthropicThinkingWire.ts). The // gateway's ids are Bedrock's (`us.anthropic.claude-…-v1:0`); the table // reads them from `claude-` on, like the Claude API id. thinkingMode: (model) => modeOf(gatewayModelOf(model, options.model)), async complete(req, hooks) { (0, abort_js_1.throwIfAborted)(req.signal); const modelId = modelIdFor(req, options.model); const deadline = deadlineFor(timeoutMs, req.signal, modelId); try { const { response, body, bindingFactory, omissions } = await post(req, 'invoke', deadline, hooks); const message = await deadline.within(readMessage(response, modelId), 'the response body'); return { ...(0, anthropicMessagesWire_js_1.fromAnthropicResponse)(message, message.id ? bindingFactory(message.id) : undefined), wireManifest: (0, wireManifest_js_1.toolManifestOf)(body.tools), ...(omissions.length > 0 && { replayOmissions: omissions }), }; } finally { deadline.dispose(); } }, async *stream(req, hooks) { (0, abort_js_1.throwIfAborted)(req.signal); const modelId = modelIdFor(req, options.model); const deadline = deadlineFor(timeoutMs, req.signal, modelId); try { const { response, body, bindingFactory, omissions } = await post(req, 'invoke-with-response-stream', deadline, hooks); if (!response.body) { throw new InvokeModelGatewayError({ reason: 'stream', modelId, message: `the stream for model ${modelId} answered ${response.status} with no body.`, }); } yield* (0, anthropicMessagesWire_js_1.assembleAnthropicStream)(readGatewayEvents(response.body, modelId, deadline), { bindingFactory, replayOmissions: omissions, wireManifest: (0, wireManifest_js_1.toolManifestOf)(body.tools), onMalformedToolArgs: (call) => { throw new InvokeModelGatewayError({ reason: 'malformed-tool-args', modelId, toolName: call.name, message: `model ${modelId} streamed arguments for tool '${call.name}' that are not JSON ` + `(${call.raw.length} chars). The call is refused rather than run with its ` + `arguments dropped. Asking again usually recovers; nothing re-sends it for you.`, }); }, }); } finally { deadline.dispose(); } }, }; } exports.invokeModelGateway = invokeModelGateway; /** * Class form of {@link invokeModelGateway}, for code that prefers `new`. */ class InvokeModelGatewayProvider { name = PROVIDER_NAME; carriesForcedToolChoice = true; /** The inner declaration resolves this adapter's shorthand before looking up the model. */ supportsForcedToolChoice; thinkingHandler = AnthropicThinkingHandler_js_1.anthropicThinkingHandler; /** The inner provider's own function — a closure, safe to forward unbound. */ thinkingMode; inner; constructor(options) { this.inner = invokeModelGateway(options); this.thinkingMode = this.inner.thinkingMode; this.supportsForcedToolChoice = this.inner.supportsForcedToolChoice; } // `hooks` is FORWARDED, not dropped — see LLMCallHooks in provider-contract/types.ts. complete(req, hooks) { return this.inner.complete(req, hooks); } stream(req, hooks) { if (!this.inner.stream) throw new Error('stream() unavailable'); return this.inner.stream(req, hooks); } } exports.InvokeModelGatewayProvider = InvokeModelGatewayProvider; function buildInvokeBody(req, modelId, thinkingMode, defaultMaxTokens, parallelToolCalls) { // No `model` field: the id is in the path, and this wire rejects it here. // The id still decides the thinking shape the body carries. return { anthropic_version: exports.INVOKE_MODEL_ANTHROPIC_VERSION, ...(0, anthropicMessagesWire_js_1.buildMessagesBody)(req, { model: modelId, thinkingMode, provider: PROVIDER_NAME, maxTokensDefault: defaultMaxTokens, ...(parallelToolCalls !== undefined && { parallelToolCalls }), }), }; } /** The model a request names, the shorthand resolved. A shorthand with no * `model` option stays as it is — `modelIdFor` refuses that request. */ function gatewayModelOf(asked, fallback) { return asked && asked !== PROVIDER_NAME ? asked : fallback || asked; } function modelIdFor(req, fallback) { const asked = req.model; if (asked && asked !== PROVIDER_NAME) return asked; if (fallback) return fallback; throw new InvokeModelGatewayError({ reason: 'no-model', message: `the request asked for '${asked ?? ''}' and the provider has no model to send. ` + `Pass invokeModelGateway({ model: '<the id your gateway knows>' }), or name the ` + `model per call.`, }); } async function keyFor(source, modelId) { let key; if (typeof source === 'string') key = source; else if (typeof source === 'function') key = await source(modelId); else key = Object.prototype.hasOwnProperty.call(source, modelId) ? source[modelId] : undefined; if (typeof key === 'string' && key.length > 0) return key; const known = typeof source === 'object' ? ` The key map names: ${Object.keys(source).join(', ')}.` : ''; throw new InvokeModelGatewayError({ reason: 'no-key', modelId, message: `no key for model ${modelId}.${known} Add one for this model id, or send another model.`, }); } /** * A 2xx body must be a Messages response (`content` array + `usage`). Anything * else — an HTML error page a proxy answered 200 with, an SSE body on the wrong * endpoint — is refused by name rather than read as an empty answer. */ async function readMessage(response, modelId) { const text = await response.text(); const parsed = tryParse(text); if (parsed && Array.isArray(parsed.content) && parsed.usage && typeof parsed.usage === 'object') { return parsed; } throw new InvokeModelGatewayError({ reason: 'unreadable-response', modelId, bodyExcerpt: text.slice(0, BODY_EXCERPT_CHARS), message: `model ${modelId} answered ${response.status} with a body that is not a Messages ` + `response (no content array and usage). Check that baseUrl points at the gateway's ` + `InvokeModel root.`, }); } // ─── Stream framing ───────────────────────────────────────────────── /** * The gateway's SSE → named events. * * Tolerates both framings a gateway sends: bare `data: {…}` lines (the event * name is the JSON's `type`) and `event:` + `data:` pairs. A `data:` line that * is not JSON on its own is joined with the next ones until it parses or a * blank line ends the event; one that never parses is refused — a dropped * event could be a dropped piece of the answer. An `error` event, or an AWS * exception object the gateway forwarded, ends the stream as a typed error. */ async function* readGatewayEvents(body, modelId, deadline) { const reader = body.getReader(); const decoder = new TextDecoder(); let buffered = ''; let eventName; let pending = []; function* flushPending(final) { (0, abort_js_1.throwIfAborted)(deadline.signal); if (pending.length === 0) return; const joined = pending.join('\n'); const data = tryParse(joined); if (data === undefined) { if (!final) return; throw unreadableEvent(joined, modelId); } pending = []; const name = eventName ?? typeOf(data) ?? 'message'; eventName = undefined; throwIfErrorEvent(name, data, modelId); yield { event: name, data }; } function* onLine(rawLine) { (0, abort_js_1.throwIfAborted)(deadline.signal); const line = rawLine.endsWith('\r') ? rawLine.slice(0, -1) : rawLine; if (line === '') { yield* flushPending(true); eventName = undefined; return; } if (line.startsWith(':')) return; // SSE comment / keep-alive if (line.startsWith('event:')) { eventName = line.slice(6).trim(); return; } if (!line.startsWith('data:')) return; const payload = line.slice(5).trim(); if (payload === '' || payload === '[DONE]') return; pending.push(payload); yield* flushPending(false); } let chunks = 0; try { for (;;) { // Each read has its own deadline — the first chunk, then every gap. const { value, done } = await deadline.within(reader.read(), chunks === 0 ? 'the first stream chunk' : 'the next stream chunk'); // A read that settled a moment before the abort is still refused // (lib/abort.ts, law 4). (0, abort_js_1.throwIfAborted)(deadline.signal); if (done) break; chunks++; buffered += decoder.decode(value, { stream: true }); let newline; while ((newline = buffered.indexOf('\n')) >= 0) { const line = buffered.slice(0, newline); buffered = buffered.slice(newline + 1); yield* onLine(line); } } buffered += decoder.decode(); if (buffered.length > 0) yield* onLine(buffered); yield* flushPending(true); } finally { // CANCEL on every way out — the end (a no-op), a consumer that stops // early, an error event, the deadline, the caller's abort — so a stream // nobody reads never holds its socket open. Not awaited once aborted: a // transport that ignores the abort may never settle its cancel, and the // caller is owed the abort now — also when it fires DURING the wait // (lib/abort.ts · bodyChunks, the same rule). const cancelled = reader.cancel().catch(() => undefined); try { // Only an abort that lands DURING the wait is thrown here: one that // fired before it is already the error on its way out (or a consumer // that broke after aborting, which exits cleanly, as abortableIterable). if (!deadline.signal?.aborted) { await deadline.within(cancelled, 'the stream close'); (0, abort_js_1.throwIfAborted)(deadline.signal); } } finally { try { reader.releaseLock(); } catch { /* a read still pending, or a lock the runtime already dropped */ } } } } function tryParse(text) { try { return JSON.parse(text); } catch { return undefined; } } function typeOf(data) { const t = data?.type; return typeof t === 'string' ? t : undefined; } /** * An Anthropic `error` event, or an AWS exception the gateway forwarded as one * object keyed by its name (`{ "throttlingException": { "message": … } }`). */ function throwIfErrorEvent(name, data, modelId) { if (name === 'error') { const inner = data.error; throw new InvokeModelGatewayError({ reason: 'stream', modelId, message: `model ${modelId} ended the stream with an error: ${inner?.type ?? 'error'} — ${inner?.message ?? JSON.stringify(data).slice(0, BODY_EXCERPT_CHARS)}`, }); } if (data !== null && typeof data === 'object' && typeOf(data) === undefined) { const [only, ...rest] = Object.keys(data); if (only !== undefined && rest.length === 0 && /exception$/i.test(only)) { const inner = data[only]; throw new InvokeModelGatewayError({ reason: 'stream', modelId, message: `model ${modelId} ended the stream with ${only}: ${inner?.message ?? ''}`, }); } } } function unreadableEvent(text, modelId) { return new InvokeModelGatewayError({ reason: 'stream', modelId, bodyExcerpt: text.slice(0, BODY_EXCERPT_CHARS), message: `the stream for model ${modelId} sent an event that is not JSON ` + `(${text.length} chars). Refused rather than dropped — it may have carried part of the answer.`, }); } // ─── Errors from the transport ────────────────────────────────────── async function statusError(response, modelId) { let bodyText = ''; try { bodyText = await response.text(); } catch { /* the status alone still says enough */ } // "Prompt is too long" is a refusal with its own type and its own fixes. const tooBig = (0, contextWindow_js_1.asContextWindowExceeded)(new Error(bodyText), { provider: PROVIDER_NAME, bodyText, status: response.status, }); if (tooBig) return tooBig; const excerpt = bodyText.slice(0, BODY_EXCERPT_CHARS); const retryAfterSeconds = readRetryAfter(response.headers.get('retry-after')); const retryAfterMs = statedWaitMs(response.headers, bodyText); return new InvokeModelGatewayError({ reason: 'http-status', modelId, status: response.status, ...(retryAfterSeconds !== undefined && { retryAfterSeconds }), ...(retryAfterMs !== undefined && { retryAfterMs }), ...(excerpt.length > 0 && { bodyExcerpt: excerpt }), message: `${statusSentence(response.status, modelId)}${excerpt ? ` Gateway said: ${excerpt}` : ''}`, }); } /** One sentence per status class, naming the model and what to check. */ function statusSentence(status, modelId) { if (status === 401) { return `HTTP 401 for model ${modelId}: the gateway did not accept the key. Check the key and apiKeyHeader.`; } if (status === 403) { return (`HTTP 403 for model ${modelId}: the key was read but is not allowed this model. ` + `Gateways often scope a key to some models — give this model id its own key ` + `(apiKey: { '${modelId}': … } or a function of the model id).`); } if (status === 404) { return `HTTP 404 for model ${modelId}: the gateway has no such model id, or baseUrl is wrong.`; } if (status === 429) { return `HTTP 429 for model ${modelId}: rate limited. Wrap the provider in withRetry to wait and retry.`; } if (status >= 500) { return `HTTP ${status} for model ${modelId}: the gateway or the model failed. Transient — withRetry retries it.`; } return `HTTP ${status} for model ${modelId}.`; } /** * The wait this gateway stated. The `Retry-After` header wins — it is the * protocol's own answer; the body's "Try again in N seconds" (the wording a * rate-limiting API gateway puts in its 429 body, often with no header at all) * is the fallback. The prose is read HERE, by the adapter that knows its * gateway's wording — `withRetry` only ever reads the declared `retryAfterMs`. */ function statedWaitMs(headers, bodyText) { const fromHeader = (0, retryAfter_js_1.retryAfterMsFromHeaders)(headers); if (fromHeader !== undefined) return fromHeader; const fromBody = /try again in (\d+(?:\.\d+)?) seconds?/i.exec(bodyText); return fromBody ? Math.round(Number(fromBody[1]) * 1000) : undefined; } function readRetryAfter(header) { if (header === null) return undefined; const trimmed = header.trim(); return /^\d+$/.test(trimmed) ? Number(trimmed) : undefined; } function networkError(err, modelId, signal) { // The caller cancelled: the call fails AS the cancellation, with the reason // they gave — never as a 'network' failure a retry would re-send. Checked // before the typed case: a caller abort under a deadline is not a timeout. if (signal?.aborted) return (0, abort_js_1.abortErrorOf)(signal); // Already typed — the deadline's own 'timeout' refusal. if (err instanceof InvokeModelGatewayError) return err; // An abort is the caller's decision, not a failure — pass it through as-is so // withRetry's AbortError check still sees it. const e = err; if (e && (e.name === 'AbortError' || e.code === 'ABORT_ERR')) return err; const tooBig = (0, contextWindow_js_1.asContextWindowExceeded)(err, { provider: PROVIDER_NAME }); if (tooBig) return tooBig; return new InvokeModelGatewayError({ reason: 'network', modelId, cause: err, message: `no answer from the gateway for model ${modelId}: ${err instanceof Error ? err.message : String(err)}`, }); } function deadlineFor(timeoutMs, callerSignal, modelId) { if (timeoutMs === undefined) { // No deadline, but still the caller's abort: every wait is raced against // it, so a transport that ignores its signal cannot hold a cancelled call // open (lib/abort.ts). return { signal: callerSignal, within: (pending) => (0, abort_js_1.untilAborted)(pending, callerSignal), dispose: () => undefined, }; } const controller = new AbortController(); const onCallerAbort = () => controller.abort(callerSignal?.reason); if (callerSignal?.aborted) onCallerAbort(); else callerSignal?.addEventListener('abort', onCallerAbort, { once: true }); return { signal: controller.signal, within(pending, what) { return new Promise((resolve, reject) => { const settle = () => { clearTimeout(timer); controller.signal.removeEventListener('abort', onAbort); }; const onAbort = () => { settle(); reject(controller.signal.reason); }; const timer = setTimeout(() => { controller.abort(new InvokeModelGatewayError({ reason: 'timeout', modelId, message: `no answer from model ${modelId} within ${timeoutMs} ms, waiting for ${what}. ` + `The request was aborted. Transient — withRetry asks again while no chunk ` + `has reached the caller; raise timeoutMs if the model needs longer.`, })); }, timeoutMs); // Handlers go on `pending` FIRST, before any early return: the wait // was already started (fetch, body or stream read), and a promise // left without a handler rejects later as an unhandled rejection — // which kills a Node process on default settings. Once the race is // decided the later settle is a no-op on this promise. pending.then((value) => { settle(); resolve(value); }, (err) => { settle(); // A transport that honoured the abort rejects with its reason — // already the typed error (or the caller's abort). reject(controller.signal.aborted ? controller.signal.reason : err); }); if (controller.signal.aborted) return onAbort(); controller.signal.addEventListener('abort', onAbort, { once: true }); }); }, dispose() { callerSignal?.removeEventListener('abort', onCallerAbort); }, }; } // ─── Option checks ────────────────────────────────────────────────── function checkedTimeout(raw) { if (raw === undefined) return undefined; if (typeof raw !== 'number' || !Number.isFinite(raw) || raw <= 0) { throw new InvokeModelGatewayError({ reason: 'invalid-options', message: `timeoutMs must be a positive number of milliseconds, got ${JSON.stringify(raw)}.`, }); } return raw; } function checkedBaseUrl(raw) { let url; try { url = new URL(raw); } catch { url = undefined; } if (!url || (url.protocol !== 'https:' && url.protocol !== 'http:')) { throw new InvokeModelGatewayError({ reason: 'invalid-options', message: `baseUrl must be an http(s) URL, got ${JSON.stringify(raw)}.`, }); } return (0, linearText_js_1.trimTrailing)(raw, linearText_js_1.isSlash); } function checkedHeader(raw) { // RFC 9110 token characters — a header name the fetch API will accept. if (typeof raw !== 'string' || !/^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/.test(raw)) { throw new InvokeModelGatewayError({ reason: 'invalid-options', message: `apiKeyHeader must be a header name such as 'api-key', got ${JSON.stringify(raw)}.`, }); } return raw; } function checkKeySource(source) { const ok = (typeof source === 'string' && source.length > 0) || typeof source === 'function' || (source !== null && typeof source === 'object' && Object.keys(source).length > 0); if (!ok) { throw new InvokeModelGatewayError({ reason: 'invalid-options', message: 'apiKey is required: a key, a map from model id to key, or a function of the model id.', }); } } //# sourceMappingURL=InvokeModelGatewayProvider.js.map