agents
Version:
A home for your AI agents
2,496 lines • 89 kB
JavaScript
import { i as _classPrivateFieldInitSpec, n as _classPrivateFieldSet2, r as _assertClassBrand, t as _classPrivateFieldGet2 } from "./classPrivateFieldGet2-DZBYAB34.js";
import { t as _classPrivateMethodInitSpec } from "./classPrivateMethodInitSpec-qMjJ6sHQ.js";
import { C as isCatalogModelId, S as isRetryableStatus, T as isRunPathLanguageModelId, _ as isGatewayErrorEnvelope, a as normalizeChatCompletion, b as classifyStatus, f as createTransport$1, g as headersToObject, h as errorFromResponse$1, i as normalizeChatCompletionsStream, m as errorFromGatewayEnvelope$1, n as imageMediaTypeOf, o as prepareChatCompletionsRequest, p as definedHeaders, r as sseDataStream, t as buildImageInput, u as requireGatewayProvider, v as unwrapEnvelope$1, w as isDynamicRouteId, x as isAbortError, y as CloudflareAIError$1 } from "./images-CfuRCjwq.js";
import { a as parseModelOptions, i as mergeModelOptions, n as PROVIDER_OPTIONS_KEY, o as resolveOptions, r as gatewayLayerOf } from "./settings-Dk2il2Hf.js";
import { APICallError, TooManyEmbeddingValuesForCallError } from "@ai-sdk/provider";
//#region src/models/ai-sdk/errors.ts
/**
* The error a string that names no model this provider runs produces.
* Language models take Workers AI ids, AI Gateway catalog ids and dynamic
* routes; the modality methods take Workers AI ids only.
*/
function notAModelId(modelId, accepts) {
return /* @__PURE__ */ new TypeError(accepts === "language" ? `"${modelId}" is not a model id this provider knows. Use a Workers AI id ("@cf/zai-org/glm-4.7-flash"), an AI Gateway catalog id ("<author>/<model>", e.g. "openai/gpt-5.5" or "anthropic/claude-opus-4.8"), or a dynamic route ("dynamic/<route>").` : `"${modelId}" is not a Workers AI model id. The embedding, image, speech, transcription and reranking methods take Workers AI ids, which start with "@cf/".`);
}
/**
* The gate for the modality methods — embedding, image, speech,
* transcription, reranking — and their fallback legs: a Workers AI id and
* nothing else. Catalog models and dynamic routes are chat models here.
*/
function requireWorkersAIId(modelId) {
if (isDynamicRouteId(modelId)) throw new TypeError(`"${modelId}" is an AI Gateway dynamic route, and a dynamic route is a chat model: it answers in the chat-completions shape. Use it as ai("${modelId}"); the embedding, image, speech, transcription and reranking methods take "@cf/" ids.`);
if (!modelId.startsWith("@cf/")) throw notAModelId(modelId, "workers-ai");
return modelId;
}
/** Whether a string names a language model: any of the three id kinds. */
function isLanguageModelId(modelId) {
return isRunPathLanguageModelId(modelId) || isCatalogModelId(modelId);
}
/**
* The gate every string that names a language model passes through, wherever
* it enters the module: `ai("…")`, a per-model fallback leg, or a leg read
* out of the per-call `providerOptions.cloudflare` bag. All three kinds run on
* `env.AI.run`. Thrown at the line that supplied the id rather than at the
* request it would have made.
*/
function requireLanguageModelId(modelId) {
if (!isLanguageModelId(modelId)) throw notAModelId(modelId, "language");
return modelId;
}
/**
* Error thrown for every failed Cloudflare AI call, whether it came back as a
* non-2xx `Response` or as a thrown Workers AI binding error.
*
* Extends the AI SDK's `APICallError` so `ai`'s built-in retry loop honours
* {@link CloudflareAIError.isRetryable} without any extra wiring.
*/
var CloudflareAIError = class CloudflareAIError extends APICallError {
constructor(options) {
super({
cause: options.cause,
data: options.data,
isRetryable: options.isRetryable ?? isRetryableStatus(options.status),
message: options.message,
requestBodyValues: options.requestBodyValues,
responseBody: options.responseBody,
responseHeaders: options.responseHeaders,
statusCode: options.status,
url: options.url
});
this.name = "CloudflareAIError";
this.status = options.status;
this.code = options.code;
this.logId = options.logId;
this.model = options.model;
this.attempts = options.attempts;
}
/** Lifts the framework-neutral core error into the AI SDK error type. */
static fromCore(error) {
return new CloudflareAIError({
attempts: error.attempts,
cause: error.cause,
code: error.code,
data: error.data,
isRetryable: error.isRetryable,
logId: error.logId,
message: error.message,
model: error.model,
requestBodyValues: error.requestBodyValues,
responseBody: error.responseBody,
responseHeaders: error.responseHeaders,
status: error.status,
url: error.url
});
}
};
/**
* Converts whatever the core transport threw into what AI SDK callers expect:
* core errors become {@link CloudflareAIError}s; everything else passes
* through untouched.
*/
function toAISDKError(error) {
return error instanceof CloudflareAIError$1 ? CloudflareAIError.fromCore(error) : error;
}
//#endregion
//#region src/models/ai-sdk/settings.ts
/**
* The fallback legs for one call, in the caller's order. Precedence matches
* the rest of the option merge: a per-call list replaces the per-model one
* rather than extending it.
*/
function fallbackLegs(model, call) {
return call?.fallback ?? model?.fallback ?? [];
}
/**
* Reads per-call options out of AI SDK `providerOptions.cloudflare`.
*
* This is the one place the untyped per-call bag enters the module, so it is
* where the "`@cf/` ids only" rule is enforced for the fallback legs a caller
* wrote there: `parseModelOptions` is framework-neutral and drops rather than
* throws, and no type ever sees that JSON. A vendor id in a leg would
* otherwise reach `env.AI.run` — the path a vendor model must never take.
*/
function parseCallOptions(providerOptions) {
const options = parseModelOptions(providerOptions?.[PROVIDER_OPTIONS_KEY]);
for (const leg of options?.fallback ?? []) requireLanguageModelId(leg);
return options;
}
/**
* Per-call options for the modality models — embedding, image, speech,
* transcription, reranking. The same bag as {@link parseCallOptions}, but a
* fallback leg must be a Workers AI id: a dynamic route answers in the
* chat-completions shape, which none of those models can read.
*/
function parseModalityCallOptions(providerOptions) {
const options = parseModelOptions(providerOptions?.[PROVIDER_OPTIONS_KEY]);
for (const leg of options?.fallback ?? []) requireWorkersAIId(leg);
return options;
}
/**
* The options each model this provider built was built with.
*
* A `WeakMap` rather than a field: `createAI` needs to read a leg's own bag
* back when it layers a fallback chain's gateway options over it, and that is
* this module's business rather than part of any model's public shape.
*/
const MODEL_OPTIONS = /* @__PURE__ */ new WeakMap();
/** Records the options a model of this provider was built with. */
function rememberOptions(model, options) {
if (options !== void 0) MODEL_OPTIONS.set(model, options);
}
/** The options a model of this provider was built with, if it is one. */
function optionsOf(model) {
return model !== null && typeof model === "object" ? MODEL_OPTIONS.get(model) : void 0;
}
/**
* The per-call options a fallback leg is asked with: everything the caller
* sent, minus the fallback list itself.
*
* Every leg here is a model object, so a leg that read the same
* `providerOptions.cloudflare.fallback` would build the whole chain again —
* once per leg, each of those re-expanding in turn. The rest of the per-call
* bag stays, so a leg still honours a per-call `cacheTtl` or gateway id.
*/
function withoutCallFallback(options) {
const providerOptions = options.providerOptions;
const ours = providerOptions?.[PROVIDER_OPTIONS_KEY];
if (ours === void 0 || !("fallback" in ours)) return options;
const bag = { ...ours };
delete bag.fallback;
return {
...options,
providerOptions: {
...providerOptions,
[PROVIDER_OPTIONS_KEY]: bag
}
};
}
//#endregion
//#region src/models/ai-sdk/transport.ts
/**
* The typed AI SDK error for the gateway's own error envelope — a missing
* gateway, an unpaid account — on the provider-native path.
*/
function errorFromGatewayEnvelope(body, context) {
return CloudflareAIError.fromCore(errorFromGatewayEnvelope$1(body, context));
}
/**
* Unwraps the Cloudflare `{ success, result }` envelope, throwing an AI SDK
* {@link CloudflareAIError} for a failed envelope.
*/
function unwrapEnvelope(value, context) {
try {
return unwrapEnvelope$1(value, context);
} catch (error) {
throw toAISDKError(error);
}
}
/** The typed AI SDK error for a non-2xx response. */
async function errorFromResponse(response, context) {
return CloudflareAIError.fromCore(await errorFromResponse$1(response, context));
}
/**
* Builds the transport for a set of provider settings, with every failure
* lifted into the AI SDK error type.
*/
function createTransport(settings) {
const inner = createTransport$1(settings);
return {
logIdFallback: (response) => inner.logIdFallback(response),
async native(request) {
try {
return await inner.native(request);
} catch (error) {
throw toAISDKError(error);
}
},
async run(request) {
try {
return await inner.run(request);
} catch (error) {
throw toAISDKError(error);
}
},
url: inner.url
};
}
//#endregion
//#region src/models/ai-sdk/models/fallback.ts
/**
* Client-side fallback across models: the primary is tried first, then each
* fallback in order, when a call fails before producing output. Shared by the
* language and embedding models.
*/
/**
* Runs each leg in order until one answers. Legs may be anything — a model id
* this provider owns, or a whole other `LanguageModelV4` — because a leg is
* only ever a name and a thunk here.
*/
async function withFallbackLegs(legs, url) {
const attempts = [];
let lastError;
for (const leg of legs) try {
return await leg.run();
} catch (error) {
if (isAbortError(error)) throw error;
attempts.push({
error,
model: leg.model
});
lastError = error;
}
if (attempts.length <= 1) throw lastError;
throw fallbackError(lastError, attempts, url);
}
/**
* The parts a stream emits before it has said anything: protocol preamble
* that every leg would repeat. Anything else is output the caller has seen,
* after which the leg is committed and a later failure is its own.
*/
const PREAMBLE_PARTS = /* @__PURE__ */ new Set([
"stream-start",
"response-metadata",
"text-start",
"reasoning-start",
"tool-input-start",
"raw"
]);
/**
* {@link withFallbackLegs} for streams. A 2xx that then fails before any
* output — an `error` part, an error finish, or a broken body — is still a
* failed leg, so the next one is tried; the preamble the failed leg sent is
* dropped and the answering leg's own is delivered instead. Once a leg has
* produced output it is the answer, failures and all.
*/
async function withStreamFallbackLegs(legs, url) {
if (legs.length <= 1) return withFallbackLegs(legs, url);
const attempts = [];
let lastError;
for (const leg of legs) {
let result;
try {
result = await leg.run();
} catch (error) {
if (isAbortError(error)) throw error;
attempts.push({
error,
model: leg.model
});
lastError = error;
continue;
}
const reader = result.stream.getReader();
const preamble = [];
let failure;
let committed = false;
try {
for (;;) {
const { done, value } = await reader.read();
if (done) {
committed = true;
break;
}
if (value.type === "error") {
failure = value.error;
break;
}
if (value.type === "finish" && value.finishReason.unified === "error") {
failure = streamFailure(leg.model, url, value.finishReason.raw);
break;
}
preamble.push(value);
if (!PREAMBLE_PARTS.has(value.type)) {
committed = true;
break;
}
}
} catch (error) {
if (isAbortError(error)) throw error;
failure = error;
}
if (!committed) {
await reader.cancel().catch(() => void 0);
attempts.push({
error: failure,
model: leg.model
});
lastError = failure;
continue;
}
return {
...result,
stream: resumed(preamble, reader)
};
}
if (attempts.length <= 1) throw lastError;
throw fallbackError(lastError, attempts, url);
}
/** The parts already read, then the rest of the stream, as one stream. */
function resumed(head, reader) {
return new ReadableStream({
cancel(reason) {
return reader.cancel(reason);
},
async pull(controller) {
const { done, value } = await reader.read();
if (done) controller.close();
else controller.enqueue(value);
},
start(controller) {
for (const part of head) controller.enqueue(part);
}
});
}
/** A stream that ended in an error finish before it produced anything. */
function streamFailure(model, url, raw) {
return new CloudflareAIError({
code: "provider-error",
isRetryable: false,
message: `The stream from ${model} ended in an error before producing any output${raw === void 0 ? "" : ` (${raw})`}.`,
model,
requestBodyValues: void 0,
url
});
}
async function withFallback(primary, fallback, url, run) {
return withFallbackLegs([primary, ...fallback].map((modelId) => ({
model: modelId,
run: () => run(modelId)
})), url);
}
/** Rebuilds the last error with the whole attempt chain attached. */
function fallbackError(lastError, attempts, url) {
const models = attempts.map((attempt) => attempt.model).join(", ");
if (lastError instanceof CloudflareAIError) return new CloudflareAIError({
attempts,
cause: lastError,
code: lastError.code,
data: lastError.data,
isRetryable: lastError.isRetryable,
logId: lastError.logId,
message: `All models failed (${models}). Last error: ${lastError.message}`,
model: lastError.model,
requestBodyValues: lastError.requestBodyValues,
responseBody: lastError.responseBody,
responseHeaders: lastError.responseHeaders,
status: lastError.status,
url: lastError.url
});
if (APICallError.isInstance(lastError)) return new CloudflareAIError({
attempts,
cause: lastError,
code: classifyStatus(lastError.statusCode),
isRetryable: lastError.isRetryable,
message: `All models failed (${models}). Last error: ${lastError.message}`,
model: attempts.at(-1)?.model ?? "",
requestBodyValues: lastError.requestBodyValues,
responseBody: lastError.responseBody,
responseHeaders: lastError.responseHeaders,
status: lastError.statusCode,
url: lastError.url
});
return new CloudflareAIError({
attempts,
cause: lastError,
code: "unknown",
isRetryable: false,
message: `All models failed (${models}). Last error: ${String(lastError)}`,
model: attempts.at(-1)?.model ?? "",
requestBodyValues: void 0,
url
});
}
//#endregion
//#region src/models/ai-sdk/wires/shared.ts
/** Usage with every field unknown — the shape callers can always fall back to. */
function emptyUsage() {
return {
inputTokens: {
cacheRead: void 0,
cacheWrite: void 0,
noCache: void 0,
total: void 0
},
outputTokens: {
reasoning: void 0,
text: void 0,
total: void 0
}
};
}
/** A fresh id for a stream part or a tool call the provider did not name. */
function newId() {
return crypto.randomUUID();
}
const BASE64_CHUNK = 32768;
/** Base64-encodes bytes without blowing the argument limit on large files. */
function toBase64(data) {
if (typeof data === "string") return data;
let binary = "";
for (let offset = 0; offset < data.length; offset += BASE64_CHUNK) binary += String.fromCharCode(...data.subarray(offset, offset + BASE64_CHUNK));
return btoa(binary);
}
/** Reads a record field without widening everything to `any`. */
function record(value) {
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
}
/** Reads an array field, or `undefined` when it is not one. */
function array(value) {
return Array.isArray(value) ? value : void 0;
}
/** Reads a string field, or `undefined` when it is not one. */
function text(value) {
return typeof value === "string" ? value : void 0;
}
/** Reads a number field, or `undefined` when it is not one. */
function count(value) {
return typeof value === "number" ? value : void 0;
}
/** The finish reason used when a provider ends a stream without saying why. */
function defaultFinishReason() {
return {
raw: void 0,
unified: "stop"
};
}
//#endregion
//#region src/models/ai-sdk/models/embedding.ts
/**
* Workers AI batches up to this many strings per embedding call.
*
* @see https://developers.cloudflare.com/workers-ai/platform/limits/
*/
const MAX_EMBEDDINGS_PER_CALL = 3e3;
var _config$3 = /* @__PURE__ */ new WeakMap();
var _options$3 = /* @__PURE__ */ new WeakMap();
/**
* An `EmbeddingModelV4` over the Workers AI text-embedding catalog. The run
* path takes `{ text: string[] }` and answers `{ data, shape, usage }`,
* sometimes inside the Cloudflare envelope.
*/
var CloudflareEmbeddingModel = class {
constructor(modelId, options, config) {
this.specificationVersion = "v4";
this.provider = "cloudflare";
this.maxEmbeddingsPerCall = MAX_EMBEDDINGS_PER_CALL;
this.supportsParallelCalls = true;
_classPrivateFieldInitSpec(this, _config$3, void 0);
_classPrivateFieldInitSpec(this, _options$3, void 0);
options?.fallback?.forEach((leg) => {
if (typeof leg === "string") requireWorkersAIId(leg);
});
this.modelId = modelId;
_classPrivateFieldSet2(_options$3, this, options);
_classPrivateFieldSet2(_config$3, this, config);
}
async doEmbed(options) {
if (options.values.length > MAX_EMBEDDINGS_PER_CALL) throw new TooManyEmbeddingValuesForCallError({
maxEmbeddingsPerCall: MAX_EMBEDDINGS_PER_CALL,
modelId: this.modelId,
provider: this.provider,
values: options.values
});
const resolved = resolveOptions(_classPrivateFieldGet2(_config$3, this).providerGateway, _classPrivateFieldGet2(_options$3, this), parseModalityCallOptions(options.providerOptions));
const input = { text: options.values };
const transport = _classPrivateFieldGet2(_config$3, this).transport;
return withFallback(this.modelId, resolved.fallback, transport.url, async (modelId) => {
const response = await transport.run({
gateway: resolved.gateway,
headers: {
...definedHeaders(options.headers),
...resolved.headers,
...resolved.sessionAffinity === void 0 ? {} : { "x-session-affinity": resolved.sessionAffinity }
},
input,
model: modelId,
signal: options.abortSignal
});
if (!response.ok) throw await errorFromResponse(response, {
model: modelId,
requestBodyValues: input,
url: transport.url
});
const body = record(unwrapEnvelope(await response.json(), {
model: modelId,
status: response.status,
url: transport.url
})) ?? {};
const embeddings = (array(body.data) ?? []).map((row) => (array(row) ?? []).filter((value) => typeof value === "number"));
const tokens = count(record(body.usage)?.prompt_tokens);
return {
embeddings,
response: { headers: headersToObject(response.headers) },
...tokens === void 0 ? {} : { usage: { tokens } },
warnings: []
};
});
}
};
//#endregion
//#region src/models/ai-sdk/wires/chat-completions.ts
function fullMediaType(mediaType) {
return mediaType.includes("/") ? mediaType : `${mediaType}/jpeg`;
}
function unsupported$1(feature, details) {
return {
details,
feature,
type: "unsupported"
};
}
/** A compat-layer warning in the AI SDK's warning shape. */
function fromCompat(warning) {
return unsupported$1(warning.feature, warning.message);
}
/**
* Gemini 3 signs each assistant turn and each function call with an opaque
* `extra_content.google.thought_signature`, and rejects a function call
* replayed in the same turn without it. Nothing here reads the payload: it is
* carried out as provider metadata and put back verbatim on the next turn.
*
* `extraContent` is the signature of the assistant message; `toolExtraContent`
* is the one that belongs to an individual `tool_calls[i]` entry.
*/
function extraContentMetadata(fields) {
const metadata = {};
const message = record(fields.extraContent);
const tool = record(fields.toolExtraContent);
if (message !== void 0) metadata.extraContent = message;
if (tool !== void 0) metadata.toolExtraContent = tool;
if (Object.keys(metadata).length === 0) return {};
return { providerMetadata: { [PROVIDER_OPTIONS_KEY]: metadata } };
}
/** Reads back what {@link extraContentMetadata} stored on a replayed part. */
function storedExtraContent(options, key) {
return record(options?.[PROVIDER_OPTIONS_KEY]?.[key]);
}
function toolResultText(output, warnings) {
switch (output.type) {
case "text":
case "error-text": return output.value;
case "json":
case "error-json": return JSON.stringify(output.value);
case "execution-denied": return output.reason ?? "The user denied this tool call.";
case "content": {
const dropped = output.value.filter((part) => part.type !== "text");
if (dropped.length > 0) warnings.push(unsupported$1("tool-result-part", `Tool result parts of type ${[...new Set(dropped.map((part) => part.type))].join(", ")} cannot be sent on the chat-completions wire and were dropped.`));
return output.value.map((part) => part.type === "text" ? part.text : "").filter((part) => part !== "").join("\n");
}
default: return String(output);
}
}
function userContent(message, warnings) {
const parts = [];
let onlyText = true;
for (const part of message.content) {
if (part.type === "text") {
parts.push({
text: part.text,
type: "text"
});
continue;
}
if (!(part.mediaType === "image" || part.mediaType.startsWith("image/"))) {
warnings.push(unsupported$1("file-part", `Files of type ${part.mediaType} are not supported on this wire and were dropped.`));
continue;
}
if (part.data.type === "data") {
onlyText = false;
parts.push({
image_url: { url: `data:${fullMediaType(part.mediaType)};base64,${toBase64(part.data.data)}` },
type: "image_url"
});
continue;
}
if (part.data.type === "url") {
onlyText = false;
parts.push({
image_url: { url: part.data.url.toString() },
type: "image_url"
});
continue;
}
warnings.push(unsupported$1("file-part", "Only inline data and URL images are supported; the file was dropped."));
}
if (onlyText) return parts.map((part) => part.type === "text" ? part.text : "").join("");
return parts;
}
function convertPrompt(prompt, warnings) {
const messages = [];
for (const message of prompt) switch (message.role) {
case "system":
messages.push({
content: message.content,
role: "system"
});
break;
case "user":
messages.push({
content: userContent(message, warnings),
role: "user"
});
break;
case "assistant": {
let content = "";
let reasoning = "";
let extraContent;
const toolCalls = [];
for (const part of message.content) {
extraContent ??= storedExtraContent(part.providerOptions, "extraContent");
if (part.type === "text") content += part.text;
else if (part.type === "reasoning") reasoning += part.text;
else if (part.type === "tool-call") {
const toolExtra = storedExtraContent(part.providerOptions, "toolExtraContent");
toolCalls.push({
function: {
arguments: typeof part.input === "string" ? part.input : JSON.stringify(part.input ?? {}),
name: part.toolName
},
id: part.toolCallId,
type: "function",
...toolExtra === void 0 ? {} : { extra_content: toolExtra }
});
} else warnings.push(unsupported$1("assistant-part", `Assistant "${part.type}" parts cannot be replayed on the chat-completions wire and were dropped.`));
}
messages.push({
content,
...reasoning !== "" ? { reasoning_content: reasoning } : {},
...extraContent === void 0 ? {} : { extra_content: extraContent },
...toolCalls.length > 0 ? { tool_calls: toolCalls } : {},
role: "assistant"
});
break;
}
case "tool":
for (const part of message.content) {
if (part.type !== "tool-result") {
warnings.push(unsupported$1("tool-part", `Tool "${part.type}" parts are not carried on the chat-completions wire and were dropped.`));
continue;
}
messages.push({
content: toolResultText(part.output, warnings),
name: part.toolName,
role: "tool",
tool_call_id: part.toolCallId
});
}
break;
default: throw new Error(`Unsupported message role: ${String(message)}`);
}
return messages;
}
/** Folds the AI SDK's schema description into the schema itself. */
function foldSchema(schema, description) {
if (description === void 0 || schema.description !== void 0) return schema;
return {
...schema,
description
};
}
function responseFormat(options) {
const format = options.responseFormat;
if (format === void 0 || format.type !== "json") return void 0;
if (format.schema === void 0) return { type: "json_object" };
return {
json_schema: {
name: format.name ?? "response",
schema: foldSchema(format.schema, format.description),
strict: true
},
type: "json_schema"
};
}
function toolFields(options, warnings) {
const tools = [];
for (const tool of options.tools ?? []) {
if (tool.type !== "function") {
warnings.push(unsupported$1("provider-tool", `Provider tool "${tool.name}" is not available through the Cloudflare run path.`));
continue;
}
tools.push({
function: {
description: tool.description,
name: tool.name,
parameters: tool.inputSchema
},
type: "function"
});
}
if (tools.length === 0) return {};
const choice = options.toolChoice;
let toolChoice;
switch (choice?.type) {
case void 0: break;
case "auto":
toolChoice = "auto";
break;
case "none":
toolChoice = "none";
break;
case "required":
toolChoice = "required";
break;
case "tool":
toolChoice = {
function: { name: choice.toolName },
type: "function"
};
break;
default: throw new Error(`Unsupported tool choice: ${String(choice)}`);
}
return {
tools,
...toolChoice === void 0 ? {} : { tool_choice: toolChoice }
};
}
/**
* The strict-body spelling of the reasoning request: an effort level, or
* `null` for "off". The compat layer turns `null` into whatever the model
* understands, or warns when nothing does.
*/
function reasoningEffortField(options, context) {
if (context.reasoningEffort !== void 0) return { reasoning_effort: context.reasoningEffort };
const reasoning = options.reasoning;
if (reasoning === void 0 || reasoning === "provider-default") return {};
if (reasoning === "none") return { reasoning_effort: null };
return { reasoning_effort: reasoning };
}
/**
* Builds a strict OpenAI chat-completions body from AI SDK call options and
* runs it through the compat layer for the target model.
*/
function buildOpenAIRequest(options, context) {
const warnings = [];
if (options.topK !== void 0) warnings.push(unsupported$1("topK", "topK is not part of the chat-completions API."));
const format = responseFormat(options);
const prepared = prepareChatCompletionsRequest({
messages: convertPrompt(options.prompt, warnings),
...options.maxOutputTokens === void 0 ? {} : { max_tokens: options.maxOutputTokens },
...options.temperature === void 0 ? {} : { temperature: options.temperature },
...options.topP === void 0 ? {} : { top_p: options.topP },
...options.stopSequences === void 0 || options.stopSequences.length === 0 ? {} : { stop: options.stopSequences },
...options.seed === void 0 ? {} : { seed: options.seed },
...options.frequencyPenalty === void 0 ? {} : { frequency_penalty: options.frequencyPenalty },
...options.presencePenalty === void 0 ? {} : { presence_penalty: options.presencePenalty },
...format === void 0 ? {} : { response_format: format },
...toolFields(options, warnings),
...reasoningEffortField(options, context),
...context.chatTemplateKwargs === void 0 ? {} : { chat_template_kwargs: context.chatTemplateKwargs },
...context.stream ? {
stream: true,
stream_options: { include_usage: true }
} : {}
}, context.modelId);
return {
body: prepared.body,
reasoningOff: prepared.reasoningOff,
warnings: [...warnings, ...prepared.warnings.map(fromCompat)]
};
}
function mapFinishReason(raw) {
const value = text(raw);
switch (value) {
case "stop": return {
raw: value,
unified: "stop"
};
case "length": return {
raw: value,
unified: "length"
};
case "tool_calls":
case "function_call": return {
raw: value,
unified: "tool-calls"
};
case "content_filter": return {
raw: value,
unified: "content-filter"
};
case "error": return {
raw: value,
unified: "error"
};
default: return value === void 0 ? defaultFinishReason() : {
raw: value,
unified: "other"
};
}
}
function mapUsage(raw) {
const usage = record(raw);
if (usage === void 0) return emptyUsage();
const promptTokens = count(usage.prompt_tokens);
const cacheRead = count(record(usage.prompt_tokens_details)?.cached_tokens);
const reasoningTokens = count(record(usage.completion_tokens_details)?.reasoning_tokens);
const completionTokens = count(usage.completion_tokens);
const totalTokens = count(usage.total_tokens);
const outputTotal = totalTokens !== void 0 && promptTokens !== void 0 ? Math.max(completionTokens ?? 0, totalTokens - promptTokens) : completionTokens;
return {
inputTokens: {
cacheRead,
cacheWrite: void 0,
noCache: promptTokens === void 0 ? void 0 : Math.max(0, promptTokens - (cacheRead ?? 0)),
total: promptTokens
},
outputTokens: {
reasoning: reasoningTokens,
text: void 0,
total: outputTotal
},
raw: usage
};
}
function toolCallsFrom(raw, messageExtra) {
const calls = array(raw) ?? [];
const content = [];
for (const entry of calls) {
const call = record(entry);
if (call === void 0) continue;
const fn = record(call.function);
const name = text(fn?.name);
if (name === void 0) continue;
content.push({
input: text(fn?.arguments) ?? "{}",
toolCallId: text(call.id) ?? newId(),
toolName: name,
type: "tool-call",
...extraContentMetadata({
extraContent: content.length === 0 ? messageExtra : void 0,
toolExtraContent: call.extra_content
})
});
}
return content;
}
/**
* Parses a non-streaming response. The body is normalized to the strict
* `chat.completion` shape first, so only `choices[0].message` is read.
*/
function parseOpenAIGeneration(json, modelId) {
const body = normalizeChatCompletion(json, modelId);
const choice = record(array(body.choices)?.[0]);
const message = record(choice?.message);
const content = [];
const reasoning = text(message?.reasoning_content);
if (reasoning !== void 0 && reasoning !== "") content.push({
text: reasoning,
type: "reasoning"
});
const answer = text(message?.content);
const messageExtra = message?.extra_content;
if (answer !== void 0 && answer !== "") content.push({
text: answer,
type: "text",
...extraContentMetadata({ extraContent: messageExtra })
});
content.push(...toolCallsFrom(message?.tool_calls, answer === void 0 || answer === "" ? messageExtra : void 0));
const created = count(body.created);
return {
content,
finishReason: mapFinishReason(choice?.finish_reason),
responseId: text(body.id),
responseModelId: text(body.model),
timestamp: created === void 0 ? void 0 : /* @__PURE__ */ new Date(created * 1e3),
usage: mapUsage(body.usage)
};
}
/**
* Streams strict OpenAI `chat.completion.chunk` events into AI SDK stream
* parts. Tool calls arrive either as incremental `arguments` fragments closed
* by a null-finalization chunk, or as one delta with the complete arguments
* string; both are handled. Usage is read from whichever chunk carries it.
*/
function openAIStreamParser() {
const toolCalls = /* @__PURE__ */ new Map();
let lastToolIndex;
let textId;
let reasoningId;
let usage = emptyUsage();
let finishReason;
let sentMetadata = false;
let receivedDone = false;
let receivedEvent = false;
/** Message-level `extra_content` (Gemini's thought signature). */
let messageExtra;
let messageExtraClaimed = false;
let textEverOpened = false;
/**
* The message signature belongs on whichever part replays the message: the
* text block when there is one, otherwise the first tool call.
*/
const claimMessageExtra = () => {
if (messageExtraClaimed) return void 0;
messageExtraClaimed = true;
return messageExtra;
};
const closeReasoning = (controller) => {
if (reasoningId === void 0) return;
controller.enqueue({
id: reasoningId,
type: "reasoning-end"
});
reasoningId = void 0;
};
const closeText = (controller) => {
if (textId === void 0) return;
controller.enqueue({
id: textId,
type: "text-end",
...extraContentMetadata({ extraContent: claimMessageExtra() })
});
textId = void 0;
};
const closeToolCall = (index, controller) => {
const state = toolCalls.get(index);
if (state === void 0 || state.closed) return;
state.closed = true;
controller.enqueue({
id: state.id,
type: "tool-input-end"
});
controller.enqueue({
input: state.input,
toolCallId: state.id,
toolName: state.name,
type: "tool-call",
...extraContentMetadata({
extraContent: textEverOpened ? void 0 : claimMessageExtra(),
toolExtraContent: state.extraContent
})
});
};
const emitToolDeltas = (raw, controller) => {
const deltas = array(raw);
if (deltas === void 0) return;
for (const entry of deltas) {
const call = record(entry);
if (call === void 0) continue;
const fn = record(call.function);
const name = text(fn?.name);
const input = text(fn?.arguments);
const id = text(call.id);
const rawIndex = count(call.index);
if (rawIndex === void 0 && id === void 0 && (name === void 0 || name === "") && (input === void 0 || input === "")) {
if (lastToolIndex !== void 0) closeToolCall(lastToolIndex, controller);
continue;
}
closeReasoning(controller);
closeText(controller);
const index = rawIndex ?? lastToolIndex ?? 0;
let state = toolCalls.get(index);
if (state?.closed === true) continue;
if (state === void 0) {
if (lastToolIndex !== void 0 && lastToolIndex !== index) closeToolCall(lastToolIndex, controller);
state = {
closed: false,
extraContent: call.extra_content,
id: id ?? newId(),
input: "",
name: name ?? ""
};
toolCalls.set(index, state);
lastToolIndex = index;
controller.enqueue({
id: state.id,
toolName: state.name,
type: "tool-input-start"
});
} else {
if (name !== void 0 && state.name === "") state.name = name;
state.extraContent ??= call.extra_content;
}
if (input !== void 0 && input !== "") {
state.input += input;
controller.enqueue({
delta: input,
id: state.id,
type: "tool-input-delta"
});
}
}
};
return new TransformStream({
flush(controller) {
for (const index of toolCalls.keys()) closeToolCall(index, controller);
closeReasoning(controller);
closeText(controller);
if (finishReason === void 0) finishReason = receivedEvent && !receivedDone ? {
raw: "stream-truncated",
unified: "error"
} : defaultFinishReason();
controller.enqueue({
finishReason,
type: "finish",
usage
});
},
transform(data, controller) {
if (data === "") return;
if (data === "[DONE]") {
receivedDone = true;
return;
}
let parsed;
try {
parsed = JSON.parse(data);
} catch {
return;
}
const chunk = record(parsed);
if (chunk === void 0) return;
receivedEvent = true;
const failure = record(chunk.error);
if (failure !== void 0) {
closeReasoning(controller);
closeText(controller);
controller.enqueue({
error: failure,
type: "error"
});
finishReason = {
raw: text(failure.type) ?? "error",
unified: "error"
};
return;
}
if (!sentMetadata) {
const created = count(chunk.created);
const id = text(chunk.id);
const modelId = text(chunk.model);
if (id !== void 0 || modelId !== void 0) {
sentMetadata = true;
controller.enqueue({
id,
modelId,
timestamp: created === void 0 ? void 0 : /* @__PURE__ */ new Date(created * 1e3),
type: "response-metadata"
});
}
}
if (record(chunk.usage) !== void 0) usage = mapUsage(chunk.usage);
const choice = record(array(chunk.choices)?.[0]);
const delta = record(choice?.delta);
if (delta !== void 0) {
messageExtra ??= record(delta.extra_content);
const reasoning = text(delta.reasoning_content);
if (reasoning !== void 0 && reasoning !== "") {
if (reasoningId === void 0) {
reasoningId = newId();
controller.enqueue({
id: reasoningId,
type: "reasoning-start"
});
}
controller.enqueue({
delta: reasoning,
id: reasoningId,
type: "reasoning-delta"
});
}
const content = text(delta.content);
if (content !== void 0 && content !== "") {
closeReasoning(controller);
if (textId === void 0) {
textId = newId();
textEverOpened = true;
controller.enqueue({
id: textId,
type: "text-start"
});
}
controller.enqueue({
delta: content,
id: textId,
type: "text-delta"
});
}
emitToolDeltas(delta.tool_calls, controller);
}
if (text(choice?.finish_reason) !== void 0) finishReason = mapFinishReason(choice?.finish_reason);
}
});
}
//#endregion
//#region src/models/ai-sdk/models/language.ts
function callHeaders(options, resolved) {
const headers = definedHeaders(options.headers);
Object.assign(headers, resolved.headers);
if (resolved.sessionAffinity !== void 0) headers["x-session-affinity"] = resolved.sessionAffinity;
return headers;
}
/**
* Builds the `providerMetadata.cloudflare` block every model in this module
* attaches to its result. Shared with the non-language models in
* `models/modality.ts`.
*/
function cloudflareMetadata(model, gateway, response, transport, provider) {
const headers = response.headers;
const rawStep = headers.get("cf-aig-step");
const step = rawStep === null ? NaN : Number(rawStep);
const metadata = {
...gateway === void 0 ? {} : { gateway },
model,
...provider === void 0 ? {} : { provider },
...Number.isNaN(step) ? {} : { step }
};
for (const [field, header] of [
["cacheStatus", "cf-aig-cache-status"],
["eventId", "cf-aig-event-id"],
["requestId", "cf-aig-request-id"],
["runId", "cf-aig-run-id"],
["traceId", "cf-aig-trace-id"]
]) {
const value = headers.get(header);
if (value !== null) metadata[field] = value;
}
const logId = headers.get("cf-aig-log-id") ?? transport.logIdFallback(response);
if (logId !== void 0 && logId !== null) metadata.logId = logId;
return { [PROVIDER_OPTIONS_KEY]: metadata };
}
function buildRequest(modelId, options, resolved, stream) {
return buildOpenAIRequest(options, {
chatTemplateKwargs: resolved.chatTemplateKwargs,
modelId,
reasoningEffort: resolved.reasoningEffort,
stream
});
}
/**
* Prepends the single `stream-start` part and stamps `providerMetadata` onto
* the `finish` part. A `TransformStream` rather than a manual enqueue so the
* pipeline keeps its backpressure and error propagation.
*/
function decorateStream(warnings, providerMetadata) {
let started = false;
const start = (controller) => {
if (started) return;
started = true;
controller.enqueue({
type: "stream-start",
warnings
});
};
return new TransformStream({
flush(controller) {
start(controller);
},
transform(part, controller) {
start(controller);
controller.enqueue(part.type === "finish" ? {
...part,
providerMetadata
} : part);
}
});
}
/**
* Replays an already-complete generation as a stream. Workers AI sometimes
* answers a `stream: true` request with a plain JSON body; degrading
* gracefully beats failing the call.
*/
function oneShotStream(generation) {
return new ReadableStream({ start(controller) {
if (generation.responseId !== void 0 || generation.responseModelId !== void 0) controller.enqueue({
id: generation.responseId,
modelId: generation.responseModelId,
timestamp: generation.timestamp,
type: "response-metadata"
});
for (const part of generation.content) emitContent(part, controller);
controller.enqueue({
finishReason: generation.finishReason,
type: "finish",
usage: generation.usage
});
controller.close();
} });
}
function emitContent(part, controller) {
switch (part.type) {
case "text": {
const id = newId();
controller.enqueue({
id,
type: "text-start"
});
controller.enqueue({
delta: part.text,
id,
type: "text-delta"
});
controller.enqueue({
id,
type: "text-end",
...part.providerMetadata === void 0 ? {} : { providerMetadata: part.providerMetadata }
});
break;
}
case "reasoning": {
const id = newId();
controller.enqueue({
id,
type: "reasoning-start"
});
controller.enqueue({
delta: part.text,
id,
type: "reasoning-delta"
});
controller.enqueue({
id,
type: "reasoning-end",
...part.providerMetadata === void 0 ? {} : { providerMetadata: part.providerMetadata }
});
break;
}
case "tool-call":
controller.enqueue({
id: part.toolCallId,
toolName: part.toolName,
type: "tool-input-start"
});
controller.enqueue({
delta: part.input,
id: part.toolCallId,
type: "tool-input-delta"
});
controller.enqueue({
id: part.toolCallId,
type: "tool-input-end"
});
controller.enqueue(part);
break;
default: controller.enqueue(part);
}
}
var _config$2 = /* @__PURE__ */ new WeakMap();
var _options$2 = /* @__PURE__ */ new WeakMap();
var _CloudflareLanguageModel_brand = /* @__PURE__ */ new WeakSet();
/**
* A `LanguageModelV4` backed by the Cloudflare run path. The model id decides
* the wire format; nothing about transport or wire format reaches user land.
*/
var CloudflareLanguageModel = class {
constructor(modelId, options, config) {
_classPrivateMethodInitSpec(this, _CloudflareLanguageModel_brand);
this.specificationVersion = "v4";
this.provider = "cloudflare";
_classPrivateFieldInitSpec(this, _config$2, void 0);
_classPrivateFieldInitSpec(this, _options$2, void 0);
this.modelId = modelId;
_classPrivateFieldSet2(_options$2, this, options);
_classPrivateFieldSet2(_config$2, this, config);
rememberOptions(this, options);
this.supportedUrls = {};
}
async doGenerate(options) {
const call = parseCallOptions(options.providerOptions);
const resolved = _assertClassBrand(_CloudflareLanguageModel_brand, this, _resolve$1).call(this, call);
const legOptions = withoutCallFallback(options);
return _assertClassBrand(_CloudflareLanguageModel_brand, this, _withFallback$1).call(this, call, (model) => model.doGenerate(legOptions), async (modelId) => {
const request = buildRequest(modelId, options, resolved, false);
const response = await _assertClassBrand(_CloudflareLanguageModel_brand, this, _send).call(this, modelId, request, options, resolved);
const generation = parseOpenAIGeneration(await _assertClassBrand(_CloudflareLanguageModel_brand, this, _readBody).call(this, modelId, request, response), modelId);
return {
content: generation.content,
finishReason: generation.finishReason,
providerMetadata: cloudflareMetadata(modelId, resolved.gateway.id, response, _classPrivateFieldGet2(_config$2, this).transport),
request: { body: request.body },
response: {
headers: headersToObject(response.headers),
id: generation.responseId,
modelId: generation.responseModelId ?? modelId,
timestamp: generation.timestamp
},
usage: generation.usage,
warnings: request.warnings
};
});
}
async doStream(options) {
const call = parseCallOptions(options.providerOptions);
const resolved = _assertClassBrand(_CloudflareLanguageModel_brand, this, _resolve$1).call(this, call);
const legOptions = withoutCallFallback(options);
return _assertClassBrand(_CloudflareLanguageModel_brand, this, _withFallback$1).call(this, call, (model) => model.doStream(legOptions), async (modelId) => {
const request = buildRequest(modelId, options, resolved, true);
const response = await _assertClassBrand(_CloudflareLanguageModel_brand, this, _send).call(this, modelId, request, options, resolved);
const providerMetadata = cloudflareMetadata(modelId, resolved.gateway.id, response, _classPrivateFieldGet2(_config$2, this).transport);
const contentType = response.headers.get("content-type") ?? "";
const decorate = decorateStream(request.warnings, providerMetadata);
let parts;
if (!contentType.includes("event-stream") || response.body === null) parts = oneShotStream(parseOpenAIGeneration(await _assertClassBrand(_CloudflareLanguageModel_brand, this, _readBody).call(this, modelId, request, response), modelId));
else parts = response.body.pipeThrough(sseDataStream()).pipeThrough(normalizeChatCompletionsStream(modelId, { reasoningOff: request.reasoningOff })).pipeThrough(openAIStreamParser());
return {
request: { body: request.body },
response: { headers: headersToObject(response.headers) },
stream: parts.pipeThrough(decorate)
};
}, "stream");
}
};
/**
* Reads a 2xx body as JSON and unwraps the Cloudflare envelope. A 2xx that
* is not JSON at all is a provider failure, not a caller bug, so it comes
* back as a {@link CloudflareAIError} rather than a bare `SyntaxError`.
*/
async function _readBody(modelId, request, response) {
let json;
try {
json = await response.json();
} catch (error) {
throw new CloudflareAIError({
cause: error,
code: "provider-error",
isRetryable: false,
message: `The model answered ${response.status} with a body that is not JSON.`,
model: modelId,
requestBodyValues: request.body,
status: response.status,
url: _classPrivateFieldGet2(_config$2, this).transport.url
});
}
return unwrapEnvelope(json, {
model: modelId,
status: response.status,
url: _classPrivateFieldGet2(_config$2, this).transport.url
});
}
function _resolve$1(call) {
return resolveOptions(_classPrivateFieldGet2(_config$2, this).providerGateway, _classPrivateFieldGet2(_options$2, this), call);
}
async function _send(modelId, request, options, resolved) {
const response = await _classPrivateFieldGet2(_config$2, this).transport.run({
format: "chat-completions",
gateway: resolved.gateway,
headers: callHeaders(options, resolved),
input: request.body,
model: modelId,
signal: options.abortSignal
});
if (!response.ok) throw await errorFromResponse(response, {
model: modelId,
requestBodyValues: request.body,
url: _classPrivateFieldGet2(_config$2, this).transport.url
});
return response;
}
/**
* Runs this model, then each fallback leg in order. A leg named by id runs
* through this same pipeline; a leg that is a model object — a vendor model
* `ai()` already wrapped, or another provider's — is asked directly, so a
* chain can mix Workers AI with anything the AI SDK can reach.
*/
function _withFallback$1(call, runLeg, runSelf, mode = "generate") {
const legs = fallbackLegs(_classPrivateFieldGet2(_options$2, this), call);
const chain = (attempts) => mode === "stream" ? withStreamFallbackLegs(attempts, _classPrivateFieldGet2(_config$2, this).transport.url) : withFallbackLegs(attempts, _classPrivateFieldGet2(_config$2, this).transport.url);
return chain([{
model: this.modelId,
run: () => runSelf(this.modelId)
}, ...legs.map((leg) => typeof leg === "string" ? {
model: requireLanguageModelId(leg),
run: () => runSelf(leg)
} : {
model: leg.modelId,
run: () => runLeg(leg)
})]);
}
//#endregion
//#region src/models/ai-sdk/models/gateway.ts
/**
* Throws unless a model's provider keeps its settings where every `@ai-sdk/*`
* provider keeps them. Checked when the model is wrapped rather than when it
* is called, so the mistake is reported at the line that made it.
*/
function requireConfig(model) {
const config = model.config;
if (config === null || typeof config !== "object") throw new TypeError(`The model "${String(model.modelId ?? "unknown")}" from provider "${String(model.provider ?? "unknown")}" has no \`config\` setting, so AI Gateway cannot take over its requests. Every \`@ai-sdk/*\` provider has one; a middleware-wrapped or hand-written model does not. Wrap the provider's own model first and apply middleware afterwards: wrapLanguageModel({ model: ai(anthropic("claude-opus-4-8")), middleware }).`);
return config;
}
/**
* A per-call copy of the upstream model whose `config.fetch` is ours. The
* user's model object is never touched: it may be shared across requests, and
* two concurrent calls must not see each other's transport.
*/
function cloneWithFetch(model, fetchImpl) {
const config = requireConfig(model);
const clone = Object.create(Object.getPrototypeOf(model));
Object.defineProperties(clone, Object.getOwnPropertyDescriptors(model));
Object.defineProperty(clone, "config", {
configurable: true,
enumerable: true,
value: {
...config,
fetch: fetchImpl
},
writable: true
});
return clone;
}
/** The request URL, whichever of `fetch`'s two call shapes was used. */
function requestUrl(input) {
if (typeof input === "string") return input;
if (input instanceof URL) return input.href;
return input.url;
}
/**
* The body of either `fetch` call form: the `RequestInit`'s when one was
* given, else the `Request`'s own, from a clone so the caller's object is
* neither consumed nor mutated.
*/
function requestBody(input, init) {
if (init?.body !== void 0) return init.body;
if (input instanceof Request && input.body !== null) return input.clone().body;
}
/** The method of either `fetch` call form. */
function requestMethod(input, init) {
return init?.method ?? (input instanceof Request ? input.method : "POST");
}
/** The vendor model id out of a JSON body, for error reporting. */
function modelOfBody(body) {
if (typeof body !== "string") return void 0;
try {
return modelOf(JSON.parse(body));
} catch {
return;
}
}
/**
* The vendor's own request headers, minus the length of a body we are about
* to re-serialize, and minus the vendor's credential unless the model asked
* for it to be forwarded.
*/
function forwardedHeaders(init, input, route, vendorKey) {
const source = init?.headers ?? (input instanceof Request ? input.headers : void 0);
const dropped = /* @__PURE__ */ new Set([...vendorKey === "request" ? [] : route.authHeaders, "content-length"]);
const headers = {};
new Headers(source).forEach((value, key) => {
if (!dropped.has(key.toLowerCase())) headers[key] = value;
});
return headers;
}
/** A response whose body can be read again, for the vendor's error handler. */
function replayed(response, text) {
const bodyless = response.status === 204 || response.status === 205 || response.status === 304;
return new Response(bodyless ? null : text, {
headers: response.headers,
status: response.status,
statusText: response.statusText
});
}
/**
* The `fetch` a routed model is given. It maps the vendor's URL onto a gateway
* provider slug and endpoint and sends the request there unchanged, through
* the binding; the answer is the vendor's own.
*/
function routedFetch(context) {
const routed = async (input, init) => {
const route = requireGatewayProvider(requestUrl(input));
const body = requestBody(input, init);
const { resolved } = context;
const response = await context.config.transport.native({
body,
endpoint: route.endpoint,
gateway: resolved.gateway,
headers: {
...forwardedHeaders(init, input, route, context.vendorKey),
...resolved.headers,
...resolved.sessionAffinity === void 0 ? {} : { "x-session-affinity": resolved.sessionAffinity }
},
method: requestMethod(input, init),
provider: route.provider,
signal: init?.signal ?? (input instanceof Request ? input.signal : void 0) ?? context.signal
});
context.capture.provider = route.provider;
context.capture.response = response;
if (response.ok) return response;
const text = await response.text();
let envelope;
try {
envelope = JSON.parse(text);
} catch {
return replayed(response, text);
}
if (!isGatewayErrorEnvelope(envelope)) return replayed(response, text);
throw errorFromGatewayEnvelope(envelope, {
model: modelOfBody(body) ?? route.provider,
requestBodyValues: void 0,
responseBody: text,
responseHeaders: headersToObject(response.headers),
status: response.status,
url: context.config.transport.url
});
};
return routed;
}
/** The model id out of a vendor body, for error reporting. */
function modelOf(query) {
if (query === null || typeof query !== "object") return void 0;
const model = query.model;
return typeof model === "string" ? model : void 0;
}
/** The call options a routed model sees: ours are read out and removed. */
function withoutOurOptions(options) {
const providerOptions = options.providerOptions;
if (providerOptions === void 0 || !("cloudflare" in providerOptions)) return options;
const rest = { ...providerOptions };
delete rest[PROVIDER_OPTIONS_KEY];
return {
...options,
providerOptions: rest
};
}
/** Adds our metadata block without dropping the vendor's own keys. */
function mergeMetadata(existing, ours) {
return {
...existing,
...ours
};
}
/**
* The same metadata, in the shape an image result's provider metadata takes.
*
* `ImageModelV4ProviderMetadata` is not the shared shape: every provider block
* must carry an `images` array, one entry per generated image, and the AI SDK
* spreads that array when it merges the calls a multi-image request fanned out
* into. A block without one makes `generateImage` throw, and only the array's
* entries survive that merge, so ours goes inside it.
*/
function metadataFor(result, ours) {
const images = result.images;
if (!Array.isArray(images)) return ours;
const block = ours[PROVIDER_OPTIONS_KEY];
return {
...ours,
[PROVIDER_OPTIONS_KEY]: {
...block,
images: images.map(() => ({ ...block }))
}
};
}
/**
* The Workers AI knobs a routed vendor model cannot honour. They are not
* dropped silently: the vendor decides its own reasoning, so a caller who set
* `reasoningEffort` here is asking for something that never leaves.
*/
function ignoredWarnings(resolved) {
const warnings = [];
for (const [option, value] of [["reasoningEffort", resolved.reasoningEffort], ["chatTemplateKwargs", resolved.chatTemplateKwargs]]) {
if (value === void 0) continue;
warnings.push({
details: `\`${option}\` is a Workers AI setting and does not apply to a model routed through AI Gateway; use the vendor's own reasoning options instead.`,
feature: option,
type: "unsupported"
});
}
return warnings;
}
/**
* The Workers AI knobs a routed non-language model was asked with, added to
* the result's own `warnings` when it has one.
*
* Only the per-call `providerOptions.cloudflare` bag can carry them here —
* {@link RoutedModelOptions} leaves them off the per-model type — and only
* some v4 result shapes have somewhere to put a warning: image, speech,
* transcription and reranking results do, an embedding result does not.
*/
function routedWarnings(result, resolved) {
const existing = result.warnings;
if (!Array.isArray(existing)) return {};
const warnings = ignoredWarnings(resolved);
if (warnings.length === 0) return {};
return { warnings: [...existing, ...warnings] };
}
/**
* Stamps the finish part with `providerMetadata.cloudflare`, and adds our own
* warnings to the `stream-start` the vendor emitted.
*/
function stampStream(metadata, warnings) {
return new TransformStream({ transform(part, controller) {
if (part.type === "finish") {
controller.enqueue({
...part,
providerMetadata: mergeMetadata(part.providerMetadata, metadata())
});
return;
}
if (part.type === "stream-start" && warnings.length > 0) {
controller.enqueue({
...part,
warnings: [...part.warnings, ...warnings]
});
return;
}
controller.enqueue(part);
} });
}
var _model = /* @__PURE__ */ new WeakMap();
var _options$1 = /* @__PURE__ */ new WeakMap();
var _config$1 = /* @__PURE__ */ new WeakMap();
var _GatewayLanguageModel_brand = /* @__PURE__ */ new WeakSet();
/**
* A `LanguageModelV4` that is the user's own vendor model, routed through AI
* Gateway. Generation, parsing, tool handling and error shapes stay the
* vendor's; only the transport is Cloudflare's.
*/
var GatewayLanguageModel = class {
constructor(model, options, config) {
_classPrivateMethodInitSpec(this, _GatewayLanguageModel_brand);
this.specificationVersion = "v4";
_classPrivateFieldInitSpec(this, _model, void 0);
_classPrivateFieldInitSpec(this, _options$1, void 0);
_classPrivateFieldInitSpec(this, _config$1, void 0);
requireConfig(model);
_classPrivateFieldSet2(_model, this, model);
_classPrivateFieldSet2(_options$1, this, options);
_classPrivateFieldSet2(_config$1, this, config);
rememberOptions(this, options);
}
/** The vendor's own provider name — `anthropic.messages`, `openai.responses`. */
get provider() {
return _classPrivateFieldGet2(_model, this).provider;
}
/** The vendor's own id spelling, which the gateway requires verbatim. */
get modelId() {
return _classPrivateFieldGet2(_model, this).modelId;
}
get supportedUrls() {
return _classPrivateFieldGet2(_model, this).supportedUrls;
}
/** The model this wraps, for callers that need the vendor object back. */
get model() {
return _classPrivateFieldGet2(_model, this);
}
async doGenerate(options) {
const call = parseCallOptions(options.providerOptions);
const resolved = _assertClassBrand(_GatewayLanguageModel_brand, this, _resolve).call(this, call);
const upstream = withoutOurOptions(options);
const legOptions = withoutCallFallback(options);
return _assertClassBrand(_GatewayLanguageModel_brand, this, _withFallback).call(this, call, (leg) => leg.doGenerate(legOptions), async () => {
const capture = {};
const result = await _assertClassBrand(_GatewayLanguageModel_brand, this, _route).call(this, resolved, capture, options.abortSignal).doGenerate(upstream);
const warnings = ignoredWarnings(resolved);
return {
...result,
providerMetadata: mergeMetadata(result.providerMetadata, _assertClassBrand(_GatewayLanguageModel_brand, this, _metadata).call(this, resolved, capture)),
response: _assertClassBrand(_GatewayLanguageModel_brand, this, _response).call(this, result.response, capture),
warnings: warnings.length > 0 ? [...result.warnings, ...warnings] : result.warnings
};
});
}
async doStream(options) {
const call = parseCallOptions(options.providerOptions);
const resolved = _assertClassBrand(_GatewayLanguageModel_brand, this, _resolve).call(this, call);
const upstream = withoutOurOptions(options);
const legOptions = withoutCallFallback(options);
return _assertClassBrand(_GatewayLanguageModel_brand, this, _withFallback).call(this, call, (leg) => leg.doStream(legOptions), async () => {
const capture = {};
const result = await _assertClassBrand(_GatewayLanguageModel_brand, this, _route).call(this, resolved, capture, options.abortSignal).doStream(upstream);
return {
...result,
response: _assertClassBrand(_GatewayLanguageModel_brand, this, _response).call(this, result.response, capture),
stream: result.stream.pipeThrough(stampStream(() => _assertClassBrand(_GatewayLanguageModel_brand, this, _metadata).call(this, resolved, capture), ignoredWarnings(resolved)))
};
}, "stream");
}
};
function _route(resolved, capture, signal) {
return cloneWithFetch(_classPrivateFieldGet2(_model, this), routedFetch({
capture,
config: _classPrivateFieldGet2(_config$1, this),
resolved,
signal,
vendorKey: _classPrivateFieldGet2(_options$1, this)?.vendorKey ?? "gateway"
}));
}
function _resolve(call) {
return resolveOptions(_classPrivateFieldGet2(_config$1, this).providerGateway, _classPrivateFieldGet2(_options$1, this), call);
}
function _metadata(resolved, capture) {
const response = capture.response ?? new Response(null);
return cloudflareMetadata(this.modelId, resolved.gateway.id, response, _classPrivateFieldGet2(_config$1, this).transport, capture.provider);
}
function _response(response, capture) {
if (capture.response === void 0) return response;
const headers = headersToObject(capture.response.headers);
return {
...response,
headers: {
...response?.headers,
...headers
}
};
}
/**
* The vendor model first, then each fallback leg. A leg named by id is a
* Workers AI model; a leg that is a model object is asked as it stands, so
* `ai()` has already wrapped it if it needed wrapping.
*/
function _withFallback(call, runLeg, run, mode = "generate") {
const legs = fallbackLegs(_classPrivateFieldGet2(_options$1, this), call);
if (legs.length === 0) return run();
const chain = (attempts) => mode === "stream" ? withStreamFallbackLegs(attempts, _classPrivateFieldGet2(_config$1, this).transport.url) : withFallbackLegs(attempts, _classPrivateFieldGet2(_config$1, this).transport.url);
return chain([{
model: this.modelId,
run
}, ...legs.map((leg) => {
const model = typeof leg === "string" ? _assertClassBrand(_GatewayLanguageModel_brand, this, _workersAI).call(this, leg) : leg;
return {
model: model.modelId,
run: () => runLeg(model)
};
})]);
}
/** A Workers AI leg, carrying this model's gateway options but no chain. */
function _workersAI(modelId) {
return new CloudflareLanguageModel(requireLanguageModelId(modelId), {
..._classPrivateFieldGet2(_options$1, this),
fallback: void 0
}, _classPrivateFieldGet2(_config$1, this));
}
/**
* Routes any other AI SDK v4 model the vendor built — embedding, image,
* speech, transcription — through the same provider-native request. Only
* `fetch` is swapped; every `do…` method stays the vendor's, and its result
* gains `providerMetadata.cloudflare` when the result is an object that can
* carry one.
*/
function routed(model, options, config) {
requireConfig(model);
return new Proxy(model, { get(target, property, receiver) {
const value = Reflect.get(target, property, receiver);
if (typeof value !== "function" || typeof property !== "string" || !property.startsWith("do")) return value;
return async (call) => {
const resolved = resolveOptions(config.providerGateway, options, parseCallOptions(call?.providerOptions));
const upstream = call?.providerOptions === void 0 ? call : withoutOurOptions(call);
const capture = {};
const clone = cloneWithFetch(target, routedFetch({
capture,
config,
resolved,
signal: call?.abortSignal,
vendorKey: options?.vendorKey ?? "gateway"
}));
const result = await Reflect.get(clone, property).call(clone, upstream);
if (result === null || typeof result !== "object") return result;
const existing = result.providerMetadata;
return {
...result,
...routedWarnings(result, resolved),
providerMetadata: mergeMetadata(existing, metadataFor(result, cloudflareMetadata(String(target.modelId ?? ""), resolved.gateway.id, capture.response ?? new Response(null), config.transport, capture.provider)))
};
};
} });
}
//#endregion
//#region src/models/ai-sdk/models/modality.ts
/** Whether the caller set any gateway option, the id included. */
function gatewayConfigured(gateway) {
const { id, ...options } = gateway;
return id !== "default" || Object.keys(options).length > 0;
}
/** The `unsupported` warning shape, which every modality needs. */
function unsupported(feature, details) {
return {
feature,
type: "unsupported",
...details === void 0 ? {} : { details }
};
}
/** Decodes base64 into bytes; passes `Uint8Array` input straight through. */
function toBytes(data) {
if (typeof data !== "string") return data;
const binary = atob(data);
const bytes = new Uint8Array(binary.length);
for (let index = 0; index < binary.length; index++) bytes[index] = binary.charCodeAt(index);
return bytes;
}
var _config = /* @__PURE__ */ new WeakMap();
var _options = /* @__PURE__ */ new WeakMap();
var _CloudflareModalityModel_brand = /* @__PURE__ */ new WeakSet();
/**
* A model over the Cloudflare run path that is not a language model. Holds
* the one call path all four modalities share; each subclass only builds a
* body and reads an answer.
*/
var CloudflareModalityModel = class {
constructor(modelId, options, config) {
_classPrivateMethodInitSpec(this, _CloudflareModalityModel_brand);
this.specificationVersion = "v4";
this.provider = "cloudflare";
_classPrivateFieldInitSpec(this, _config, void 0);
_classPrivateFieldInitSpec(this, _options, void 0);
options?.fallback?.forEach((leg) => {
if (typeof leg === "string") requireWorkersAIId(leg);
});
this.modelId = modelId;
_classPrivateFieldSet2(_options, this, options);
_classPrivateFieldSet2(_config, this, config);
}
/** The endpoint this model calls, for error reporting. */
get endpoint() {
return _classPrivateFieldGet2(_config, this).transport.url;
}
/**
* Sends one call, trying each fallback leg in turn, and decodes the answer
* as JSON or as raw bytes depending on what the model sent back.
*/
async send(call) {
const resolved = resolveOptions(_classPrivateFieldGet2(_config, this).providerGateway, _classPrivateFieldGet2(_options, this), parseModalityCallOptions(call.providerOptions));
const transport = _classPrivateFieldGet2(_config, this).transport;
return withFallback(this.modelId, resolved.fallback, transport.url, async (modelId) => {
const request = call.build(modelId);
const direct = request.audio !== void 0;
const sent = request.audio === void 0 ? request.input : {
...request.input,
audio: {
body: toBase64(request.audio.bytes),
contentType: request.audio.contentType
}
};
const warnings = direct && gatewayConfigured(resolved.gateway) ? [...request.warnings, unsupported("gateway", `${modelId} goes to Workers AI directly, so the gateway options do not apply.`)] : request.warnings;
const response = await transport.run({
...request.audio === void 0 ? {} : { audio: request.audio },
gateway: resolved.gateway,
headers: {
...definedHeaders(call.headers),
...resolved.headers,
...resolved.sessionAffinity === void 0 ? {} : { "x-session-affinity": resolved.sessionAffinity }
},
input: request.input,
model: modelId,
signal: call.abortSignal
});
if (!response.ok) throw await errorFromResponse(response, {
model: modelId,
requestBodyValues: sent,
url: transport.url
});
const mediaType = (response.headers.get("content-type") ?? "").split(";")[0].trim().toLowerCase();
const body = await _assertClassBrand(_CloudflareModalityModel_brand, this, _decode).call(this, response, modelId, sent, mediaType);
const answer = {
headers: headersToObject(response.headers),
input: sent,
mediaType,
modelId,
providerMetadata: cloudflareMetadata(modelId, direct ? void 0 : resolved.gateway.id, response, transport),
timestamp: /* @__PURE__ */ new Date(),
url: transport.url,
warnings,
...body
};
call.check?.(answer);
return answer;
});
}
};
/**
* Reads a 2xx body. A JSON content type is parsed and unwrapped; anything
* else is the model's own bytes (PNG, JPEG, MP3). A JSON content type with a
* body that will not parse is a provider failure, not a caller bug, so it
* comes back as a {@link CloudflareAIError} rather than a bare `SyntaxError`.
*/
async function _decode(response, modelId, input, mediaType) {
if (!mediaType.includes("json")) return {
bytes: new Uint8Array(await response.arrayBuffer()),
json: void 0
};
let json;
try {
json = await response.json();
} catch (error) {
throw new CloudflareAIError({
cause: error,
code: "provider-error",
isRetryable: false,
message: `The model answered ${response.status} with a body that is not JSON.`,
model: modelId,
requestBodyValues: input,
status: response.status,
url: _classPrivateFieldGet2(_config, this).transport.url
});
}
return {
bytes: void 0,
json: unwrapEnvelope(json, {
model: modelId,
status: response.status,
url: _classPrivateFieldGet2(_config, this).transport.url
})
};
}
//#endregion
//#region src/models/ai-sdk/models/image.ts
function imageOptions(providerOptions) {
const raw = record(providerOptions?.["cloudflare"]) ?? {};
const negativePrompt = text(raw.negativePrompt);
return {
...count(raw.steps) === void 0 ? {} : { steps: count(raw.steps) },
...count(raw.guidance) === void 0 ? {} : { guidance: count(raw.guidance) },
...count(raw.strength) === void 0 ? {} : { strength: count(raw.strength) },
...negativePrompt === void 0 ? {} : { negativePrompt }
};
}
/** `1024x768` → `{ width: 1024, height: 768 }`. */
function parseSize(size) {
const [width, height] = size.split("x").map(Number);
return Number.isFinite(width) && Number.isFinite(height) ? {
height,
width
} : void 0;
}
/** The base64 bytes of a file the caller passed for img2img or masking. */
function fileData(file) {
if (file.type === "file") return toBase64(file.data);
}
/**
* Builds the run-path body. The knobs and the flux-1 subset rule live in
* `core/images.ts`, shared with the pi-ai module so the two cannot drift; the
* AI SDK call options and the `files`/`mask` byte handling are mapped here.
*/
function buildImageRequest(modelId, options) {
const warnings = [];
const extra = imageOptions(options.providerOptions);
if (options.prompt === void 0) warnings.push({
message: "Cloudflare image models require a prompt; sent an empty one.",
type: "other"
});
if (options.aspectRatio !== void 0) warnings.push(unsupported("aspectRatio", "Use `size` instead: Workers AI takes pixels."));
let size;
if (options.size !== void 0) {
size = parseSize(options.size);
if (size === void 0) warnings.push(unsupported("size", `Could not read "${options.size}".`));
}
const built = buildImageInput(modelId, options.prompt ?? "", {
...size === void 0 ? {} : {
height: size.height,
width: size.width
},
...options.seed === void 0 ? {} : { seed: options.seed },
...extra.steps === void 0 ? {} : { steps: extra.steps },
...extra.guidance === void 0 ? {} : { guidance: extra.guidance },
...extra.strength === void 0 ? {} : { strength: extra.strength },
...extra.negativePrompt === void 0 ? {} : { negativePrompt: extra.negativePrompt },
...options.mask === void 0 ? {} : { mask: true },
...options.files?.[0] === void 0 ? {} : { sourceImage: true }
});
const input = built.input;
for (const warning of built.warnings) warnings.push(unsupported(warning.feature, warning.message));
if (built.minimal) return {
input,
warnings
};
const source = options.files?.[0];
if (source !== void 0) {
const data = fileData(source);
if (data === void 0) warnings.push(unsupported("files", "Image URLs are not supported; send the bytes."));
else input.image_b64 = data;
if ((options.files?.length ?? 0) > 1) warnings.push(unsupported("files", "Only the first image is used for img2img."));
}
if (options.mask !== void 0) {
const data = fileData(options.mask);
if (data === void 0) warnings.push(unsupported("mask", "Mask URLs are not supported; send the bytes."));
else input.mask = Array.from(toBytes(data));
}
return {
input,
warnings
};
}
function readImage(answer) {
if (answer.bytes !== void 0) return {
base64: void 0,
bytes: answer.bytes,
mediaType: answer.mediaType === "" ? "image/png" : answer.mediaType
};
const base64 = text((record(answer.json) ?? {}).image);
if (base64 === void 0) throw new CloudflareAIError({
code: "provider-error",
data: answer.json,
isRetryable: false,
message: "The image model answered JSON without an `image` field.",
model: answer.modelId,
requestBodyValues: answer.input,
url: answer.url
});
return {
base64,
bytes: void 0,
mediaType: imageMediaTypeOf(base64)
};
}
function imageUsage(json) {
const usage = record(record(json)?.usage);
if (usage === void 0) return void 0;
return {
inputTokens: count(usage.prompt_tokens),
outputTokens: count(usage.completion_tokens),
totalTokens: count(usage.total_tokens)
};
}
/**
* An `ImageModelV4` over the Cloudflare text-to-image catalog.
*
* The run path generates one image per call, so `maxImagesPerCall` is 1 and
* `generateImage` fans a larger `n` out itself; a direct `doGenerate` with
* `n > 1` is fanned out here instead.
*/
var CloudflareImageModel = class extends CloudflareModalityModel {
constructor(modelId, options, config) {
super(modelId, options, config);
this.maxImagesPerCall = 1;
}
async doGenerate(options) {
const wanted = Math.max(1, options.n);
const answers = await Promise.all(Array.from({ length: wanted }, () => this.send({
abortSignal: options.abortSignal,
build: (modelId) => buildImageRequest(modelId, options),
check: (answer) => {
readImage(answer);
},
headers: options.headers,
providerOptions: options.providerOptions
})));
const first = answers[0];
const generated = answers.map(readImage);
const images = generated.some((image) => image.base64 !== void 0) ? generated.map((image) => image.base64 ?? toBase64(image.bytes)) : generated.map((image) => image.bytes);
const usage = imageUsage(first.json);
return {
images,
providerMetadata: { [PROVIDER_OPTIONS_KEY]: {
...first.providerMetadata[PROVIDER_OPTIONS_KEY],
images: generated.map((image, index) => ({
...answers[index]?.providerMetadata[PROVIDER_OPTIONS_KEY],
mediaType: image.mediaType
}))
} },
response: {
headers: first.headers,
modelId: first.modelId,
timestamp: first.timestamp
},
...usage === void 0 ? {} : { usage },
warnings: first.warnings
};
}
};
//#endregion
//#region src/models/ai-sdk/models/reranking.ts
/** Builds the run-path body for one reranking leg. */
function buildRerankingRequest(options) {
const warnings = [];
const documents = options.documents.type === "text" ? options.documents.values : options.documents.values.map((value) => JSON.stringify(value));
if (options.documents.type === "object") warnings.push({
details: "Object documents are sent as JSON text.",
feature: "documents",
type: "compatibility"
});
const input = {
contexts: documents.map((value) => ({ text: value })),
query: options.query
};
if (options.topN !== void 0) input.top_k = options.topN;
return {
input,
warnings
};
}
/**
* A `RerankingModelV4` over the Workers AI reranker catalog.
*/
var CloudflareRerankingModel = class extends CloudflareModalityModel {
constructor(modelId, options, config) {
super(modelId, options, config);
}
async doRerank(options) {
const answer = await this.send({
abortSignal: options.abortSignal,
build: () => buildRerankingRequest(options),
headers: options.headers,
providerOptions: options.providerOptions
});
const ranking = (array((record(answer.json) ?? {}).response) ?? []).flatMap((entry) => {
const row = record(entry);
const index = count(row?.id);
const relevanceScore = count(row?.score);
if (index === void 0 || relevanceScore === void 0) return [];
return [{
index,
relevanceScore
}];
});
ranking.sort((left, right) => right.relevanceScore - left.relevanceScore);
return {
providerMetadata: answer.providerMetadata,
ranking,
response: {
body: answer.json,
headers: answer.headers,
modelId: answer.modelId,
timestamp: answer.timestamp
},
warnings: answer.warnings
};
}
};
//#endregion
//#region src/models/ai-sdk/models/speech.ts
/**
* MeloTTS is the odd one out: the prompt key is `prompt`, the language key is
* `lang`, and there is no voice.
*
* @see https://developers.cloudflare.com/workers-ai/models/melotts/
*/
const MELOTTS = "@cf/myshell-ai/melotts";
/**
* Output encodings Aura accepts. `outputFormat` maps onto this; anything else
* is warned about rather than sent, because the run path validates the enum.
*
* @see https://developers.cloudflare.com/workers-ai/models/aura-1/
*/
const AURA_ENCODINGS = /* @__PURE__ */ new Set([
"linear16",
"flac",
"mulaw",
"alaw",
"mp3",
"opus",
"aac"
]);
/** Builds the run-path body for one speech leg. */
function buildSpeechRequest(modelId, options) {
const warnings = [];
if (options.speed !== void 0) warnings.push(unsupported("speed", "Workers AI speech models do not take a speed."));
if (options.instructions !== void 0) warnings.push(unsupported("instructions", "Workers AI speech models take text, not instructions."));
if (modelId === MELOTTS) {
const input = { prompt: options.text };
if (options.language !== void 0) input.lang = options.language;
if (options.voice !== void 0) warnings.push(unsupported("voice", `${MELOTTS} has a single voice.`));
if (options.outputFormat !== void 0) warnings.push(unsupported("outputFormat", `${MELOTTS} picks its own audio format.`));
return {
input,
warnings
};
}
const input = { text: options.text };
if (options.voice !== void 0) input.speaker = options.voice;
if (options.outputFormat !== void 0) if (AURA_ENCODINGS.has(options.outputFormat)) input.encoding = options.outputFormat;
else warnings.push(unsupported("outputFormat", `Expected one of ${[...AURA_ENCODINGS].join(", ")}.`));
if (options.language !== void 0) warnings.push(unsupported("language", "Aura picks its language from the model id, e.g. @cf/deepgram/aura-2-es."));
return {
input,
warnings
};
}
/**
* The audio in one answer. Bytes stay bytes and base64 stays base64: the AI
* SDK converts either way on demand, and converting here would only lose
* fidelity.
*/
function readAudio(answer) {
if (answer.bytes !== void 0) return answer.bytes;
const encoded = text((record(answer.json) ?? {}).audio);
if (encoded === void 0) throw new CloudflareAIError({
code: "provider-error",
data: answer.json,
isRetryable: false,
message: "The speech model answered JSON without an `audio` field.",
model: answer.modelId,
requestBodyValues: answer.input,
url: answer.url
});
return encoded;
}
/**
* A `SpeechModelV4` over the Workers AI text-to-speech catalog.
*/
var CloudflareSpeechModel = class extends CloudflareModalityModel {
constructor(modelId, options, config) {
super(modelId, options, config);
}
async doGenerate(options) {
const answer = await this.send({
abortSignal: options.abortSignal,
build: (modelId) => buildSpeechRequest(modelId, options),
check: (answer) => {
readAudio(answer);
},
headers: options.headers,
providerOptions: options.providerOptions
});
return {
audio: readAudio(answer),
providerMetadata: answer.providerMetadata,
request: { body: answer.input },
response: {
headers: answer.headers,
modelId: answer.modelId,
timestamp: answer.timestamp
},
warnings: answer.warnings
};
}
};
//#endregion
//#region src/models/ai-sdk/models/transcription.ts
/**
* Whisper ids that take the audio as an unsigned-byte array rather than as
* base64.
*
* @see https://developers.cloudflare.com/workers-ai/models/whisper/
*/
const BYTE_ARRAY_AUDIO = /* @__PURE__ */ new Set(["@cf/openai/whisper", "@cf/openai/whisper-tiny-en"]);
/**
* Models that declare `audio: { body, contentType }`. JSON cannot carry that
* body (base64 and byte arrays answer `400 required properties at '/audio' are
* 'body,contentType'`), so the audio travels as a raw body.
*/
const RAW_AUDIO = /* @__PURE__ */ new Set(["@cf/deepgram/nova-3"]);
/** Deepgram's Flux answers `only supports websocket connections` over HTTP. */
const WEBSOCKET_ONLY = /* @__PURE__ */ new Set(["@cf/deepgram/flux"]);
function transcriptionOptions(providerOptions) {
const raw = record(providerOptions?.["cloudflare"]) ?? {};
const vadFilter = raw.vadFilter;
return {
...text(raw.language) === void 0 ? {} : { language: text(raw.language) },
...text(raw.task) === void 0 ? {} : { task: text(raw.task) },
...text(raw.initialPrompt) === void 0 ? {} : { initialPrompt: text(raw.initialPrompt) },
...text(raw.prefix) === void 0 ? {} : { prefix: text(raw.prefix) },
...typeof vadFilter === "boolean" ? { vadFilter } : {}
};
}
/**
* Builds the request for a raw-audio model: the audio as a raw body, and
* `language` as a field. The whisper-only options have no equivalent there.
*/
function buildRawAudioRequest(modelId, options, extra) {
const warnings = [];
for (const feature of [
"task",
"initialPrompt",
"prefix",
"vadFilter"
]) if (extra[feature] !== void 0) warnings.push(unsupported(feature, `${modelId} has no ${feature}.`));
return {
audio: {
bytes: toBytes(options.audio),
contentType: options.mediaType
},
input: extra.language === void 0 ? {} : { language: extra.language },
warnings
};
}
/** Builds the run-path body for one leg. */
function buildTranscriptionRequest(modelId, options) {
const warnings = [];
const extra = transcriptionOptions(options.providerOptions);
if (RAW_AUDIO.has(modelId)) return buildRawAudioRequest(modelId, options, extra);
const input = BYTE_ARRAY_AUDIO.has(modelId) ? { audio: Array.from(toBytes(options.audio)) } : { audio: toBase64(options.audio) };
if (extra.language !== void 0) input.language = extra.language;
if (extra.task !== void 0) input.task = extra.task;
if (extra.initialPrompt !== void 0) input.initial_prompt = extra.initialPrompt;
if (extra.prefix !== void 0) input.prefix = extra.prefix;
if (extra.vadFilter !== void 0) input.vad_filter = extra.vadFilter;
return {
input,
warnings
};
}
/** Reads a timed `{ text|word, start, end }` entry as a segment. */
function timed(entry, textKeys) {
const item = record(entry);
if (item === void 0) return [];
const value = textKeys.map((key) => text(item[key])).find(Boolean);
if (value === void 0) return [];
return [{
endSecond: count(item.end) ?? 0,
startSecond: count(item.start) ?? 0,
text: value
}];
}
/**
* Reads a Deepgram answer: `results.channels[0].alternatives[0]`. Its
* sentences are the segments when it sends paragraphs, and its words
* otherwise.
*/
function readDeepgram(body) {
const channel = record(array(record(body.results)?.channels)?.[0]);
const alternative = record(array(channel?.alternatives)?.[0]);
const sentences = (array(record(alternative?.paragraphs)?.paragraphs) ?? []).flatMap((paragraph) => array(record(paragraph)?.sentences) ?? []);
const segments = sentences.length > 0 ? sentences.flatMap((sentence) => timed(sentence, ["text"])) : (array(alternative?.words) ?? []).flatMap((word) => timed(word, ["punctuated_word", "word"]));
return {
durationInSeconds: count(record(body.metadata)?.duration),
language: text(array(alternative?.languages)?.[0]) ?? text(channel?.detected_language),
segments,
text: text(alternative?.transcript) ?? ""
};
}
/** Reads a whisper answer. */
function readWhisper(body) {
const info = record(body.transcription_info);
return {
durationInSeconds: count(info?.duration),
language: text(info?.language),
segments: readSegments(body),
text: text(body.text) ?? ""
};
}
/**
* Reads segments out of a whisper answer. `segments` is what the large models
* send; the smaller ones send only `words`, whose timings are the best segment
* boundaries available.
*/
function readSegments(body) {
const segments = array(body.segments);
if (segments !== void 0) return segments.flatMap((entry) => {
const segment = record(entry);
const value = text(segment?.text);
if (segment === void 0 || value === void 0) return [];
return [{
endSecond: count(segment.end) ?? 0,
startSecond: count(segment.start) ?? 0,
text: value
}];
});
return (array(body.words) ?? []).flatMap((entry) => {
const word = record(entry);
const value = text(word?.word);
if (word === void 0 || value === void 0) return [];
return [{
endSecond: count(word.end) ?? 0,
startSecond: count(word.start) ?? 0,
text: value
}];
});
}
/**
* A `TranscriptionModelV4` over the Workers AI speech-recognition catalog.
*/
var CloudflareTranscriptionModel = class extends CloudflareModalityModel {
constructor(modelId, options, config) {
super(modelId, options, config);
}
async doGenerate(options) {
const answer = await this.send({
abortSignal: options.abortSignal,
build: (modelId) => {
if (WEBSOCKET_ONLY.has(modelId)) throw new CloudflareAIError({
code: "bad-request",
isRetryable: false,
message: `${modelId} takes websocket connections only. Use @cf/deepgram/nova-3 or @cf/openai/whisper-large-v3-turbo.`,
model: modelId,
requestBodyValues: void 0,
url: this.endpoint
});
return buildTranscriptionRequest(modelId, options);
},
headers: options.headers,
providerOptions: options.providerOptions
});
const body = record(answer.json) ?? {};
const transcript = record(body.results) === void 0 ? readWhisper(body) : readDeepgram(body);
return {
durationInSeconds: transcript.durationInSeconds,
language: transcript.language,
providerMetadata: answer.providerMetadata,
request: { body: JSON.stringify(answer.input) },
response: {
body: answer.json,
headers: answer.headers,
modelId: answer.modelId,
timestamp: answer.timestamp
},
segments: transcript.segments,
text: transcript.text,
warnings: answer.warnings
};
}
};
//#endregion
//#region src/models/ai-sdk/index.ts
/** Whether a value is a model object rather than a model id. */
function isModel(value) {
return typeof value === "object" && value !== null;
}
/**
* Creates a provider over Workers AI and the AI Gateway catalog.
*
* A Workers AI id goes to Workers AI and a catalog id or dynamic route to AI
* Gateway's `/run`, both on `env.AI.run`; a vendor's model object goes to AI
* Gateway's provider-native path. With `{ accountId, apiToken }` instead of
* `binding`, the same requests go to the REST API and to
* `gateway.ai.cloudflare.com`. Gateway
* options are flat (`id`, `cacheTtl`, …) or nested under `gateway`; per-model
* options and per-call `providerOptions.cloudflare` override them in that
* order.
*
* ```ts
* const ai = createAI({ binding: env.AI, id: "prod", cacheTtl: 60 });
*
* ai("@cf/zai-org/glm-4.7-flash");
* ai("anthropic/claude-opus-4.8", { fallback: ["openai/gpt-5.5"] });
* ai.embedding("@cf/baai/bge-base-en-v1.5");
* ```
*/
function createAI(settings) {
const config = {
providerGateway: settings,
transport: createTransport(settings)
};
/**
* Fallback legs are normalized once, here. A leg written as an id is gated
* at the line that wrote it; a model object is routed like `ai(model)`.
*
* A leg inherits the chain's *gateway* options and nothing else. The rest of
* the bag — `reasoningEffort`, `chatTemplateKwargs`, `headers`,
* `sessionAffinity`, `vendorKey` — was written for the model it was written
* on, and a leg that was configured with its own keeps every one of them.
*/
const normalize = (options) => {
if (options?.fallback === void 0) return options;
const inherited = gatewayLayerOf(options);
return {
...options,
fallback: options.fallback.map((leg) => typeof leg === "string" ? requireLanguageModelId(leg) : languageModel(leg, inherited))
};
};
function languageModel(model, options) {
const resolved = normalize(options);
if (!isModel(model)) return new CloudflareLanguageModel(requireLanguageModelId(model), resolved, config);
if (model instanceof CloudflareLanguageModel) {
const merged = normalize(mergeModelOptions(optionsOf(model), resolved));
return new CloudflareLanguageModel(model.modelId, merged, config);
}
if (model instanceof GatewayLanguageModel) {
const merged = normalize(mergeModelOptions(optionsOf(model), resolved));
return new GatewayLanguageModel(model.model, merged, config);
}
return new GatewayLanguageModel(model, resolved, config);
}
const embedding = (modelId, options) => new CloudflareEmbeddingModel(modelId, options, config);
const image = (modelId, options) => new CloudflareImageModel(modelId, options, config);
const transcription = (modelId, options) => new CloudflareTranscriptionModel(modelId, options, config);
const speech = (modelId, options) => new CloudflareSpeechModel(modelId, options, config);
const reranking = (modelId, options) => new CloudflareRerankingModel(modelId, options, config);
const provider = (model, options) => languageModel(model, options);
return Object.assign(provider, {
embedding,
embeddingModel: embedding,
image,
imageModel: image,
languageModel,
reranking,
rerankingModel: reranking,
routed: (model, options) => routed(model, options, config),
specificationVersion: "v4",
speech,
speechModel: speech,
textEmbeddingModel: embedding,
transcription,
transcriptionModel: transcription
});
}
//#endregion
export { GatewayLanguageModel as n, CloudflareAIError as r, createAI as t };
//# sourceMappingURL=ai-sdk-Kon40i0q.js.map