UNPKG

agents

Version:

A home for your AI agents

1,891 lines • 68.6 kB
//#region src/models/core/catalog.ts
/**
* Whether an id names an AI Gateway dynamic route: `dynamic/` and a route
* name after it.
*
* @experimental This surface is experimental and may change.
*/
function isDynamicRouteId(modelId) {
	return modelId.startsWith("dynamic/") && modelId.length > 8;
}
/**
* Whether an id names a Workers AI model, served by Workers AI itself rather
* than by AI Gateway's `/run`.
*
* @experimental This surface is experimental and may change.
*/
function isWorkersAIId(modelId) {
	return modelId.startsWith("@cf/") || modelId.startsWith("@hf/");
}
/**
* Whether an id names an AI Gateway catalog model: an author and a model,
* neither empty, and none of the other id namespaces (`@cf/`, `@hf/`,
* `dynamic/`).
*
* @experimental This surface is experimental and may change.
*/
function isCatalogModelId(modelId) {
	if (modelId.startsWith("@") || modelId.startsWith("dynamic/")) return false;
	const slash = modelId.indexOf("/");
	return slash > 0 && slash < modelId.length - 1;
}
/**
* Whether a string names a language model that runs on `env.AI.run`: a
* Workers AI id or an AI Gateway dynamic route.
*
* @experimental This surface is experimental and may change.
*/
function isRunPathLanguageModelId(modelId) {
	return modelId.startsWith("@cf/") || isDynamicRouteId(modelId);
}
//#endregion
//#region src/models/core/errors.ts
/**
* 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. Framework
* modules wrap it into their own error type at their boundary (the AI SDK
* module into an `APICallError`, so `ai`'s retry loop honours
* {@link CloudflareAIError.isRetryable}).
*/
var CloudflareAIError = class extends Error {
	constructor(options) {
		super(options.message);
		this.name = "CloudflareAIError";
		this.status = options.status;
		this.code = options.code;
		this.logId = options.logId;
		this.model = options.model;
		this.url = options.url;
		this.requestBodyValues = options.requestBodyValues;
		this.responseHeaders = options.responseHeaders;
		this.responseBody = options.responseBody;
		this.isRetryable = options.isRetryable ?? isRetryableStatus(options.status);
		this.data = options.data;
		this.cause = options.cause;
		this.attempts = options.attempts;
	}
};
/**
* Whether a status is worth retrying. Missing status means a transport-level
* failure (connection reset, DNS, abort-adjacent), which is retryable.
*
* @experimental This surface is experimental and may change.
*/
function isRetryableStatus(status) {
	if (status === void 0) return true;
	return status === 408 || status === 409 || status === 429 || status >= 500;
}
/**
* Classifies an HTTP status into a {@link CloudflareAIErrorCode}. The
* `AiGatewayError` envelope name catches gateway-level statuses that are not
* otherwise meaningful (notably 402 "insufficient balance").
*/
function classifyStatus(status, envelopeName) {
	if (status === 401 || status === 403) return "auth";
	if (status === 429) return "rate-limit";
	if (status === 404) return "not-found";
	if (status === 400 || status === 422) return "bad-request";
	if (status !== void 0 && status >= 500) return "provider-error";
	if (envelopeName === "AiGatewayError") return "gateway-error";
	return "unknown";
}
/**
* Workers AI binding error codes that map onto an HTTP status.
*
* @see https://developers.cloudflare.com/workers-ai/platform/errors/
*/
const BINDING_CODE_STATUS = {
	3003: 400,
	3006: 413,
	3007: 408,
	3008: 408,
	3023: 403,
	3036: 429,
	3039: 400,
	3040: 429,
	3041: 403,
	3042: 404,
	5004: 400,
	5005: 405,
	5007: 400,
	5016: 403,
	5018: 403,
	5019: 405
};
/** Abort-shaped errors must propagate untouched so callers can detect them. */
function isAbortError(error) {
	const name = error instanceof Error || error instanceof DOMException ? error.name : void 0;
	return name === "AbortError" || name === "ResponseAborted" || name === "TimeoutError";
}
function messageOf(error) {
	if (typeof error === "string") return error;
	if (error instanceof Error) return error.message;
	if (error !== null && typeof error === "object" && "message" in error) {
		const message = error.message;
		if (typeof message === "string") return message;
	}
	return String(error);
}
/**
* Extracts a Workers AI internal error code from a thrown binding error. The
* binding reports codes inside the message text (`"3040: Capacity temporarily
* exceeded"`) rather than as a status, so only known codes are accepted — an
* unrelated leading number must not be misread as a code.
*/
function bindingErrorCode(error) {
	if (error !== null && typeof error === "object" && "code" in error) {
		const code = error.code;
		if (typeof code === "number" && code in BINDING_CODE_STATUS) return code;
	}
	const matches = messageOf(error).matchAll(/\b(\d{3,5})\s*:/g);
	for (const match of matches) {
		const code = Number(match[1]);
		if (code in BINDING_CODE_STATUS) return code;
	}
}
/**
* Normalizes a thrown Workers AI binding error into a {@link CloudflareAIError}.
* Abort errors and errors that are already {@link CloudflareAIError}s pass through.
*/
function normalizeThrownError(error, context) {
	if (isAbortError(error)) return error;
	if (error instanceof CloudflareAIError) return error;
	const code = bindingErrorCode(error);
	const status = code === void 0 ? void 0 : BINDING_CODE_STATUS[code];
	return new CloudflareAIError({
		cause: error,
		code: classifyStatus(status),
		data: code === void 0 ? void 0 : { code },
		isRetryable: status === void 0 ? false : isRetryableStatus(status),
		message: messageOf(error),
		model: context.model,
		requestBodyValues: context.requestBodyValues,
		status,
		url: context.url
	});
}
//#endregion
//#region src/models/core/resume.ts
/**
* Resuming a streamed `/run` answer that dropped mid-way.
*
* AI Gateway's Run Durable Object keeps every SSE event of a streamed run and
* names the run in `cf-aig-run-id`. The binding can replay the stream from any
* event index:
*
* ```ts
* env.AI.fetch(`https://workers-binding.ai/ai-gateway/gateways/${id}/run/${runId}/resume?from=${n}`)
* ```
*
* The run keeps going on the gateway after the client is gone, so a replay
* from the right index continues the same answer rather than starting a new
* one. This wrapper makes that transparent: it reads the original body, and
* when a read fails it reconnects to the resume route and keeps feeding the
* same consumer, which never sees the break.
*
* The one subtlety is alignment. `from` counts complete SSE events, so the
* wrapper only ever passes complete events on and holds back a trailing
* partial one. After a drop the partial is discarded and the replay starts at
* the count of events already delivered: no byte is duplicated or lost.
*
* @experimental This surface is experimental and may change.
*/
const EMPTY = /* @__PURE__ */ new Uint8Array(0);
function concat(a, b) {
	const out = new Uint8Array(a.length + b.length);
	out.set(a, 0);
	out.set(b, a.length);
	return out;
}
/** Index just past the last `\n\n` in `buffer`, or -1 when none is complete. */
function lastEventBoundary(buffer) {
	for (let i = buffer.length - 2; i >= 0; i--) if (buffer[i] === 10 && buffer[i + 1] === 10) return i + 2;
	return -1;
}
/** Complete SSE events in `buffer`: `\n\n` terminators, counted once each. */
function countEvents(buffer) {
	let count = 0;
	for (let i = 0; i + 1 < buffer.length; i++) if (buffer[i] === 10 && buffer[i + 1] === 10) {
		count++;
		i++;
	}
	return count;
}
/**
* Wraps a streamed `/run` body so a dropped connection resumes from the last
* complete event. Errors once the replay buffer has expired (404), when a
* replay fails, or after `maxReconnects` drops.
*
* @experimental This surface is experimental and may change.
*/
function resumableBody(options) {
	const maxReconnects = options.maxReconnects ?? 5;
	let delivered = 0;
	let pending = EMPTY;
	let reconnects = 0;
	let cancelled = false;
	let reader;
	const stopped = () => cancelled || options.signal?.aborted === true;
	const failure = (message, extra = {}) => new CloudflareAIError({
		code: "provider-error",
		isRetryable: false,
		message,
		model: options.model,
		requestBodyValues: void 0,
		url: options.url,
		...extra
	});
	/** The replay from the events already delivered, or undefined after an error. */
	async function replay(controller) {
		let response;
		try {
			response = await options.resume(delivered, options.signal);
		} catch (error) {
			controller.error(failure(`The stream dropped at event ${delivered} and the resume request failed.`, { cause: error }));
			return;
		}
		if (response.status === 404) {
			controller.error(failure(`The stream dropped at event ${delivered}, and AI Gateway no longer holds the run to resume it.`, { status: 404 }));
			return;
		}
		if (!response.ok || response.body === null) {
			controller.error(failure(`The stream dropped at event ${delivered} and resuming it failed (${response.status}).`, { status: response.status }));
			return;
		}
		return response.body;
	}
	return new ReadableStream({
		async start(controller) {
			let current = options.initial;
			for (;;) {
				reader = current.getReader();
				try {
					for (;;) {
						const { done, value } = await reader.read();
						if (done) {
							if (pending.length > 0) controller.enqueue(pending);
							pending = EMPTY;
							controller.close();
							return;
						}
						if (value === void 0 || value.length === 0) continue;
						pending = concat(pending, value);
						const boundary = lastEventBoundary(pending);
						if (boundary > 0) {
							const complete = pending.slice(0, boundary);
							controller.enqueue(complete);
							delivered += countEvents(complete);
							pending = pending.slice(boundary);
						}
					}
				} catch (error) {
					try {
						reader.releaseLock();
					} catch {}
					if (stopped()) {
						if (!cancelled) controller.error(error);
						return;
					}
					if (reconnects >= maxReconnects) {
						controller.error(failure(`The stream dropped ${reconnects + 1} times; giving up at event ${delivered}.`, { cause: error }));
						return;
					}
					pending = EMPTY;
					reconnects++;
					const body = await replay(controller);
					if (body === void 0) return;
					current = body;
				}
			}
		},
		cancel(reason) {
			cancelled = true;
			reader?.cancel(reason).catch(() => {});
		}
	});
}
//#endregion
//#region src/models/core/transport.ts
/** The reported `url` for a provider-native call made through the binding. */
const NATIVE_BINDING_URL = "ai-gateway-binding-native";
/** The origin the binding's own `fetch` answers on. */
const BINDING_ORIGIN = "https://workers-binding.ai";
/** The REST API's run path, for a model id reached with an account and token. */
const REST_API_ORIGIN = "https://api.cloudflare.com/client/v4/accounts";
/** The provider-native path, for a model object reached with a token. */
const GATEWAY_ORIGIN = "https://gateway.ai.cloudflare.com/v1";
/**
* Drops entries whose value is `undefined`. The AI SDK types call headers as
* `Record<string, string | undefined>`, and an undefined value reaches the
* binding as the string "undefined" rather than as an absent header.
*/
function definedHeaders(headers) {
	const result = {};
	for (const [key, value] of Object.entries(headers ?? {})) if (value !== void 0) result[key] = value;
	return result;
}
/** Copies response headers into a plain object. */
function headersToObject(headers) {
	const result = {};
	headers.forEach((value, key) => {
		result[key] = value;
	});
	return result;
}
/**
* Some run-path responses arrive bare and some wrapped in the Cloudflare API
* envelope. Unwrap only when the envelope keys are actually present, so a
* provider payload that happens to have a `result` field is left alone.
*/
function unwrapEnvelope(value, context) {
	if (value === null || typeof value !== "object" || Array.isArray(value)) return value;
	const record = value;
	if (record.success === false && firstEnvelopeError(record) !== void 0 || "success" in record && "result" in record) {
		if (record.success === true) return record.result;
		throw new CloudflareAIError({
			code: classifyStatus(context?.status, typeof record.name === "string" ? record.name : void 0),
			data: value,
			message: errorMessageFrom(value, context?.status),
			model: context?.model ?? "",
			requestBodyValues: void 0,
			status: context?.status,
			url: context?.url ?? ""
		});
	}
	return value;
}
function firstEnvelopeError(record) {
	for (const key of ["error", "errors"]) {
		const value = record[key];
		if (Array.isArray(value) && value.length > 0) {
			const first = value[0];
			if (first !== null && typeof first === "object") return first;
		}
		if (value !== null && typeof value === "object" && !Array.isArray(value)) return value;
	}
}
/**
* Pulls a human message out of any of the error envelopes the platform emits:
* the gateway envelope (`error: [{ code, message }]`), the Workers AI
* validation envelope (`message`/`description` only), a bare
* `{ error: "..." }`, and the "Model ID missing" internal error, which carries
* only `description`.
*/
function errorMessageFrom(body, status) {
	const record = body !== null && typeof body === "object" && !Array.isArray(body) ? body : void 0;
	if (record !== void 0) {
		const envelope = firstEnvelopeError(record);
		if (typeof envelope?.message === "string") return envelope.message;
		if (typeof record.error === "string") return record.error;
		if (typeof record.message === "string") return record.message;
		if (typeof record.description === "string") return record.description;
	}
	if (typeof body === "string" && body !== "") return body;
	return `HTTP ${status ?? "error"}`;
}
/** Builds a {@link CloudflareAIError} from a non-2xx run-path response. */
async function errorFromResponse(response, context) {
	const text = await response.text().catch(() => "");
	let body = text;
	try {
		body = text === "" ? void 0 : JSON.parse(text);
	} catch {}
	const record = body !== null && typeof body === "object" && !Array.isArray(body) ? body : void 0;
	const envelopeName = typeof record?.name === "string" ? record.name : void 0;
	const headers = headersToObject(response.headers);
	return new CloudflareAIError({
		code: classifyStatus(response.status, envelopeName),
		data: body,
		logId: headers["cf-aig-log-id"],
		message: errorMessageFrom(body, response.status),
		model: context.model,
		requestBodyValues: context.requestBodyValues,
		responseBody: text,
		responseHeaders: headers,
		status: response.status,
		url: context.url
	});
}
/**
* The gateway options as the `cf-aig-*` request headers AI Gateway reads.
* The provider-native path has no options object, so every option travels
* this way; the names are the gateway's own.
*/
function gatewayHeaders(gateway) {
	const headers = {};
	if (gateway.cacheTtl !== void 0) headers["cf-aig-cache-ttl"] = String(gateway.cacheTtl);
	if (gateway.skipCache !== void 0) headers["cf-aig-skip-cache"] = String(gateway.skipCache);
	if (gateway.cacheKey !== void 0) headers["cf-aig-cache-key"] = gateway.cacheKey;
	if (gateway.metadata !== void 0) headers["cf-aig-metadata"] = JSON.stringify(gateway.metadata);
	if (gateway.collectLog !== void 0) headers["cf-aig-collect-log"] = String(gateway.collectLog);
	if (gateway.eventId !== void 0) headers["cf-aig-event-id"] = gateway.eventId;
	if (gateway.requestTimeoutMs !== void 0) headers["cf-aig-request-timeout"] = String(gateway.requestTimeoutMs);
	const retries = gateway.retries;
	if (retries?.maxAttempts !== void 0) headers["cf-aig-max-attempts"] = String(retries.maxAttempts);
	if (retries?.retryDelayMs !== void 0) headers["cf-aig-retry-delay"] = String(retries.retryDelayMs);
	if (retries?.backoff !== void 0) headers["cf-aig-backoff"] = retries.backoff;
	if (gateway.byokAlias !== void 0) headers["cf-aig-byok-alias"] = gateway.byokAlias;
	if (gateway.zdr !== void 0) headers["cf-aig-zdr"] = String(gateway.zdr);
	return headers;
}
/**
* The run path's gateway options, and the headers for the ones the binding's
* `GatewayOptions` type does not define. Both transports use it: the binding
* takes `gateway` as an options object, HTTP as `cf-aig-*` headers.
*
* `byokAlias` reaches `/run` as `cf-aig-byok-alias`, which `/run` reads for a
* catalog model's stored key. `zdr` is left off: `/run` does not read it, so it
* applies to provider-native requests only.
*/
function runOptions(request) {
	const { byokAlias, zdr: _zdr, ...gateway } = request.gateway;
	return {
		gateway,
		headers: {
			...request.headers,
			...byokAlias !== void 0 && isCatalogModelId(request.model) ? { "cf-aig-byok-alias": byokAlias } : {}
		}
	};
}
/**
* The binding's resume route for the run a response names. Only called for a
* response {@link withResume} has checked carries `cf-aig-run-id`.
*/
function resumeUrl(gatewayId, response, from) {
	const runId = response.headers.get("cf-aig-run-id") ?? "";
	return `${BINDING_ORIGIN}/ai-gateway/gateways/${encodeURIComponent(gatewayId)}/run/${encodeURIComponent(runId)}/resume?from=${from}`;
}
/**
* A streamed `/run` answer that names its run (`cf-aig-run-id`) resumes from
* the last complete event if the connection drops; see `resume.ts`. Anything
* else — a JSON answer, an error, a Workers AI stream with no run id — is
* returned as it is.
*/
function withResume(response, request, resume) {
	const contentType = response.headers.get("content-type") ?? "";
	if (!response.ok || response.body === null || !contentType.includes("text/event-stream") || response.headers.get("cf-aig-run-id") === null) return response;
	return new Response(resumableBody({
		initial: response.body,
		model: request.model,
		resume,
		signal: request.signal,
		url: "workers-ai-binding"
	}), {
		headers: response.headers,
		status: response.status,
		statusText: response.statusText
	});
}
/**
* What `/run` calls each request format in the binding's `openaiEndpoint`
* option, which `aig-entry` turns into `/run`'s `requestFormat`.
*/
const OPENAI_ENDPOINTS = {
	"chat-completions": "/v1/chat/completions",
	messages: "/v1/messages",
	responses: "/v1/responses"
};
/**
* Whether a parsed body is the gateway's own error envelope rather than a
* vendor payload — `{ success: false, error: [{ code, message }] }`, named
* `AiGatewayError`. The gateway answers this for its own failures (no such
* gateway, unified billing unpaid, provider not configured); everything else
* on this path is the vendor's own error, which the vendor's provider reads.
*
* @experimental This surface is experimental and may change.
*/
function isGatewayErrorEnvelope(json) {
	if (json === null || typeof json !== "object" || Array.isArray(json)) return false;
	const record = json;
	if (record.name === "AiGatewayError") return true;
	if (record.success !== false) return false;
	const errors = record.error ?? record.errors;
	return Array.isArray(errors) && errors.some((entry) => entry !== null && typeof entry === "object" && typeof entry.message === "string");
}
/**
* Builds a {@link CloudflareAIError} from the gateway's own error envelope, so
* a gateway failure on the provider-native path classifies and retries exactly as
* one on the Workers AI run path.
*
* @experimental This surface is experimental and may change.
*/
function errorFromGatewayEnvelope(body, context) {
	const record = body !== null && typeof body === "object" && !Array.isArray(body) ? body : void 0;
	return new CloudflareAIError({
		code: classifyStatus(context.status, typeof record?.name === "string" ? record.name : "AiGatewayError"),
		data: body,
		logId: context.responseHeaders?.["cf-aig-log-id"],
		message: errorMessageFrom(body, context.status),
		model: context.model,
		requestBodyValues: context.requestBodyValues,
		responseBody: context.responseBody,
		responseHeaders: context.responseHeaders,
		status: context.status,
		url: context.url
	});
}
/** The keyless in-Worker backend: `env.AI.run(...)`. */
function bindingTransport(binding) {
	const raw = binding;
	const logIds = /* @__PURE__ */ new WeakMap();
	return {
		logIdFallback(response) {
			return logIds.get(response);
		},
		async run(request) {
			try {
				if (request.audio !== void 0) return await raw.run(request.model, {
					...request.input,
					audio: {
						body: new Response(new Uint8Array(request.audio.bytes)).body,
						contentType: request.audio.contentType
					}
				}, {
					...Object.keys(request.headers).length > 0 ? { extraHeaders: request.headers } : {},
					returnRawResponse: true,
					signal: request.signal
				});
				const run = runOptions(request);
				const response = await raw.run(request.model, request.input, {
					...Object.keys(run.headers).length > 0 ? { extraHeaders: run.headers } : {},
					gateway: run.gateway,
					...isWorkersAIId(request.model) ? {} : { openaiEndpoint: OPENAI_ENDPOINTS[request.format ?? "chat-completions"] },
					returnRawResponse: true,
					signal: request.signal
				});
				const resumable = withResume(response, request, (from, signal) => binding.fetch(resumeUrl(run.gateway.id, response, from), {
					method: "GET",
					signal
				}));
				const logId = binding.aiGatewayLogId;
				if (logId !== null && logId !== void 0) logIds.set(resumable, logId);
				return resumable;
			} catch (error) {
				throw normalizeThrownError(error, {
					model: request.model,
					requestBodyValues: request.input,
					url: "workers-ai-binding"
				});
			}
		},
		async native(request) {
			try {
				return await binding.fetch(`${BINDING_ORIGIN}/ai-gateway/gateways/${encodeURIComponent(request.gateway.id)}/${request.provider}/${request.endpoint}`, {
					body: request.body ?? null,
					headers: {
						...request.headers,
						...gatewayHeaders(request.gateway)
					},
					method: request.method,
					signal: request.signal
				});
			} catch (error) {
				throw normalizeThrownError(error, {
					model: `${request.provider}/${request.endpoint}`,
					requestBodyValues: void 0,
					url: NATIVE_BINDING_URL
				});
			}
		},
		url: "workers-ai-binding"
	};
}
/**
* Request headers with the credential set last. `Headers` matches names
* case-insensitively, so a caller's `Authorization` is replaced rather than
* sent alongside the token.
*/
function withCredential(headers, name, token) {
	const result = new Headers(headers);
	result.set(name, `Bearer ${token}`);
	return result;
}
/**
* A Workers AI answer as the binding returns it. The REST API wraps a JSON
* answer in the `{ success, result }` envelope where the binding returns the
* bare result, so a successful envelope is unwrapped; anything else is left
* for the caller, as it is on the binding.
*/
async function bareWorkersAIAnswer(response) {
	const contentType = response.headers.get("content-type") ?? "";
	if (!response.ok || !contentType.includes("application/json")) return response;
	const text = await response.text();
	let body;
	try {
		body = JSON.parse(text);
	} catch {
		body = void 0;
	}
	const record = body !== null && typeof body === "object" && !Array.isArray(body) ? body : void 0;
	const unwrapped = record?.success === true && "result" in record ? record.result : void 0;
	const headers = new Headers(response.headers);
	headers.delete("content-length");
	return new Response(unwrapped === void 0 ? text : JSON.stringify(unwrapped), {
		headers,
		status: response.status,
		statusText: response.statusText
	});
}
/**
* The HTTP backend, for an account id and API token. A model id goes to the
* REST API, which serves it with the same `/run` call the binding makes: a
* Workers AI id on `/ai/run/{model}` with its own input, any other id on the
* endpoint for its request format with the model in the body. A model object
* goes to the provider-native path on `gateway.ai.cloudflare.com`, exactly as
* the binding's `fetch` forwards it.
*
* A dropped stream is not resumed: the binding has a resume route, but the
* public one is not open to every gateway yet.
*/
function httpTransport({ accountId, apiToken }) {
	const account = encodeURIComponent(accountId);
	const runBase = `${REST_API_ORIGIN}/${account}/ai`;
	/**
	* A raw audio body goes to Workers AI's own run route, with the model's
	* other fields as query parameters. TODO: send it through the gateway once
	* AI Gateway accepts the body; today the REST API answers `500` when
	* `cf-aig-gateway-id` is set.
	*/
	async function runRawAudio(request, audio) {
		const query = new URLSearchParams();
		for (const [key, value] of Object.entries(request.input)) if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") query.set(key, String(value));
		const search = query.size > 0 ? `?${query}` : "";
		const url = `${runBase}/run/${request.model}${search}`;
		try {
			return await bareWorkersAIAnswer(await fetch(url, {
				body: new Uint8Array(audio.bytes),
				headers: withCredential({
					...request.headers,
					"content-type": audio.contentType
				}, "authorization", apiToken),
				method: "POST",
				signal: request.signal
			}));
		} catch (error) {
			throw normalizeThrownError(error, {
				model: request.model,
				requestBodyValues: request.input,
				url
			});
		}
	}
	return {
		logIdFallback() {},
		async run(request) {
			if (request.audio !== void 0) return runRawAudio(request, request.audio);
			const workersAI = isWorkersAIId(request.model);
			const run = runOptions(request);
			const url = workersAI ? `${runBase}/run/${request.model}` : `${runBase}${OPENAI_ENDPOINTS[request.format ?? "chat-completions"]}`;
			try {
				const response = await fetch(url, {
					body: JSON.stringify(workersAI ? request.input : {
						...request.input,
						model: request.model
					}),
					headers: withCredential({
						"content-type": "application/json",
						...gatewayHeaders(run.gateway),
						"cf-aig-gateway-id": run.gateway.id,
						...run.headers
					}, "authorization", apiToken),
					method: "POST",
					signal: request.signal
				});
				return workersAI ? await bareWorkersAIAnswer(response) : response;
			} catch (error) {
				throw normalizeThrownError(error, {
					model: request.model,
					requestBodyValues: request.input,
					url
				});
			}
		},
		async native(request) {
			const url = `${GATEWAY_ORIGIN}/${account}/${encodeURIComponent(request.gateway.id)}/${request.provider}/${request.endpoint}`;
			try {
				return await fetch(url, {
					body: request.body ?? null,
					...request.body instanceof ReadableStream ? { duplex: "half" } : {},
					headers: withCredential({
						...request.headers,
						...gatewayHeaders(request.gateway)
					}, "cf-aig-authorization", apiToken),
					method: request.method,
					signal: request.signal
				});
			} catch (error) {
				throw normalizeThrownError(error, {
					model: `${request.provider}/${request.endpoint}`,
					requestBodyValues: void 0,
					url
				});
			}
		},
		url: runBase
	};
}
/**
* Builds the transport for a set of provider settings. The model id, not the
* caller, decides the wire format; the transport only decides how the request
* physically leaves the Worker.
*
* @experimental This surface is experimental and may change.
*/
function createTransport(settings) {
	if (settings.binding !== void 0) return bindingTransport(settings.binding);
	if (settings.accountId && settings.apiToken) return httpTransport(settings);
	throw new Error("createAI requires { binding } (the AI binding) or { accountId, apiToken }.");
}
//#endregion
//#region src/models/core/gateway-providers.ts
/**
* The AI Gateway provider table: which gateway provider slug a vendor's own
* base URL belongs to, and what the provider-native `endpoint` is for a
* given upstream URL.
*
* This is platform knowledge, not vendor knowledge — the gateway publishes the
* list, and every entry here appears in `AIGatewayProviders` from
* `@cloudflare/workers-types`. Nothing about a vendor's wire format, model ids
* or capabilities lives here: the vendor's own provider package builds the
* request, and this table only says where it goes.
*
* Adapted from `@cloudflare/ai-gateway-provider`'s `providers.ts`.
*
* @experimental This surface is experimental and may change.
*/
/**
* The gateway provider slugs this table maps onto. A subset of the ambient
* `AIGatewayProviders` union; {@link GatewayProviderNameCheck} keeps it one.
*/
const GATEWAY_PROVIDER_NAMES = [
	"workers-ai",
	"anthropic",
	"openai",
	"azure-openai",
	"google-ai-studio",
	"google-vertex-ai",
	"grok",
	"mistral",
	"perplexity-ai",
	"replicate",
	"groq",
	"deepseek",
	"openrouter",
	"cerebras",
	"cohere",
	"huggingface",
	"aws-bedrock",
	"elevenlabs",
	"cartesia"
];
/** The default credential header; providers add their own on top. */
const AUTHORIZATION = ["authorization"];
/** Path after the host, leading slash removed, query string kept. */
function pathOf(url) {
	return `${url.pathname.replace(/^\/+/, "")}${url.search}`;
}
function buildProvider(spec) {
	const map = spec.endpoint ?? ((url) => {
		const path = pathOf(url);
		return spec.pathPrefix === void 0 ? path : path.replace(spec.pathPrefix, "");
	});
	return {
		authHeaders: spec.authHeaders ?? AUTHORIZATION,
		endpoint(url) {
			const parsed = parseUrl(url);
			if (parsed === void 0 || !spec.host.test(parsed.host)) return;
			return map(parsed);
		},
		host: spec.host,
		name: spec.name
	};
}
function parseUrl(url) {
	try {
		return new URL(url);
	} catch {
		return;
	}
}
/**
* Azure names the deployment in the path; the gateway names it in the
* endpoint as `<resource>/<deployment>/<rest>`.
*/
function azureEndpoint(url) {
	const resource = /^([^.]+)\.openai\.azure\.com$/.exec(url.host)?.[1];
	const rest = /^\/openai\/deployments\/([^/]+)\/(.+)$/.exec(url.pathname);
	if (resource === void 0 || rest === null) return void 0;
	return `${resource}/${rest[1]}/${rest[2]}${url.search}`;
}
/**
* Bedrock's region and service live in the host; the gateway wants them as the
* first two endpoint segments.
*/
function bedrockEndpoint(url) {
	const match = /^(bedrock[a-z0-9-]*)\.([a-z0-9-]+)\.amazonaws\.com$/.exec(url.host);
	if (match === null) return void 0;
	return `${match[1]}/${match[2]}/${pathOf(url)}`;
}
/**
* The provider table, in declaration order. Hosts do not overlap, so the order
* only decides which row a future overlapping host would win.
*
* @experimental This surface is experimental and may change.
*/
const GATEWAY_PROVIDERS = [
	{
		host: /^api\.cloudflare\.com$/,
		name: "workers-ai",
		pathPrefix: /^client\/v4\/accounts\/[^/]+\/ai\//
	},
	{
		authHeaders: ["authorization", "x-api-key"],
		host: /^api\.anthropic\.com$/,
		name: "anthropic"
	},
	{
		host: /^api\.openai\.com$/,
		name: "openai"
	},
	{
		authHeaders: ["authorization", "api-key"],
		endpoint: azureEndpoint,
		host: /^[^.]+\.openai\.azure\.com$/,
		name: "azure-openai"
	},
	{
		authHeaders: ["authorization", "x-goog-api-key"],
		host: /^generativelanguage\.googleapis\.com$/,
		name: "google-ai-studio"
	},
	{
		host: /^(?:[a-z0-9-]+-)?aiplatform\.googleapis\.com$/,
		name: "google-vertex-ai"
	},
	{
		host: /^api\.x\.ai$/,
		name: "grok"
	},
	{
		host: /^api\.mistral\.ai$/,
		name: "mistral"
	},
	{
		host: /^api\.perplexity\.ai$/,
		name: "perplexity-ai"
	},
	{
		host: /^api\.replicate\.com$/,
		name: "replicate"
	},
	{
		host: /^api\.groq\.com$/,
		name: "groq",
		pathPrefix: /^openai\/v1\//
	},
	{
		host: /^api\.deepseek\.com$/,
		name: "deepseek"
	},
	{
		host: /^openrouter\.ai$/,
		name: "openrouter",
		pathPrefix: /^api\//
	},
	{
		host: /^api\.cerebras\.ai$/,
		name: "cerebras"
	},
	{
		host: /^api\.cohere\.(?:ai|com)$/,
		name: "cohere"
	},
	{
		host: /^(?:api-inference|router)\.huggingface\.co$/,
		name: "huggingface",
		pathPrefix: /^models\//
	},
	{
		endpoint: bedrockEndpoint,
		host: /^bedrock.*\.amazonaws\.com$/,
		name: "aws-bedrock"
	},
	{
		host: /^api\.elevenlabs\.io$/,
		name: "elevenlabs"
	},
	{
		host: /^api\.cartesia\.ai$/,
		name: "cartesia"
	}
].map(buildProvider);
/**
* The table row for a bare host (`api.anthropic.com`) or for anything a `URL`
* can be built from. pi-ai models carry a `baseUrl` rather than a full request
* URL, so both spellings resolve.
*
* @experimental This surface is experimental and may change.
*/
function gatewayProviderForHost(hostOrUrl) {
	const host = parseUrl(hostOrUrl)?.host ?? hostOf(hostOrUrl);
	if (host === void 0) return void 0;
	return GATEWAY_PROVIDERS.find((provider) => provider.host.test(host));
}
/** Reads a host out of a bare host, `host/path` or `//host` spelling. */
function hostOf(value) {
	const host = value.replace(/^\/\//, "").split("/")[0];
	return host === "" ? void 0 : host;
}
/**
* Maps a vendor request URL onto `{ provider, endpoint, authHeaders }` for the
* universal gateway request, or `undefined` when the host is not a gateway
* provider.
*
* @experimental This surface is experimental and may change.
*/
function resolveGatewayProvider(url) {
	const parsed = parseUrl(url);
	if (parsed === void 0) return void 0;
	for (const provider of GATEWAY_PROVIDERS) {
		if (!provider.host.test(parsed.host)) continue;
		const endpoint = provider.endpoint(url);
		if (endpoint === void 0) continue;
		return {
			authHeaders: provider.authHeaders,
			endpoint,
			provider: provider.name
		};
	}
}
/**
* {@link resolveGatewayProvider}, throwing a `TypeError` that names the host
* and the provider list when the URL belongs to no gateway provider.
*
* @experimental This surface is experimental and may change.
*/
function requireGatewayProvider(url) {
	const resolved = resolveGatewayProvider(url);
	if (resolved !== void 0) return resolved;
	const host = parseUrl(url)?.host ?? url;
	throw new TypeError(`AI Gateway has no provider for ${host}. Requests are routed by host; the supported providers are ${GATEWAY_PROVIDER_NAMES.join(", ")}.`);
}
/** The host a gateway-shaped base URL points at. */
const GATEWAY_HOST = "gateway.ai.cloudflare.com";
/**
* The provider slug of a gateway-shaped base URL —
* `https://gateway.ai.cloudflare.com/v1/{account}/{gateway}/{slug}`. Generated
* registries write models that way (pi-ai's Cloudflare AI Gateway catalog
* leaves the account and gateway as literal placeholders), so the slug is
* already in the path rather than implied by a vendor host. The account and
* gateway segments are ignored: the binding has already chosen both.
*/
function gatewayShapedSlug(url) {
	if (url.host !== GATEWAY_HOST) return void 0;
	const segments = url.pathname.split("/").filter((part) => part !== "");
	if (segments[0] !== "v1" || segments.length < 4) return void 0;
	return segments[3];
}
/**
* Whether a base URL is written in the gateway's own shape
* (`https://gateway.ai.cloudflare.com/v1/{account}/{gateway}/{slug}`) rather
* than as a vendor's. A generated Cloudflare registry writes models that way,
* and the path after the slug is then the vendor's own, so a caller that
* derives an endpoint from a base URL has to tell the two apart.
*
* @experimental This surface is experimental and may change.
*/
function isGatewayShapedUrl(url) {
	return parseUrl(url)?.host === GATEWAY_HOST;
}
function isGatewayProviderName(slug) {
	return GATEWAY_PROVIDER_NAMES.includes(slug);
}
/**
* The gateway provider slug a model's base URL belongs to. Two spellings
* resolve, because two kinds of registry write them:
*
* - a vendor's own base URL (`https://api.anthropic.com` → `anthropic`), which
*   is what a provider package configured for the vendor carries;
* - a gateway-shaped base URL
*   (`https://gateway.ai.cloudflare.com/v1/{account}/{gateway}/anthropic` →
*   `anthropic`), which is what a generated Cloudflare registry carries.
*
* A base URL neither shape resolves is a wiring mistake rather than a request
* failure, so it throws a `TypeError` naming what was not recognised.
*
* @experimental This surface is experimental and may change.
*/
function gatewaySlugForBaseUrl(baseUrl) {
	const url = parseUrl(baseUrl);
	const shaped = url === void 0 ? void 0 : gatewayShapedSlug(url);
	if (shaped !== void 0) {
		if (isGatewayProviderName(shaped)) return shaped;
		throw new TypeError(`AI Gateway has no provider "${shaped}" (from ${baseUrl}). The supported providers are ${GATEWAY_PROVIDER_NAMES.join(", ")}.`);
	}
	const provider = gatewayProviderForHost(baseUrl);
	if (provider !== void 0) return provider.name;
	throw new TypeError(`AI Gateway has no provider for ${url?.host ?? baseUrl}. Requests are routed by the model's base URL; the supported providers are ${GATEWAY_PROVIDER_NAMES.join(", ")}.`);
}
//#endregion
//#region src/models/core/chat-completions/quirks.ts
const STRICT_OPENAI = {
	chatTemplateKwargs: "unsupported",
	imageUrls: "any",
	jsonSchema: "supported",
	jsonSchemaStrict: false,
	maxTokensField: "max_completion_tokens",
	modelEchoSuffix: void 0,
	nullAssistantContent: "allowed",
	reasoningEffortLevels: "basic",
	reasoningOff: "unsupported",
	reasoningOffStreamLabel: "content",
	reasoningReplay: "drop",
	requiresAssistantAfterToolResult: false,
	toolCallIds: "any",
	toolChoiceRequired: "supported"
};
/** Workers AI's vLLM front end: what every `@cf/` model has in common. */
const WORKERS_AI = {
	...STRICT_OPENAI,
	chatTemplateKwargs: "supported",
	maxTokensField: "max_tokens",
	imageUrls: "data-only",
	nullAssistantContent: "empty-string",
	reasoningOff: "chat-template",
	reasoningReplay: "reasoning_content"
};
/**
* Longest-prefix overrides. Keep each one evidence-backed (the capture that
* proves it is named in the comment) and delete it when the platform fixes
* the deviation.
*/
const OVERRIDES = [
	["@cf/zai-org/glm-4.7-flash", { toolChoiceRequired: "named-fallback" }],
	["@cf/moonshotai/", { reasoningOff: "unsupported" }],
	["@cf/openai/", { reasoningOff: "unsupported" }],
	["@cf/mistralai/", {
		chatTemplateKwargs: "unsupported",
		jsonSchema: "unsupported",
		reasoningOff: "unsupported",
		requiresAssistantAfterToolResult: true,
		toolCallIds: "alnum-9"
	}],
	["@cf/google/", { modelEchoSuffix: "-external" }],
	["@cf/nvidia/nemotron-", { reasoningOffStreamLabel: "reasoning" }]
];
/** The quirks for a catalog id: base profile plus the longest matching override. */
function quirksFor(modelId) {
	const base = modelId.startsWith("@cf/") ? WORKERS_AI : STRICT_OPENAI;
	let best;
	for (const entry of OVERRIDES) if (modelId.startsWith(entry[0]) && (best === void 0 || entry[0].length > best[0].length)) best = entry;
	return best === void 0 ? base : {
		...base,
		...best[1]
	};
}
/** Whether an id is a Workers AI model rather than a third-party catalog model. */
function isWorkersAI(modelId) {
	return modelId.startsWith("@cf/");
}
//#endregion
//#region src/models/core/chat-completions/request.ts
/**
* Rewrites a strict OpenAI chat-completions request body into what a given
* catalog model accepts. Frameworks build the strict body from their own
* prompt types; everything Cloudflare- or vendor-specific happens here, once.
*
* The rewrite never throws: a feature the model cannot take is dropped and
* reported as a {@link CompatWarning}.
*/
function isRecord$3(value) {
	return value !== null && typeof value === "object" && !Array.isArray(value);
}
function clampEffort(effort) {
	if (effort === "minimal") return "low";
	if (effort === "xhigh") return "high";
	return effort;
}
/** Drops URL image parts (keeps inline data URLs) from a user message. */
function stripImageUrls(message, warnings) {
	if (!Array.isArray(message.content)) return message;
	let dropped = 0;
	const content = message.content.filter((part) => {
		if (!isRecord$3(part) || part.type !== "image_url") return true;
		const image = isRecord$3(part.image_url) ? part.image_url : void 0;
		if ((typeof image?.url === "string" ? image.url : "").startsWith("data:")) return true;
		dropped += 1;
		return false;
	});
	if (dropped > 0) warnings.push({
		feature: "image-url",
		message: `Workers AI models need image bytes; ${dropped} image URL${dropped === 1 ? " was" : "s were"} dropped.`
	});
	return {
		...message,
		content
	};
}
/** Nine `[A-Za-z0-9]` characters: what Mistral accepts as a tool-call id. */
const CONFORMANT_TOOL_CALL_ID = /^[0-9A-Za-z]{9}$/;
const TOOL_CALL_ID_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
/**
* A deterministic nine-character id for an original id: the same conversation
* replayed twice produces the same ids, so a provider cache or a log stays
* comparable. `salt` is bumped only to break a collision.
*/
function hashedToolCallId(original, salt) {
	let state = (2166136261 ^ salt) >>> 0;
	for (let index = 0; index < original.length; index += 1) state = Math.imul(state ^ original.charCodeAt(index), 16777619) >>> 0;
	let id = "";
	for (let index = 0; index < 9; index += 1) {
		state = Math.imul(state ^ state >>> 15, 16777619) >>> 0;
		id += TOOL_CALL_ID_ALPHABET[state % 62];
	}
	return id;
}
/** Every tool-call id a message set mentions, in the order it mentions them. */
function toolCallIdsIn(messages) {
	const ids = [];
	const seen = /* @__PURE__ */ new Set();
	const add = (value) => {
		if (typeof value !== "string" || seen.has(value)) return;
		seen.add(value);
		ids.push(value);
	};
	for (const message of messages) {
		if (!isRecord$3(message)) continue;
		if (Array.isArray(message.tool_calls)) {
			for (const call of message.tool_calls) if (isRecord$3(call)) add(call.id);
		}
		add(message.tool_call_id);
	}
	return ids;
}
/**
* Plans the rewrite: which ids have to change, and to what. Ids that already
* conform are kept exactly as they are — including as reserved values, so a
* rewritten id can never collide with one.
*/
function planToolCallIds(messages) {
	const plan = /* @__PURE__ */ new Map();
	const ids = toolCallIdsIn(messages);
	const taken = new Set(ids.filter((id) => CONFORMANT_TOOL_CALL_ID.test(id)));
	for (const id of ids) {
		if (taken.has(id)) continue;
		let replacement = hashedToolCallId(id, 0);
		for (let salt = 1; taken.has(replacement) && salt <= 64; salt += 1) replacement = hashedToolCallId(id, salt);
		taken.add(replacement);
		plan.set(id, replacement);
	}
	return plan;
}
/** Applies the plan to an assistant `tool_calls` turn or a `tool` result. */
function rewriteToolCallIds(message, plan) {
	let rewritten = message;
	if (Array.isArray(rewritten.tool_calls)) rewritten = {
		...rewritten,
		tool_calls: rewritten.tool_calls.map((call) => {
			if (!isRecord$3(call) || typeof call.id !== "string") return call;
			const replacement = plan.get(call.id);
			return replacement === void 0 ? call : {
				...call,
				id: replacement
			};
		})
	};
	if (typeof rewritten.tool_call_id === "string") {
		const replacement = plan.get(rewritten.tool_call_id);
		if (replacement !== void 0) rewritten = {
			...rewritten,
			tool_call_id: replacement
		};
	}
	return rewritten;
}
/**
* Applies the model's quirks to a strict OpenAI chat-completions body.
*
* Conventions the strict body follows, which this function translates:
* - `max_tokens` is the cap; it becomes `max_completion_tokens` where needed.
* - `reasoning_effort: null` means "turn reasoning off".
* - Replayed assistant reasoning sits in `reasoning_content`.
* - `response_format.json_schema` is the OpenAI `{ name, schema }` envelope.
* - `chat_template_kwargs` may be present; it is only meaningful on Workers AI.
*/
function prepareChatCompletionsRequest(input, modelId) {
	const quirks = quirksFor(modelId);
	const warnings = [];
	const body = { ...input };
	if (quirks.maxTokensField === "max_completion_tokens" && "max_tokens" in body) {
		body.max_completion_tokens = body.max_tokens;
		delete body.max_tokens;
	}
	if ("reasoning_effort" in body) {
		const effort = body.reasoning_effort;
		if (effort === null) {
			delete body.reasoning_effort;
			if (quirks.reasoningOff === "chat-template") body.chat_template_kwargs = {
				...isRecord$3(body.chat_template_kwargs) ? body.chat_template_kwargs : {},
				enable_thinking: false
			};
			else warnings.push({
				feature: "reasoning-off",
				message: `Reasoning cannot be turned off on ${modelId}; the request was sent with the model's default effort.`
			});
		} else if (typeof effort === "string" && quirks.reasoningEffortLevels === "basic") body.reasoning_effort = clampEffort(effort);
	}
	if ("chat_template_kwargs" in body && quirks.chatTemplateKwargs === "unsupported") {
		delete body.chat_template_kwargs;
		warnings.push({
			feature: "chat-template-kwargs",
			message: `${modelId} does not take chat_template_kwargs; they were dropped.`
		});
	}
	if (isRecord$3(body.response_format)) {
		if (quirks.jsonSchema === "unsupported") {
			delete body.response_format;
			warnings.push({
				feature: "response-format",
				message: `${modelId} does not support response_format; the request was sent without it.`
			});
		} else if (body.response_format.type === "json_schema" && isRecord$3(body.response_format.json_schema)) {
			const { strict: _strict, ...envelope } = body.response_format.json_schema;
			body.response_format = {
				...body.response_format,
				json_schema: quirks.jsonSchemaStrict ? {
					...envelope,
					strict: true
				} : envelope
			};
		}
	}
	if (body.tool_choice === "required" && quirks.toolChoiceRequired === "named-fallback") {
		const tools = Array.isArray(body.tools) ? body.tools : [];
		const only = tools.length === 1 && isRecord$3(tools[0]) ? tools[0] : void 0;
		const name = only !== void 0 && isRecord$3(only.function) && typeof only.function.name === "string" ? only.function.name : void 0;
		if (name !== void 0) body.tool_choice = {
			function: { name },
			type: "function"
		};
		else {
			delete body.tool_choice;
			warnings.push({
				feature: "tool-choice-required",
				message: `${modelId} rejects tool_choice "required"; the model was left free to choose.`
			});
		}
	}
	if (Array.isArray(body.messages)) {
		const toolCallIds = quirks.toolCallIds === "alnum-9" ? planToolCallIds(body.messages) : /* @__PURE__ */ new Map();
		if (toolCallIds.size > 0) warnings.push({
			feature: "tool-call-id",
			message: `${modelId} needs 9-character alphanumeric tool-call ids; ${toolCallIds.size} id${toolCallIds.size === 1 ? " was" : "s were"} rewritten.`
		});
		const messages = [];
		let previousRole;
		for (const entry of body.messages) {
			if (!isRecord$3(entry)) continue;
			let message = entry;
			if (toolCallIds.size > 0) message = rewriteToolCallIds(message, toolCallIds);
			if (message.role === "user" && quirks.imageUrls === "data-only") message = stripImageUrls(message, warnings);
			if (message.role === "assistant" && message.content === null && quirks.nullAssistantContent === "empty-string") message = {
				...message,
				content: ""
			};
			if (message.role === "assistant" && "reasoning_content" in message) {
				if (quirks.reasoningReplay === "drop") {
					const { reasoning_content: _reasoning, ...rest } = message;
					message = rest;
					warnings.push({
						feature: "assistant-reasoning",
						message: `${modelId} has no field for replayed reasoning; the assistant reasoning was dropped.`
					});
				}
			}
			if (quirks.requiresAssistantAfterToolResult && previousRole === "tool" && message.role === "user") messages.push({
				content: "",
				role: "assistant"
			});
			messages.push(message);
			previousRole = message.role;
		}
		body.messages = messages;
	}
	return {
		body,
		reasoningOff: isRecord$3(body.chat_template_kwargs) && body.chat_template_kwargs.enable_thinking === false,
		warnings
	};
}
//#endregion
//#region src/models/core/chat-completions/response.ts
/**
* Normalizes a chat-completions response body from the run path into the
* strict OpenAI `chat.completion` shape, so framework parsers never see
* Workers AI's native envelope or its field spellings.
*/
function isRecord$2(value) {
	return value !== null && typeof value === "object" && !Array.isArray(value);
}
/** Native `{ name, arguments: object }` → OpenAI `{ id, type, function }`. */
function normalizeToolCall(entry, index) {
	if (!isRecord$2(entry)) return void 0;
	const fn = isRecord$2(entry.function) ? entry.function : void 0;
	const name = typeof fn?.name === "string" ? fn.name : typeof entry.name === "string" ? entry.name : void 0;
	if (name === void 0) return void 0;
	const rawArguments = fn !== void 0 && "arguments" in fn ? fn.arguments : entry.arguments;
	const args = typeof rawArguments === "string" ? rawArguments : rawArguments === void 0 || rawArguments === null ? "{}" : JSON.stringify(rawArguments);
	return {
		...typeof entry.index === "number" ? { index: entry.index } : { index },
		...typeof entry.id === "string" ? { id: entry.id } : {},
		function: {
			arguments: args,
			name
		},
		type: "function",
		...entry.extra_content !== void 0 ? { extra_content: entry.extra_content } : {}
	};
}
/** Text as the model produced it; structured-output models may return an object. */
function contentText(value) {
	if (typeof value === "string") return value;
	if (value === null || value === void 0) return null;
	return JSON.stringify(value);
}
function stripEchoSuffix$1(model, modelId) {
	const suffix = quirksFor(modelId).modelEchoSuffix;
	if (typeof model !== "string" || suffix === void 0) return model;
	return model.endsWith(suffix) ? model.slice(0, -suffix.length) : model;
}
/** Puts reasoning under `reasoning_content` whichever spelling the model used. */
function normalizeMessage(message) {
	const out = { ...message };
	const reasoning = typeof out.reasoning_content === "string" && out.reasoning_content !== "" ? out.reasoning_content : typeof out.reasoning === "string" && out.reasoning !== "" ? out.reasoning : void 0;
	delete out.reasoning;
	if (reasoning !== void 0) out.reasoning_content = reasoning;
	else delete out.reasoning_content;
	if ("content" in out) out.content = contentText(out.content);
	if (Array.isArray(out.tool_calls)) {
		const calls = out.tool_calls.map((call, index) => normalizeToolCall(call, index)).filter((call) => call !== void 0);
		if (calls.length > 0) out.tool_calls = calls;
		else delete out.tool_calls;
	}
	return out;
}
/**
* Returns the body in the strict `chat.completion` shape. Already-OpenAI
* bodies come back with their reasoning spelling unified and the echoed model
* id cleaned; native Workers AI `{ response, tool_calls, usage }` bodies are
* lifted into `choices[0]`.
*/
function normalizeChatCompletion(body, modelId) {
	if (!isRecord$2(body)) return { choices: [] };
	if (Array.isArray(body.choices)) {
		const choices = body.choices.map((choice) => {
			if (!isRecord$2(choice)) return choice;
			const message = isRecord$2(choice.message) ? normalizeMessage(choice.message) : choice.message;
			const hasToolCalls = isRecord$2(message) && Array.isArray(message.tool_calls) && message.tool_calls.length > 0;
			return {
				...choice,
				message,
				finish_reason: choice.finish_reason ?? (hasToolCalls ? "tool_calls" : "stop")
			};
		});
		return {
			...body,
			choices,
			model: stripEchoSuffix$1(body.model, modelId)
		};
	}
	if ("response" in body || "tool_calls" in body) {
		const toolCalls = Array.isArray(body.tool_calls) ? body.tool_calls.map((call, index) => normalizeToolCall(call, index)).filter((call) => call !== void 0) : [];
		const { response, tool_calls: _toolCalls, usage, ...rest } = body;
		return {
			...rest,
			choices: [{
				finish_reason: typeof body.finish_reason === "string" ? body.finish_reason : toolCalls.length > 0 ? "tool_calls" : "stop",
				index: 0,
				message: {
					content: contentText(response),
					role: "assistant",
					...toolCalls.length > 0 ? { tool_calls: toolCalls } : {}
				}
			}],
			model: stripEchoSuffix$1(body.model, modelId),
			...usage !== void 0 ? { usage } : {}
		};
	}
	return body;
}
//#endregion
//#region src/models/core/chat-completions/anthropic-stream.ts
function isRecord$1(value) {
	return value !== null && typeof value === "object" && !Array.isArray(value);
}
/** Anthropic's stop reasons, as chat-completions finish reasons. */
const FINISH_REASONS = {
	end_turn: "stop",
	max_tokens: "length",
	pause_turn: "stop",
	refusal: "content_filter",
	stop_sequence: "stop",
	tool_use: "tool_calls"
};
/** Whether a parsed SSE payload is an Anthropic Messages event. */
function isAnthropicEvent(event) {
	return typeof event.type === "string" && !("choices" in event);
}
/**
* A stateful translator for one stream. Returns the chat-completions chunks
* an Anthropic event becomes — possibly none — or `undefined` for an event
* that is not Anthropic, which the caller handles as it is.
*/
function createAnthropicStreamTranslator() {
	let id;
	let model;
	let inputTokens = 0;
	let outputTokens = 0;
	let cachedTokens = 0;
	const toolIndexes = /* @__PURE__ */ new Map();
	const chunk = (delta, finishReason = null) => ({
		choices: [{
			delta,
			finish_reason: finishReason,
			index: 0
		}],
		...id === void 0 ? {} : { id },
		...model === void 0 ? {} : { model },
		object: "chat.completion.chunk"
	});
	return (event) => {
		if (!isAnthropicEvent(event)) return void 0;
		switch (event.type) {
			case "message_start": {
				const message = isRecord$1(event.message) ? event.message : {};
				if (typeof message.id === "string") id = message.id;
				if (typeof message.model === "string") model = message.model;
				const usage = isRecord$1(message.usage) ? message.usage : {};
				const cacheRead = Number(usage.cache_read_input_tokens ?? 0) || 0;
				const cacheWrite = Number(usage.cache_creation_input_tokens ?? 0) || 0;
				inputTokens = (Number(usage.input_tokens ?? 0) || 0) + cacheRead + cacheWrite;
				cachedTokens = cacheRead;
				outputTokens = Number(usage.output_tokens ?? 0) || 0;
				return [chunk({ role: "assistant" })];
			}
			case "content_block_start": {
				const block = isRecord$1(event.content_block) ? event.content_block : {};
				const index = Number(event.index ?? 0);
				if (block.type !== "tool_use") return [];
				const toolIndex = toolIndexes.size;
				toolIndexes.set(index, toolIndex);
				return [chunk({ tool_calls: [{
					function: {
						arguments: "",
						name: typeof block.name === "string" ? block.name : ""
					},
					id: typeof block.id === "string" ? block.id : void 0,
					index: toolIndex,
					type: "function"
				}] })];
			}
			case "content_block_delta": {
				const delta = isRecord$1(event.delta) ? event.delta : {};
				if (delta.type === "text_delta" && typeof delta.text === "string") return [chunk({ content: delta.text })];
				if (delta.type === "thinking_delta" && typeof delta.thinking === "string") return [chunk({ reasoning_content: delta.thinking })];
				if (delta.type === "input_json_delta" && typeof delta.partial_json === "string") {
					const toolIndex = toolIndexes.get(Number(event.index ?? 0));
					if (toolIndex === void 0) return [];
					return [chunk({ tool_calls: [{
						function: { arguments: delta.partial_json },
						index: toolIndex
					}] })];
				}
				return [];
			}
			case "message_delta": {
				const usage = isRecord$1(event.usage) ? event.usage : {};
				if (typeof usage.output_tokens === "number") outputTokens = usage.output_tokens;
				const delta = isRecord$1(event.delta) ? event.delta : {};
				const stop = typeof delta.stop_reason === "string" ? delta.stop_reason : void 0;
				if (stop === void 0) return [];
				return [chunk({}, FINISH_REASONS[stop] ?? "stop")];
			}
			case "message_stop": return [{
				choices: [],
				...id === void 0 ? {} : { id },
				...model === void 0 ? {} : { model },
				object: "chat.completion.chunk",
				usage: {
					completion_tokens: outputTokens,
					prompt_tokens: inputTokens,
					...cachedTokens > 0 ? { prompt_tokens_details: { cached_tokens: cachedTokens } } : {},
					total_tokens: inputTokens + outputTokens
				}
			}];
			case "error": {
				const error = isRecord$1(event.error) ? event.error : {};
				return [{ error: {
					message: typeof error.message === "string" ? error.message : "The model's stream ended with an error.",
					type: typeof error.type === "string" ? error.type : "error"
				} }];
			}
			default: return [];
		}
	};
}
//#endregion
//#region src/models/core/chat-completions/stream.ts
/**
* Normalizes a chat-completions SSE stream from the run path into strict
* OpenAI `chat.completion.chunk` events.
*
* Every Workers AI model streams the same three-part hybrid (live captures,
* all families): OpenAI-shaped delta chunks that each carry a per-delta
* `usage`, then a `choices: []` heartbeat, then a native
* `{ response: "", usage }` tail with the cumulative usage, then `[DONE]`.
* Some models also emit native `{ response: "<text>" }` deltas or top-level
* `tool_calls`. After this transform a consumer sees only:
*
* - delta chunks without per-delta usage and with reasoning under
*   `reasoning_content`;
* - at most one final `choices: []` chunk carrying the cumulative usage
*   (the OpenAI `stream_options.include_usage` shape);
* - a `finish_reason` on some chunk before the end, synthesized when the
*   model finished cleanly without sending one;
* - `[DONE]`.
*
* Input and output are SSE `data` payloads (strings), so it slots between the
* SSE decoder and either framework's OpenAI chunk parser.
*/
function isRecord(value) {
	return value !== null && typeof value === "object" && !Array.isArray(value);
}
const NOISE_FIELDS = [
	"p",
	"token_ids",
	"prompt_token_ids",
	"kv_transfer_params"
];
/** Fields the run path adds to every chunk that mean nothing to a consumer. */
function stripNoise(chunk) {
	const out = { ...chunk };
	for (const key of NOISE_FIELDS) delete out[key];
	return out;
}
function stripEchoSuffix(chunk, modelId) {
	const suffix = quirksFor(modelId).modelEchoSuffix;
	if (suffix === void 0 || typeof chunk.model !== "string") return chunk;
	return chunk.model.endsWith(suffix) ? {
		...chunk,
		model: chunk.model.slice(0, -suffix.length)
	} : chunk;
}
function normalizeDelta(delta, relabelReasoning) {
	const out = { ...delta };
	const reasoning = typeof out.reasoning_content === "string" && out.reasoning_content !== "" ? out.reasoning_content : typeof out.reasoning === "string" && out.reasoning !== "" ? out.reasoning : void 0;
	delete out.reasoning;
	delete out.reasoning_content;
	if (reasoning !== void 0) if (relabelReasoning) out.content = (typeof out.content === "string" ? out.content : "") + reasoning;
	else out.reasoning_content = reasoning;
	if (Array.isArray(out.tool_calls)) out.tool_calls = out.tool_calls.map((call, index) => {
		if (!isRecord(call)) return call;
		const fn = isRecord(call.function) ? call.function : void 0;
		const args = fn !== void 0 ? fn.arguments : call.arguments;
		return fn === void 0 && typeof call.name === "string" || isRecord(args) ? normalizeToolCall(call, index) ?? call : call;
	});
	return out;
}
/**
* The transform. Backpressure is preserved: nothing is buffered except the
* single pending usage chunk, which must wait for the tail.
*/
function normalizeChatCompletionsStream(modelId, options = {}) {
	const quirks = quirksFor(modelId);
	const relabelReasoning = options.reasoningOff === true && quirks.reasoningOffStreamLabel === "reasoning";
	let id;
	let model;
	let created;
	let sawFinishReason = false;
	let sawContent = false;
	let sawToolCalls = false;
	let sawDone = false;
	let finalUsage;
	let finalUsageTemplate;
	const perDeltaUsage = isWorkersAI(modelId);
	const translateAnthropic = createAnthropicStreamTranslator();
	const envelope = (extra) => ({
		...id !== void 0 ? { id } : {},
		object: "chat.completion.chunk",
		...created !== void 0 ? { created } : {},
		...model !== void 0 ? { model } : {},
		...extra
	});
	const emit = (controller, chunk) => {
		controller.enqueue(JSON.stringify(chunk));
	};
	const emitTail = (controller) => {
		if (!sawFinishReason && sawDone && (sawContent || sawToolCalls)) {
			emit(controller, envelope({ choices: [{
				delta: {},
				finish_reason: sawToolCalls ? "tool_calls" : "stop",
				index: 0
			}] }));
			sawFinishReason = true;
		}
		if (finalUsage !== void 0) {
			emit(controller, {
				...finalUsageTemplate ?? envelope({}),
				choices: [],
				usage: finalUsage
			});
			finalUsage = void 0;
		}
		if (sawDone) controller.enqueue("[DONE]");
	};
	/** Normalizes one chat-completions chunk (or native Workers AI event). */
	const handle = (parsed, controller) => {
		const chunk = stripEchoSuffix(stripNoise(parsed), modelId);
		if (typeof chunk.id === "string") id ??= chunk.id;
		if (typeof chunk.model === "string") model ??= chunk.model;
		if (typeof chunk.created === "number") created ??= chunk.created;
		if (isRecord(chunk.error)) {
			sawFinishReason = true;
			controller.enqueue(JSON.stringify(chunk));
			return;
		}
		if (Array.isArray(chunk.choices)) {
			if (chunk.choices.length === 0) {
				if (isRecord(chunk.usage)) {
					finalUsage = chunk.usage;
					finalUsageTemplate = {
						...chunk,
						usage: void 0
					};
					delete finalUsageTemplate.usage;
				}
				return;
			}
			const choices = chunk.choices.map((choice) => {
				if (!isRecord(choice)) return choice;
				if (typeof choice.finish_reason === "string") sawFinishReason = true;
				const delta = isRecord(choice.delta) ? normalizeDelta(choice.delta, relabelReasoning) : choice.delta;
				if (isRecord(delta)) {
					if (typeof delta.content === "string" && delta.content !== "") sawContent = true;
					if (typeof delta.reasoning_content === "string") sawContent = true;
					if (Array.isArray(delta.tool_calls) && delta.tool_calls.length > 0) sawToolCalls = true;
				}
				if (!perDeltaUsage) return {
					...choice,
					delta
				};
				const { usage: _perDeltaUsage, ...rest } = choice;
				return {
					...rest,
					delta
				};
			});
			if (perDeltaUsage) {
				const { usage: _usage, ...rest } = chunk;
				emit(controller, {
					...rest,
					choices
				});
			} else emit(controller, {
				...chunk,
				choices
			});
			return;
		}
		if ("response" in chunk || "tool_calls" in chunk) {
			const text = chunk.response;
			const hasText = typeof text === "string" && text !== "";
			const calls = Array.isArray(chunk.tool_calls) ? chunk.tool_calls.map((call, index) => normalizeToolCall(call, index)).filter((call) => call !== void 0) : [];
			if (hasText || calls.length > 0) {
				if (hasText) sawContent = true;
				if (calls.length > 0) sawToolCalls = true;
				emit(controller, envelope({ choices: [{
					delta: {
						...hasText ? { content: text } : {},
						...calls.length > 0 ? { tool_calls: calls } : {}
					},
					finish_reason: null,
					index: 0
				}] }));
			}
			if (isRecord(chunk.usage)) {
				finalUsage = chunk.usage;
				finalUsageTemplate = void 0;
			}
			if (typeof chunk.finish_reason === "string") {
				sawFinishReason = true;
				emit(controller, envelope({ choices: [{
					delta: {},
					finish_reason: chunk.finish_reason,
					index: 0
				}] }));
			}
		}
	};
	return new TransformStream({
		flush(controller) {
			emitTail(controller);
		},
		transform(data, controller) {
			if (data === "") return;
			if (data === "[DONE]") {
				sawDone = true;
				return;
			}
			let parsed;
			try {
				parsed = JSON.parse(data);
			} catch {
				return;
			}
			if (!isRecord(parsed)) return;
			for (const event of translateAnthropic(parsed) ?? [parsed]) handle(event, controller);
		}
	});
}
//#endregion
//#region src/models/core/sse.ts
/**
* Decodes a `text/event-stream` byte stream into the `data` payload of each
* event, as a `TransformStream` so the whole pipeline keeps backpressure and
* nothing is ever buffered beyond the current event.
*
* Handles all three line terminators the SSE spec allows (LF, CRLF and a bare
* CR), multi-line `data:` fields (joined with `\n`, per the spec), comment
* lines (`:` prefixed), and non-`data` fields (`event:`, `id:`, `retry:`),
* which are ignored. A trailing event that the server never terminated with a
* blank line is still flushed.
*
* @experimental This surface is experimental and may change.
*/
function sseDataStream() {
	const decoder = new TextDecoder();
	let buffer = "";
	let data = [];
	const flushEvent = (controller) => {
		if (data.length === 0) return;
		controller.enqueue(data.join("\n"));
		data = [];
	};
	const consumeLine = (line, controller) => {
		if (line === "") {
			flushEvent(controller);
			return;
		}
		if (line.startsWith(":")) return;
		const colon = line.indexOf(":");
		if ((colon === -1 ? line : line.slice(0, colon)) !== "data") return;
		let value = colon === -1 ? "" : line.slice(colon + 1);
		if (value.startsWith(" ")) value = value.slice(1);
		data.push(value);
	};
	/**
	* Consumes whole lines out of the buffer. A buffer ending in a lone `\r` is
	* held back while more bytes may arrive, because the next byte decides
	* whether that `\r` was a CRLF pair or a bare-CR terminator.
	*/
	const drain = (controller, final) => {
		for (;;) {
			const match = /\r\n|[\r\n]/.exec(buffer);
			if (match === null) break;
			const end = match.index + match[0].length;
			if (!final && match[0] === "\r" && end === buffer.length) break;
			consumeLine(buffer.slice(0, match.index), controller);
			buffer = buffer.slice(end);
		}
	};
	return new TransformStream({
		flush(controller) {
			buffer += decoder.decode();
			drain(controller, true);
			if (buffer !== "") {
				consumeLine(buffer, controller);
				buffer = "";
			}
			flushEvent(controller);
		},
		transform(chunk, controller) {
			buffer += decoder.decode(chunk, { stream: true });
			drain(controller, false);
		}
	});
}
//#endregion
//#region src/models/core/images.ts
/**
* `flux-1-schnell` takes `{ prompt, steps }` and nothing else — no size, no
* seed, no guidance. Sending those anyway would be silently ignored, so they
* come back as warnings instead.
*
* @see https://developers.cloudflare.com/workers-ai/models/flux-1-schnell/
*/
const PROMPT_AND_STEPS_ONLY = /^@cf\/black-forest-labs\/flux-1/;
/**
* Builds the run-path body for a text-to-image model: `prompt`, `width`,
* `height`, `num_steps`, `guidance`, `strength`, `negative_prompt` and `seed`,
* or the flux-1 two-key subset with a warning for everything dropped.
*
* @experimental This surface is experimental and may change.
*/
function buildImageInput(modelId, prompt, settings) {
	const warnings = [];
	const input = { prompt };
	if (PROMPT_AND_STEPS_ONLY.test(modelId)) {
		if (settings.steps !== void 0) input.steps = settings.steps;
		for (const [feature, value] of [
			["size", settings.width ?? settings.height],
			["seed", settings.seed],
			["guidance", settings.guidance],
			["negativePrompt", settings.negativePrompt],
			["strength", settings.strength],
			["files", settings.sourceImage === true ? true : void 0],
			["mask", settings.mask === true ? true : void 0]
		]) if (value !== void 0) warnings.push({
			feature,
			message: `${modelId} takes only a prompt and steps.`
		});
		return {
			input,
			minimal: true,
			warnings
		};
	}
	if (settings.width !== void 0) input.width = settings.width;
	if (settings.height !== void 0) input.height = settings.height;
	if (settings.seed !== void 0) input.seed = settings.seed;
	if (settings.steps !== void 0) input.num_steps = settings.steps;
	if (settings.guidance !== void 0) input.guidance = settings.guidance;
	if (settings.strength !== void 0) input.strength = settings.strength;
	if (settings.negativePrompt !== void 0) input.negative_prompt = settings.negativePrompt;
	return {
		input,
		minimal: false,
		warnings
	};
}
const JPEG_PREFIX = "/9j/";
const PNG_PREFIX = "iVBORw0KGgo";
const WEBP_PREFIX = "UklGR";
const GIF_PREFIX = "R0lGOD";
/**
* Sniffs an image media type from the head of its base64. The bytes decide,
* never the model name: `flux-1-schnell` answers JPEG where every other model
* in this catalog answers PNG.
*
* @experimental This surface is experimental and may change.
*/
function imageMediaTypeOf(base64) {
	if (base64.startsWith(JPEG_PREFIX)) return "image/jpeg";
	if (base64.startsWith(PNG_PREFIX)) return "image/png";
	if (base64.startsWith(WEBP_PREFIX)) return "image/webp";
	if (base64.startsWith(GIF_PREFIX)) return "image/gif";
	return "image/png";
}
//#endregion
export { isCatalogModelId as C, isRetryableStatus as S, isRunPathLanguageModelId as T, isGatewayErrorEnvelope as _, normalizeChatCompletion as a, classifyStatus as b, gatewaySlugForBaseUrl as c, resolveGatewayProvider as d, createTransport as f, headersToObject as g, errorFromResponse as h, normalizeChatCompletionsStream as i, isGatewayShapedUrl as l, errorFromGatewayEnvelope as m, imageMediaTypeOf as n, prepareChatCompletionsRequest as o, definedHeaders as p, sseDataStream as r, GATEWAY_PROVIDER_NAMES as s, buildImageInput as t, requireGatewayProvider as u, unwrapEnvelope as v, isDynamicRouteId as w, isAbortError as x, CloudflareAIError as y };

//# sourceMappingURL=images-CfuRCjwq.js.map