workers-ai-provider
Version:
Workers AI Provider for the vercel AI SDK
975 lines (974 loc) • 33.9 kB
JavaScript
//#region \0@oxc-project+runtime@0.137.0/helpers/esm/typeof.js
function _typeof(o) {
"@babel/helpers - typeof";
return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function(o) {
return typeof o;
} : function(o) {
return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o;
}, _typeof(o);
}
//#endregion
//#region \0@oxc-project+runtime@0.137.0/helpers/esm/toPrimitive.js
function toPrimitive(t, r) {
if ("object" != _typeof(t) || !t) return t;
var e = t[Symbol.toPrimitive];
if (void 0 !== e) {
var i = e.call(t, r || "default");
if ("object" != _typeof(i)) return i;
throw new TypeError("@@toPrimitive must return a primitive value.");
}
return ("string" === r ? String : Number)(t);
}
//#endregion
//#region \0@oxc-project+runtime@0.137.0/helpers/esm/toPropertyKey.js
function toPropertyKey(t) {
var i = toPrimitive(t, "string");
return "symbol" == _typeof(i) ? i : i + "";
}
//#endregion
//#region \0@oxc-project+runtime@0.137.0/helpers/esm/defineProperty.js
function _defineProperty(e, r, t) {
return (r = toPropertyKey(r)) in e ? Object.defineProperty(e, r, {
value: t,
enumerable: !0,
configurable: !0,
writable: !0
}) : e[r] = t, e;
}
//#endregion
//#region ../gateway-core/src/errors.ts
var GatewayDelegateError = class extends Error {
constructor(kind, message, cause) {
super(message);
_defineProperty(this, "kind", void 0);
_defineProperty(this, "cause", void 0);
this.name = "GatewayDelegateError";
this.kind = kind;
this.cause = cause;
}
};
//#endregion
//#region ../gateway-core/src/gateway-fetch.ts
/** JSON-encode metadata for the `cf-aig-metadata` header (`bigint` → string). */
function serializeMetadata(metadata) {
return JSON.stringify(metadata, (_k, v) => typeof v === "bigint" ? v.toString() : v);
}
/** Hop-by-hop headers that must never be forwarded to the gateway. */
const STRIP_HEADERS_BASE = ["content-length", "host"];
/** Normalize any `HeadersInit` shape into a plain object. */
function headersToObject(h) {
const out = {};
if (!h) return out;
if (h instanceof Headers) for (const [k, v] of h) out[k] = v;
else if (Array.isArray(h)) for (const [k, v] of h) out[k] = v;
else Object.assign(out, h);
return out;
}
/** Best-effort decode of a request body to text for re-parsing as JSON. */
function asText(body) {
if (typeof body === "string") return body;
if (body instanceof Uint8Array) return new TextDecoder().decode(body);
if (body instanceof ArrayBuffer) return new TextDecoder().decode(body);
return "{}";
}
/**
* Set the `cf-aig-*` cache/log/request headers on `headers` for every option
* that is defined. Mutates `headers` in place (callers pass the entry's header
* object).
*/
function applyGatewayCacheHeaders(headers, opts) {
if (opts.cacheTtl !== void 0) headers["cf-aig-cache-ttl"] = String(opts.cacheTtl);
if (opts.skipCache) headers["cf-aig-skip-cache"] = "true";
if (opts.cacheKey !== void 0) headers["cf-aig-cache-key"] = opts.cacheKey;
if (opts.metadata) headers["cf-aig-metadata"] = serializeMetadata(opts.metadata);
if (opts.collectLog !== void 0) headers["cf-aig-collect-log"] = String(opts.collectLog);
if (opts.eventId !== void 0) headers["cf-aig-event-id"] = opts.eventId;
if (opts.requestTimeoutMs !== void 0) headers["cf-aig-request-timeout"] = String(opts.requestTimeoutMs);
if (opts.byokAlias !== void 0) headers["cf-aig-byok-alias"] = opts.byokAlias;
if (opts.zdr !== void 0) headers["cf-aig-zdr"] = String(opts.zdr);
if (opts.retries) {
const { maxAttempts, retryDelayMs, backoff } = opts.retries;
if (maxAttempts !== void 0) headers["cf-aig-max-attempts"] = String(maxAttempts);
if (retryDelayMs !== void 0) headers["cf-aig-retry-delay"] = String(retryDelayMs);
if (backoff !== void 0) headers["cf-aig-backoff"] = backoff;
}
}
/**
* Assemble a single gateway entry: strip hop-by-hop + provider-auth headers,
* layer on `extraHeaders` and `cf-aig-*` cache headers, and attach the body as
* `query`. Endpoint derivation stays with the caller because the gateway-path
* (registry host-strip) and the REST path (`/v1/` strip) differ.
*/
function buildGatewayEntry(params) {
const strip = new Set(STRIP_HEADERS_BASE);
if (params.stripAuthHeaders) for (const h of params.stripAuthHeaders) strip.add(h.toLowerCase());
const headers = {};
for (const [k, v] of Object.entries(headersToObject(params.initHeaders))) if (!strip.has(k.toLowerCase())) headers[k] = v;
if (params.extraHeaders) Object.assign(headers, params.extraHeaders);
if (params.cache) applyGatewayCacheHeaders(headers, params.cache);
return {
provider: params.providerId,
endpoint: params.endpoint,
headers,
query: params.body
};
}
//#endregion
//#region ../gateway-core/src/gateway-providers.ts
/** Strip a leading `https://<host>/` prefix, leaving the endpoint path + query. */
function hostStrip(pattern) {
return (url) => url.replace(pattern, "");
}
const OPENAI_HOST = /^https:\/\/api\.openai\.com\//;
const ANTHROPIC_HOST = /^https:\/\/api\.anthropic\.com\//;
const GOOGLE_HOST = /^https:\/\/generativelanguage\.googleapis\.com\//;
const VERTEX_HOST = /^https:\/\/(?:[a-z0-9-]+-)?aiplatform\.googleapis\.com\//;
const XAI_HOST = /^https:\/\/api\.x\.ai\//;
const GROQ_HOST = /^https:\/\/api\.groq\.com\/openai\/v1\//;
const DEEPSEEK_HOST = /^https:\/\/api\.deepseek\.com\//;
const MISTRAL_HOST = /^https:\/\/api\.mistral\.ai\//;
const PERPLEXITY_HOST = /^https:\/\/api\.perplexity\.ai\//;
const CEREBRAS_HOST = /^https:\/\/api\.cerebras\.ai\//;
const OPENROUTER_HOST = /^https:\/\/openrouter\.ai\/api\//;
const FIREWORKS_HOST = /^https:\/\/api\.fireworks\.ai\/inference\/v1\//;
const COHERE_HOST = /^https:\/\/api\.cohere\.(?:com|ai)\//;
const REPLICATE_HOST = /^https:\/\/api\.replicate\.com\//;
const HUGGINGFACE_HOST = /^https:\/\/api-inference\.huggingface\.co\/models\//;
const CARTESIA_HOST = /^https:\/\/api\.cartesia\.ai\//;
const FAL_HOST = /^https:\/\/fal\.run\//;
const IDEOGRAM_HOST = /^https:\/\/api\.ideogram\.ai\//;
const DEEPGRAM_HOST = /^https:\/\/api\.deepgram\.com\//;
const ELEVENLABS_HOST = /^https:\/\/api\.elevenlabs\.io\//;
const GROK_KEY = "grok";
const BEDROCK_HOST = /^https:\/\/bedrock-runtime\.(?<region>[^.]+)\.amazonaws\.com\//;
function bedrockTransform(url) {
const m = url.match(/^https:\/\/bedrock-runtime\.(?<region>[^.]+)\.amazonaws\.com\/(?<rest>.*)$/);
if (!m?.groups) return url;
const { region, rest } = m.groups;
if (!region || rest === void 0) return url;
return `bedrock-runtime/${region}/${rest}`;
}
const AZURE_HOST = /^https:\/\/(?<resource>[^.]+)\.openai\.azure\.com\/openai\/deployments\/(?<deployment>[^/]+)\/(?<rest>.*)$/;
function azureTransform(url) {
const m = url.match(AZURE_HOST);
if (!m?.groups) return url;
const { resource, deployment, rest } = m.groups;
if (!resource || !deployment || !rest) return url;
return `${resource}/${deployment}/${rest}`;
}
/**
* The provider table. Order matters only for `detectProviderByUrl` (first match
* wins); slugs are looked up by `resolverKey`.
*/
const GATEWAY_PROVIDERS = [
{
resolverKey: "openai",
gatewayProviderId: "openai",
wireFormat: "openai",
runCatalog: true,
billing: "unified",
authHeaders: ["authorization"],
hostPattern: OPENAI_HOST,
transformEndpoint: hostStrip(OPENAI_HOST)
},
{
resolverKey: "anthropic",
gatewayProviderId: "anthropic",
wireFormat: "anthropic",
runWireFormat: "anthropic",
runCatalog: true,
billing: "unified",
authHeaders: ["x-api-key", "authorization"],
hostPattern: ANTHROPIC_HOST,
transformEndpoint: hostStrip(ANTHROPIC_HOST)
},
{
resolverKey: "google",
gatewayProviderId: "google-ai-studio",
wireFormat: "google",
runCatalog: true,
billing: "unified",
authHeaders: ["x-goog-api-key", "authorization"],
hostPattern: GOOGLE_HOST,
transformEndpoint: hostStrip(GOOGLE_HOST)
},
{
resolverKey: "xai",
gatewayProviderId: GROK_KEY,
wireFormat: "openai",
baseURL: "https://api.x.ai/v1",
runCatalog: true,
billing: "unified",
authHeaders: ["authorization"],
hostPattern: XAI_HOST,
transformEndpoint: hostStrip(XAI_HOST)
},
{
resolverKey: "groq",
gatewayProviderId: "groq",
wireFormat: "openai",
baseURL: "https://api.groq.com/openai/v1",
runCatalog: true,
billing: "unified",
authHeaders: ["authorization"],
hostPattern: GROQ_HOST,
transformEndpoint: hostStrip(GROQ_HOST)
},
{
resolverKey: "alibaba",
gatewayProviderId: "alibaba",
wireFormat: "openai",
runCatalog: true,
gatewayPath: false,
billing: "unified",
authHeaders: ["authorization"]
},
{
resolverKey: "minimax",
gatewayProviderId: "minimax",
wireFormat: "openai",
runCatalog: true,
gatewayPath: false,
billing: "unified",
authHeaders: ["authorization"]
},
{
resolverKey: "google-vertex",
gatewayProviderId: "google-vertex-ai",
runCatalog: false,
billing: "unified",
authHeaders: ["authorization"],
hostPattern: VERTEX_HOST,
transformEndpoint: hostStrip(VERTEX_HOST)
},
{
resolverKey: "deepseek",
gatewayProviderId: "deepseek",
wireFormat: "openai",
baseURL: "https://api.deepseek.com",
runCatalog: true,
billing: "unified",
authHeaders: ["authorization"],
hostPattern: DEEPSEEK_HOST,
transformEndpoint: hostStrip(DEEPSEEK_HOST)
},
{
resolverKey: "mistral",
gatewayProviderId: "mistral",
wireFormat: "openai",
baseURL: "https://api.mistral.ai/v1",
runCatalog: false,
billing: "byok",
authHeaders: ["authorization"],
hostPattern: MISTRAL_HOST,
transformEndpoint: hostStrip(MISTRAL_HOST)
},
{
resolverKey: "perplexity",
gatewayProviderId: "perplexity-ai",
wireFormat: "openai",
baseURL: "https://api.perplexity.ai",
runCatalog: false,
billing: "byok",
authHeaders: ["authorization"],
hostPattern: PERPLEXITY_HOST,
transformEndpoint: hostStrip(PERPLEXITY_HOST)
},
{
resolverKey: "cerebras",
gatewayProviderId: "cerebras",
wireFormat: "openai",
baseURL: "https://api.cerebras.ai/v1",
runCatalog: false,
billing: "byok",
authHeaders: ["authorization"],
hostPattern: CEREBRAS_HOST,
transformEndpoint: hostStrip(CEREBRAS_HOST)
},
{
resolverKey: "openrouter",
gatewayProviderId: "openrouter",
wireFormat: "openai",
baseURL: "https://openrouter.ai/api/v1",
runCatalog: false,
billing: "byok",
authHeaders: ["authorization"],
hostPattern: OPENROUTER_HOST,
transformEndpoint: hostStrip(OPENROUTER_HOST)
},
{
resolverKey: "fireworks",
gatewayProviderId: "fireworks",
wireFormat: "openai",
baseURL: "https://api.fireworks.ai/inference/v1",
runCatalog: false,
billing: "byok",
authHeaders: ["authorization"],
hostPattern: FIREWORKS_HOST,
transformEndpoint: hostStrip(FIREWORKS_HOST)
},
{
resolverKey: "cohere",
gatewayProviderId: "cohere",
runCatalog: false,
billing: "byok",
authHeaders: ["authorization"],
hostPattern: COHERE_HOST,
transformEndpoint: hostStrip(COHERE_HOST)
},
{
resolverKey: "baseten",
gatewayProviderId: "baseten",
runCatalog: false,
billing: "byok",
authHeaders: ["authorization"]
},
{
resolverKey: "parallel",
gatewayProviderId: "parallel",
runCatalog: false,
billing: "byok",
authHeaders: ["authorization", "x-api-key"]
},
{
resolverKey: "azure-openai",
gatewayProviderId: "azure-openai",
runCatalog: false,
billing: "byok",
authHeaders: ["api-key", "authorization"],
hostPattern: AZURE_HOST,
transformEndpoint: azureTransform
},
{
resolverKey: "aws-bedrock",
gatewayProviderId: "aws-bedrock",
runCatalog: false,
billing: "byok",
authHeaders: ["authorization"],
hostPattern: BEDROCK_HOST,
transformEndpoint: bedrockTransform
},
{
resolverKey: "huggingface",
gatewayProviderId: "huggingface",
runCatalog: false,
billing: "byok",
authHeaders: ["authorization"],
hostPattern: HUGGINGFACE_HOST,
transformEndpoint: hostStrip(HUGGINGFACE_HOST)
},
{
resolverKey: "replicate",
gatewayProviderId: "replicate",
runCatalog: false,
billing: "byok",
authHeaders: ["authorization"],
hostPattern: REPLICATE_HOST,
transformEndpoint: hostStrip(REPLICATE_HOST)
},
{
resolverKey: "fal",
gatewayProviderId: "fal",
runCatalog: false,
billing: "byok",
authHeaders: ["authorization"],
hostPattern: FAL_HOST,
transformEndpoint: hostStrip(FAL_HOST)
},
{
resolverKey: "ideogram",
gatewayProviderId: "ideogram",
runCatalog: false,
billing: "byok",
authHeaders: ["authorization"],
hostPattern: IDEOGRAM_HOST,
transformEndpoint: hostStrip(IDEOGRAM_HOST)
},
{
resolverKey: "cartesia",
gatewayProviderId: "cartesia",
runCatalog: false,
billing: "byok",
authHeaders: ["authorization", "x-api-key"],
hostPattern: CARTESIA_HOST,
transformEndpoint: hostStrip(CARTESIA_HOST)
},
{
resolverKey: "deepgram",
gatewayProviderId: "deepgram",
runCatalog: false,
billing: "byok",
authHeaders: ["authorization", "token"],
hostPattern: DEEPGRAM_HOST,
transformEndpoint: hostStrip(DEEPGRAM_HOST)
},
{
resolverKey: "elevenlabs",
gatewayProviderId: "elevenlabs",
runCatalog: false,
billing: "byok",
authHeaders: ["xi-api-key", "authorization"],
hostPattern: ELEVENLABS_HOST,
transformEndpoint: hostStrip(ELEVENLABS_HOST)
}
];
/** Aliases that map a friendly slug prefix to a canonical `resolverKey`. */
const RESOLVER_ALIASES = {
grok: "xai",
"google-ai-studio": "google",
"google-vertex-ai": "google-vertex",
bedrock: "aws-bedrock",
azure: "azure-openai"
};
const BY_RESOLVER_KEY = new Map(GATEWAY_PROVIDERS.map((p) => [p.resolverKey, p]));
/** Look up a provider by the slug prefix the user typed (honoring aliases). */
function findProviderBySlug(resolverKey) {
const canonical = RESOLVER_ALIASES[resolverKey] ?? resolverKey;
return BY_RESOLVER_KEY.get(canonical);
}
/** Detect the gateway provider from a wrapped provider's request URL (BYOG). */
function detectProviderByUrl(url) {
return GATEWAY_PROVIDERS.find((p) => p.hostPattern?.test(url));
}
/** All slug keys with a built-in parser (auto-wireable by the slug delegate). */
function wireableProviders() {
return GATEWAY_PROVIDERS.filter((p) => p.wireFormat !== void 0);
}
//#endregion
//#region ../gateway-core/src/resumable-stream.ts
function concat(a, b) {
const out = new Uint8Array(new ArrayBuffer(a.length + b.length));
out.set(a, 0);
out.set(b, a.length);
return out;
}
/** Index just past the last `\n\n` in `buf`, or -1 if there is no complete event. */
function lastEventBoundary(buf) {
for (let i = buf.length - 2; i >= 0; i--) if (buf[i] === 10 && buf[i + 1] === 10) return i + 2;
return -1;
}
/** Count of `\n\n` terminators (= complete SSE events) in `buf`. */
function countEvents(buf) {
let n = 0;
for (let i = 0; i + 1 < buf.length; i++) if (buf[i] === 10 && buf[i + 1] === 10) {
n++;
i++;
}
return n;
}
function resumeUrl(gateway, runId, from) {
return `https://workers-binding.ai/ai-gateway/gateways/${gateway}/run/${runId}/resume?from=${from}`;
}
function createResumableStream(options) {
const { binding, gateway, runId } = options;
const maxReconnects = options.maxReconnects ?? 5;
const onExpired = options.onResumeExpired ?? "error";
let emittedEvents = options.fromEvent ?? 0;
let pending = new Uint8Array(/* @__PURE__ */ new ArrayBuffer(0));
let reconnects = 0;
let canceled = false;
let activeReader = null;
const isAborted = () => canceled || options.signal?.aborted === true;
async function fetchResume(controller) {
let res;
try {
res = await binding.fetch(resumeUrl(gateway, runId, emittedEvents), {
method: "GET",
signal: options.signal
});
} catch (fetchErr) {
controller.error(new GatewayDelegateError("dispatch", `Resume request threw at event ${emittedEvents}.`, fetchErr));
return null;
}
if (res.status === 404) {
if (onExpired === "accept-partial") {
controller.close();
return null;
}
controller.error(new GatewayDelegateError("resume-expired", `Resume buffer expired (404) at event ${emittedEvents}. The gateway buffer TTL (~5.5 min) elapsed; fall back to continuation or regeneration.`));
return null;
}
if (!res.ok || !res.body) {
controller.error(new GatewayDelegateError("dispatch", `Resume failed (${res.status}) at event ${emittedEvents}.`));
return null;
}
return res.body;
}
return new ReadableStream({
async start(controller) {
let current;
if (options.initial) current = options.initial;
else {
const body = await fetchResume(controller);
if (!body) return;
current = body;
}
for (;;) {
const reader = current.getReader();
activeReader = reader;
try {
for (;;) {
const { done, value } = await reader.read();
if (done) {
if (pending.length > 0) {
controller.enqueue(pending);
pending = new Uint8Array(/* @__PURE__ */ new ArrayBuffer(0));
}
controller.close();
return;
}
if (!value || value.length === 0) continue;
pending = concat(pending, value);
const boundary = lastEventBoundary(pending);
if (boundary > 0) {
const complete = pending.slice(0, boundary);
controller.enqueue(complete);
emittedEvents += countEvents(complete);
options.onProgress?.(emittedEvents);
pending = pending.slice(boundary);
}
}
} catch (err) {
try {
reader.releaseLock();
} catch {}
if (isAborted()) return;
if (reconnects >= maxReconnects) {
controller.error(new GatewayDelegateError("resume-expired", `Exceeded ${maxReconnects} reconnect attempts at event ${emittedEvents}.`, err));
return;
}
pending = new Uint8Array(/* @__PURE__ */ new ArrayBuffer(0));
reconnects++;
options.onReconnect?.(emittedEvents, reconnects);
const body = await fetchResume(controller);
if (!body) return;
current = body;
}
}
},
cancel(reason) {
canceled = true;
if (activeReader) activeReader.cancel(reason).catch(() => {});
}
});
}
//#endregion
//#region ../gateway-core/src/workers-ai.ts
/**
* Shared, framework-agnostic Workers AI helpers.
*
* These are the pieces that `workers-ai-provider` (native `LanguageModelV3`) and
* `@cloudflare/tanstack-ai` (OpenAI-SDK shim) both need: the SSE byte decoder,
* message normalization for the binding's stricter schema, response-text
* extraction across WAI's response shapes, and the gpt-oss forced-tool-call
* salvage.
*
* IMPORTANT — id/dependency decoupling: nothing here depends on the `ai` package
* or mints framework tool-call ids. `parseLeakedToolCalls` returns neutral
* `{ toolName, input }` records; each consumer assigns its own ids and adapts to
* its own tool-call shape. This keeps `gateway-core` free of an `ai` dependency.
*/
/**
* TransformStream that decodes a raw byte stream into SSE `data:` payloads.
* Each output chunk is the string content after `data: ` (one per SSE event),
* with line buffering for partial chunks.
*/
var SSEDecoder = class extends TransformStream {
constructor() {
let buffer = "";
const decoder = new TextDecoder();
const emit = (line, controller) => {
const trimmed = line.trim();
if (!trimmed) return;
if (trimmed.startsWith("data: ")) controller.enqueue(trimmed.slice(6));
else if (trimmed.startsWith("data:")) controller.enqueue(trimmed.slice(5));
};
super({
transform(chunk, controller) {
buffer += decoder.decode(chunk, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop() || "";
for (const line of lines) emit(line, controller);
},
flush(controller) {
if (buffer.trim()) emit(buffer, controller);
}
});
}
};
/**
* Normalize messages before passing to the Workers AI binding.
*
* The binding has strict schema validation that differs from the OpenAI API:
* `content` must not be `null`/`undefined` (coerced to `""`). Content arrays
* (image_url parts) pass through untouched for vision-capable models.
*/
function normalizeMessagesForBinding(messages) {
return messages.map((msg) => {
const normalized = { ...msg };
if (normalized.content === null || normalized.content === void 0) normalized.content = "";
return normalized;
});
}
/**
* Extract text from a Workers AI response, handling multiple response shapes:
* - OpenAI format: `{ choices: [{ message: { content: "..." } }] }`
* - Native format: `{ response: "..." }`
* - Structured-output quirk: `{ response: { ... } }` (object) / `"{ ... }"` (JSON string)
* - Numeric `{ response: 42 }`
*/
function processText(output) {
const choiceContent = output.choices?.[0]?.message?.content;
if (choiceContent != null && String(choiceContent).length > 0) return String(choiceContent);
if ("response" in output) {
const response = output.response;
if (typeof response === "object" && response !== null) return JSON.stringify(response);
if (typeof response === "number") return String(response);
if (response === null || response === void 0) return;
return String(response);
}
}
/**
* Was a specific tool forced for this request?
*
* True for both `tool_choice: "required"` and the named-function form
* `{ type: "function", function: { name } }`.
*/
function isForcedToolChoice(toolChoice) {
if (toolChoice === "required") return true;
return typeof toolChoice === "object" && toolChoice !== null && toolChoice.type === "function";
}
/** Collect the requested tool names from mapped tools. */
function getToolNames(tools) {
return new Set((tools ?? []).map((tool) => tool.function?.name).filter((name) => typeof name === "string"));
}
/**
* Parse tool calls that a model leaked as JSON text instead of structured
* `tool_calls`. Shared by the non-streaming salvage and the streaming buffer.
*
* Only JSON objects whose `name` is one of `knownToolNames` are recovered;
* everything else (prose, harmony channel/role leaks like `{"name":"analysis"}`,
* hallucinated names) is ignored to avoid fabricating bogus calls.
*
* Returns neutral `{ toolName, input }` records — callers assign their own ids.
*/
function parseLeakedToolCalls(text, knownToolNames) {
let parsed;
try {
parsed = JSON.parse(text.trim());
} catch {
return [];
}
const candidates = Array.isArray(parsed) ? parsed : [parsed];
const salvaged = [];
for (const candidate of candidates) {
if (typeof candidate !== "object" || candidate === null) continue;
const obj = candidate;
const name = obj.name;
if (typeof name !== "string" || !knownToolNames.has(name)) continue;
let args;
if ("arguments" in obj) args = obj.arguments;
else if ("parameters" in obj) args = obj.parameters;
else {
const { name: _name, ...rest } = obj;
args = rest;
}
salvaged.push({
toolName: name,
input: typeof args === "string" ? args : JSON.stringify(args ?? {})
});
}
return salvaged;
}
//#endregion
//#region ../gateway-core/src/workers-ai-errors.ts
/**
* Shared, dependency-free Workers AI error classification used by every
* consumer of `@cloudflare/gateway-core` (`workers-ai-provider` and
* `@cloudflare/tanstack-ai`).
*
* The Workers AI **binding** (`env.AI.run`) throws plain `Error`s whose message
* carries an internal code (e.g. `"3040: Capacity temporarily exceeded"`) but
* no HTTP status. Retry machinery (the AI SDK's `APICallError` retry, the OpenAI
* SDK's status-based retry, and our own non-chat retry loop) all key off an HTTP
* status, so we translate the internal code into the documented status and let
* each layer derive retryability from it — this is what makes transient failures
* like "out of capacity" (3040 → 429) automatically retried.
*
* This module is intentionally free of any provider-SDK types so it can be
* inlined into each consumer's bundle.
*/
/**
* Workers AI internal error code → HTTP status code.
*
* Source: https://developers.cloudflare.com/workers-ai/platform/errors/
*/
const WORKERS_AI_ERROR_CODE_TO_STATUS = {
5007: 400,
5004: 400,
3039: 400,
3003: 400,
5018: 403,
5016: 403,
3023: 403,
3041: 403,
5019: 405,
5005: 405,
3042: 404,
3006: 413,
3007: 408,
3008: 408,
3036: 429,
3040: 429
};
/** Read a human-readable message from any thrown value (Error, DOMException, plain object, string). */
function messageOf(error) {
if (error instanceof Error) return error.message;
if (typeof error === "string") return error;
if (error && typeof error === "object") {
const message = error.message;
if (typeof message === "string") return message;
}
return String(error);
}
/**
* Best-effort extraction of a Workers AI internal error code from a thrown
* binding error. Prefers a numeric `code` property when present, otherwise
* parses the `"<code>: <message>"` form the binding uses (optionally prefixed,
* e.g. `"InferenceUpstreamError: 3040: ..."`). Only recognized codes are
* returned, and when several `"<number>:"` groups appear (e.g. a leading
* request id) the first that maps to a known code wins, so unrelated numbers
* can't be misread as a code.
*/
function parseWorkersAIErrorCode(error) {
if (error && typeof error === "object") {
const code = error.code;
if (typeof code === "number" && code in WORKERS_AI_ERROR_CODE_TO_STATUS) return code;
if (typeof code === "string") {
const parsed = Number.parseInt(code, 10);
if (Number.isFinite(parsed) && parsed in WORKERS_AI_ERROR_CODE_TO_STATUS) return parsed;
}
}
const message = messageOf(error);
for (const match of message.matchAll(/\b(\d{3,5})\s*:/g)) {
const parsed = Number.parseInt(match[1], 10);
if (parsed in WORKERS_AI_ERROR_CODE_TO_STATUS) return parsed;
}
}
/**
* True for cancellation errors that must propagate untouched (never wrapped or
* retried, so each layer's own abort detection still fires). Mirrors the AI
* SDK's `isAbortError`: a real abort from `fetch`/the binding is a
* `DOMException`, which is NOT `instanceof Error`, so both must be checked.
*/
function isAbortError(error) {
return (error instanceof Error || error instanceof DOMException) && (error.name === "AbortError" || error.name === "ResponseAborted" || error.name === "TimeoutError");
}
//#endregion
//#region src/errors.ts
/** Map an HTTP status to a {@link GatewayErrorCode} + recoverability hint. */
function classifyStatus(status) {
if (status === 401 || status === 403) return {
code: "auth",
recoverable: false
};
if (status === 429) return {
code: "rate-limit",
recoverable: true
};
if (status === 404) return {
code: "not-found",
recoverable: false
};
if (status === 400 || status === 422) return {
code: "bad-request",
recoverable: false
};
if (status >= 500) return {
code: "provider-error",
recoverable: true
};
return {
code: "unknown",
recoverable: false
};
}
/** Best-effort extraction of a human message from a CF/provider error envelope. */
function extractErrorMessage(raw) {
if (typeof raw === "string") {
const trimmed = raw.trim();
if (!trimmed) return void 0;
try {
return extractErrorMessage(JSON.parse(trimmed));
} catch {
return trimmed.slice(0, 500);
}
}
if (!raw || typeof raw !== "object") return void 0;
const obj = raw;
if (Array.isArray(obj.errors) && obj.errors.length > 0) {
const first = obj.errors[0];
if (typeof first?.message === "string") return first.message;
}
if (obj.error && typeof obj.error === "object") {
const err = obj.error;
if (typeof err.message === "string") return err.message;
}
if (typeof obj.error === "string") return obj.error;
if (typeof obj.message === "string") return obj.message;
}
/** A single dispatch failure through AI Gateway (run or gateway path). */
var WorkersAIGatewayError = class WorkersAIGatewayError extends Error {
constructor(code, message, opts = {}) {
super(message);
_defineProperty(this, "code", void 0);
_defineProperty(this, "recoverable", void 0);
_defineProperty(this, "status", void 0);
_defineProperty(this, "context", void 0);
_defineProperty(this, "raw", void 0);
_defineProperty(this, "cause", void 0);
this.name = "WorkersAIGatewayError";
this.code = code;
this.recoverable = opts.recoverable ?? false;
this.status = opts.status ?? null;
this.context = opts.context ?? {};
this.raw = opts.raw;
this.cause = opts.cause;
}
/**
* Classify an arbitrary thrown value. Understands AI SDK `APICallError`
* (reads `statusCode` / `responseBody` / `isRetryable`); falls back to a
* recoverable `gateway-error` for transport/connection failures so a fallback
* chain keeps trying.
*/
static fromUnknown(e) {
if (e instanceof WorkersAIGatewayError) return e;
const obj = e && typeof e === "object" ? e : {};
const status = typeof obj.statusCode === "number" ? obj.statusCode : null;
const responseBody = typeof obj.responseBody === "string" ? obj.responseBody : void 0;
if (status !== null) {
const classified = classifyStatus(status);
const recoverable = typeof obj.isRetryable === "boolean" ? obj.isRetryable : classified.recoverable;
const message = extractErrorMessage(responseBody) ?? (e instanceof Error ? e.message : `Gateway dispatch failed (HTTP ${status}).`);
let raw = responseBody;
try {
raw = responseBody ? JSON.parse(responseBody) : responseBody;
} catch {}
return new WorkersAIGatewayError(classified.code, message, {
recoverable,
status,
raw,
cause: e
});
}
return new WorkersAIGatewayError("gateway-error", e instanceof Error ? e.message : String(e), {
recoverable: true,
cause: e
});
}
/** Build from an HTTP `Response` (reads the body for the envelope). */
static async fromResponse(resp, context = {}) {
const text = await resp.text().catch(() => "");
const { code, recoverable } = classifyStatus(resp.status);
const message = extractErrorMessage(text) ?? `Gateway dispatch failed (HTTP ${resp.status}).`;
let raw = text;
try {
raw = text ? JSON.parse(text) : text;
} catch {}
return new WorkersAIGatewayError(code, message, {
recoverable,
status: resp.status,
raw,
context: {
...context,
status: resp.status,
logId: resp.headers.get("cf-aig-log-id"),
runId: resp.headers.get("cf-aig-run-id")
}
});
}
};
/** Every model in a client-side fallback chain failed. */
var WorkersAIFallbackError = class extends Error {
constructor(attempts, message) {
const tried = attempts.map((a) => a.model).join(" → ");
super(message ?? `All fallback models failed: ${tried}.`);
_defineProperty(this, "attempts", void 0);
this.name = "WorkersAIFallbackError";
this.attempts = attempts;
}
/** The last (most recent) attempt's error, if any. */
get lastError() {
for (let i = this.attempts.length - 1; i >= 0; i--) {
const e = this.attempts[i].error;
if (e) return e;
}
}
};
//#endregion
//#region src/gateway-provider.ts
/**
* A `fetch` that dispatches the wrapped provider's request through AI Gateway.
* Detects the gateway provider id from the request URL (or uses `config.provider`),
* strips the provider host to the endpoint path, and forwards the body verbatim.
*/
function createGatewayFetch(config) {
if (!config?.binding) throw new WorkersAIGatewayError("gateway-error", "createGatewayFetch requires a `binding` (e.g. { binding: env.AI }).");
const gatewayId = typeof config.gateway === "string" ? config.gateway : config.gateway?.id;
if (!gatewayId) throw new WorkersAIGatewayError("gateway-error", "createGatewayFetch requires a `gateway` id (e.g. gateway: \"my-gateway\").");
return (async (input, init) => {
const rawUrl = typeof input === "string" ? input : input.toString();
let info;
if (config.provider) info = void 0;
else {
info = detectProviderByUrl(rawUrl);
if (!info) throw new WorkersAIGatewayError("gateway-error", `Could not detect a gateway provider from URL "${rawUrl}". Pass \`provider: "<gateway-provider-id>"\` explicitly.`, { recoverable: false });
}
const providerId = config.provider ?? info.gatewayProviderId;
const endpoint = info?.transformEndpoint ? info.transformEndpoint(rawUrl) : rawUrl.replace(/^https?:\/\/[^/]+\//, "");
const body = JSON.parse(asText(init?.body));
const entry = buildGatewayEntry({
providerId,
endpoint,
initHeaders: init?.headers,
body,
...!config.byok && info ? { stripAuthHeaders: info.authHeaders } : {},
...config.extraHeaders ? { extraHeaders: config.extraHeaders } : {},
cache: {
cacheTtl: config.cacheTtl,
skipCache: config.skipCache
}
});
const gw = config.binding.gateway(gatewayId);
const runOptions = {};
if (init?.signal) runOptions.signal = init.signal;
return gw.run([entry], runOptions);
});
}
/**
* Wrap an `@ai-sdk/*` provider factory so its traffic flows through AI Gateway.
* A thin convenience over {@link createGatewayFetch} — it injects the gateway
* `fetch` (and a placeholder `apiKey` unless you supply one for BYOK).
*
* @example
* ```ts
* import { createOpenAI } from "@ai-sdk/openai";
* import { createGatewayProvider } from "workers-ai-provider/gateway";
*
* const openai = createGatewayProvider(createOpenAI, {
* binding: env.AI,
* gateway: "my-gw",
* });
* const model = openai("gpt-5");
* ```
*/
function createGatewayProvider(factory, config) {
return factory({
apiKey: config.apiKey ?? "unused",
...config.baseURL ? { baseURL: config.baseURL } : {},
fetch: createGatewayFetch(config)
});
}
//#endregion
export { GatewayDelegateError as C, headersToObject as S, detectProviderByUrl as _, WORKERS_AI_ERROR_CODE_TO_STATUS as a, asText as b, parseWorkersAIErrorCode as c, isForcedToolChoice as d, normalizeMessagesForBinding as f, GATEWAY_PROVIDERS as g, createResumableStream as h, WorkersAIGatewayError as i, SSEDecoder as l, processText as m, createGatewayProvider as n, isAbortError as o, parseLeakedToolCalls as p, WorkersAIFallbackError as r, messageOf as s, createGatewayFetch as t, getToolNames as u, findProviderBySlug as v, _defineProperty as w, buildGatewayEntry as x, wireableProviders as y };
//# sourceMappingURL=gateway-provider-CQU-v2IO.mjs.map