UNPKG

agents

Version:

A home for your AI agents

1,861 lines • 71 kB
import { C as isCatalogModelId, T as isRunPathLanguageModelId, _ as isGatewayErrorEnvelope, a as normalizeChatCompletion, c as gatewaySlugForBaseUrl, d as resolveGatewayProvider, f as createTransport, g as headersToObject, h as errorFromResponse, i as normalizeChatCompletionsStream, l as isGatewayShapedUrl, m as errorFromGatewayEnvelope, n as imageMediaTypeOf, o as prepareChatCompletionsRequest, r as sseDataStream, s as GATEWAY_PROVIDER_NAMES, t as buildImageInput, v as unwrapEnvelope, x as isAbortError, y as CloudflareAIError } from "../../images-CfuRCjwq.js";
import { i as mergeModelOptions, o as resolveOptions, r as gatewayLayerOf } from "../../settings-Dk2il2Hf.js";
import { ToolCallArgumentsStream, calculateCost, clampThinkingLevel, createAssistantMessageEventStream, createProvider, getCurrentTools, normalizeContext } from "@earendil-works/pi-ai";
import { CLOUDFLARE_AI_GATEWAY_MODELS } from "@earendil-works/pi-ai/providers/cloudflare-ai-gateway.models";
import { CLOUDFLARE_WORKERS_AI_MODELS } from "@earendil-works/pi-ai/providers/cloudflare-workers-ai.models";
import { convertMessages } from "@earendil-works/pi-ai/api/openai-completions";
import { anthropicMessagesApi } from "@earendil-works/pi-ai/api/anthropic-messages.lazy";
import { adjustMaxTokensForThinking } from "@earendil-works/pi-ai/api/simple-options";
import { convertResponsesMessages, convertResponsesTools, processResponsesStream } from "@earendil-works/pi-ai/api/openai-responses-shared";
import { clampOpenAIPromptCacheKey } from "@earendil-works/pi-ai/api/openai-prompt-cache";
//#region src/models/pi-ai/catalog.ts
/**
* The `api` marker Workers AI models from this provider carry. It is not a
* wire format: Workers AI runs on `env.AI.run`, and the compat layer maps that
* onto strict OpenAI chat completions.
*/
const CLOUDFLARE_AI_API = "cloudflare-ai";
/**
* The pi-ai provider id. A `Models` registry resolves specifiers as
* `cloudflare/<id>`, e.g. `cloudflare/anthropic/claude-opus-4.8`.
*/
const CLOUDFLARE_PROVIDER_ID = "cloudflare";
const MODEL_OPTIONS = Symbol.for("agents.models.pi-ai.options");
const MODEL_ROUTING = Symbol.for("agents.models.pi-ai.routing");
function tag(model, key, value) {
	Object.defineProperty(model, key, {
		configurable: true,
		enumerable: true,
		value,
		writable: false
	});
}
/** Reads the options a model was created with, if it came from this provider. */
function optionsOf(model) {
	const options = model[MODEL_OPTIONS];
	return options !== null && typeof options === "object" ? options : void 0;
}
/**
* The gateway registry entry a model's own id names, for a model this provider
* built from a `<slug>/<id>` specifier. It is looked up rather than inferred,
* so a model that lost its symbol tags — `JSON.stringify` and
* `structuredClone` drop them, and an Agent's state goes through both — still
* reaches the vendor under the vendor's own spelling.
*/
function registryEntryFor(model) {
	if (model.provider !== "cloudflare" || model.id.startsWith("@cf/")) return;
	gatewayRegistry ??= buildGatewayRegistry();
	return gatewayRegistry.get(model.id);
}
/**
* The id the request body must carry. It differs from `model.id` only for a
* `<slug>/<id>` specifier, whose model is listed under the specifier so a
* `Models` registry can find it while the vendor still sees its own spelling.
*/
function wireModelId(model) {
	const tagged = model[MODEL_ROUTING]?.modelId;
	if (tagged !== void 0) return tagged;
	return registryEntryFor(model)?.id ?? model.id;
}
/** The `<slug>/<id>` specifier a model was resolved from, for diagnostics. */
function specifierOf(model) {
	const tagged = model[MODEL_ROUTING]?.specifier;
	if (tagged !== void 0) return tagged;
	return registryEntryFor(model) === void 0 ? void 0 : model.id;
}
/**
* Copies a caller's model and tags the copy with the resolved options. The
* caller's object is never mutated, and every field it declares — `api`,
* `provider`, `id`, `baseUrl`, `compat`, `thinkingLevelMap`, cost and limits —
* is kept verbatim, because that metadata is the model's own author's, not
* ours.
*/
function withOptions(model, options) {
	const copy = { ...model };
	if (options !== void 0) tag(copy, MODEL_OPTIONS, options);
	return copy;
}
const DEFAULT_CONTEXT_WINDOW = 128e3;
const DEFAULT_MAX_TOKENS = 8192;
const ZERO_COST = {
	cacheRead: 0,
	cacheWrite: 0,
	input: 0,
	output: 0
};
let workersAI;
function workersAIEntry(modelId) {
	workersAI ??= new Map(Object.values(CLOUDFLARE_WORKERS_AI_MODELS).map((entry) => [entry.id, entry]));
	return workersAI.get(modelId);
}
/** Every Workers AI id pi-ai knows, for the provider's static model list. */
function workersAIModelIds() {
	workersAI ??= new Map(Object.values(CLOUDFLARE_WORKERS_AI_MODELS).map((entry) => [entry.id, entry]));
	return [...workersAI.keys()].sort();
}
/**
* Builds the pi-ai model for a Workers AI id. Registry metadata is used when
* pi-ai knows the id; explicit options override it; everything else falls back
* to defaults that keep the model usable, because the Workers AI catalog moves
* faster than any generated registry.
*/
function buildWorkersAIModel(modelId, options) {
	const known = workersAIEntry(modelId);
	const model = {
		api: CLOUDFLARE_AI_API,
		baseUrl: "",
		contextWindow: options?.contextWindow ?? known?.contextWindow ?? DEFAULT_CONTEXT_WINDOW,
		cost: options?.cost ?? known?.cost ?? ZERO_COST,
		id: modelId,
		input: options?.input ?? known?.input ?? ["text"],
		maxTokens: options?.maxTokens ?? known?.maxTokens ?? DEFAULT_MAX_TOKENS,
		name: options?.name ?? known?.name ?? modelId,
		provider: CLOUDFLARE_PROVIDER_ID,
		reasoning: options?.reasoning ?? known?.reasoning ?? true,
		...known?.thinkingLevelMap !== void 0 ? { thinkingLevelMap: known.thinkingLevelMap } : {}
	};
	if (options !== void 0) tag(model, MODEL_OPTIONS, options);
	return model;
}
/** The gateway provider slug a registry entry's base URL names. */
function slugOf(entry) {
	return entry.baseUrl.split("/").filter((part) => part !== "").at(-1);
}
/**
* The id AI Gateway's model catalog — `env.AI.run` — knows a registry entry
* by. pi-ai spells Claude ids as Anthropic does (`claude-opus-4-8`), because pi
* sends them through the gateway's Anthropic passthrough, while the catalog
* puts a dot between version numbers (`anthropic/claude-opus-4.8`) and answers
* `Model not found` to the dashed spelling. Checked against the live catalog
* for every entry in pi-ai 1.2: that dot is the only difference, and only
* Anthropic ids have it.
*/
function gatewayCatalogId(slug, id) {
	return `${slug}/${slug === "anthropic" ? id.replace(/(\d)-(\d)(?!\d)/g, "$1.$2") : id}`;
}
let gatewayRegistry;
/**
* pi-ai's generated Cloudflare AI Gateway registry, keyed by the id AI
* Gateway's catalog runs each entry under (see {@link gatewayCatalogId}).
* `/compat` entries are skipped: they alias `@cf/` ids the Workers AI registry
* already declares, and those belong on the run path.
*/
function buildGatewayRegistry() {
	const map = /* @__PURE__ */ new Map();
	for (const entry of Object.values(CLOUDFLARE_AI_GATEWAY_MODELS)) {
		const slug = slugOf(entry);
		if (slug === void 0 || slug === "compat") continue;
		map.set(gatewayCatalogId(slug, entry.id), entry);
	}
	return map;
}
/** Every `<slug>/<id>` specifier pi-ai's gateway registry declares. */
function gatewayModelIds() {
	gatewayRegistry ??= buildGatewayRegistry();
	return [...gatewayRegistry.keys()].sort();
}
/** An id with case and separators (`.`, `-`, `_`) ignored. */
function looseId(id) {
	return id.toLowerCase().replace(/[._-]/g, "-");
}
/**
* Catalog specifiers that differ from `specifier` only in case or separators.
* AI Gateway's catalog spells `anthropic/claude-opus-4.8` where Anthropic
* spells `claude-opus-4-8`, and the difference is easy to miss.
*/
function nearSpecifiers(specifier, specifiers) {
	const wanted = looseId(specifier);
	return [...specifiers].filter((id) => looseId(id) === wanted).sort();
}
/**
* Resolves a `<slug>/<id>` specifier through pi-ai's generated Cloudflare AI
* Gateway registry. The entry's metadata — api, base URL, compat,
* `thinkingLevelMap`, cost, limits — is kept exactly as pi generated it; only
* `id` and `provider` are restated so a `Models` registry can find the model
* under `cloudflare/<slug>/<id>`, and the vendor's own spelling rides along for
* the request body.
*
* There is no prefix guessing: an id pi-ai does not list is a `TypeError`,
* pointing at the model-object form, which needs no catalog of ours at all.
*/
function buildGatewayModel(specifier, options) {
	gatewayRegistry ??= buildGatewayRegistry();
	const separator = specifier.indexOf("/");
	const slug = separator === -1 ? "" : specifier.slice(0, separator);
	const vendorId = specifier.slice(separator + 1);
	if (slug === "workers-ai") throw new TypeError(`"${specifier}" is a Workers AI model: pass its id directly, ai("${vendorId}").`);
	const entry = gatewayRegistry.get(specifier);
	if (entry === void 0) {
		const near = nearSpecifiers(specifier, gatewayRegistry.keys());
		const suggestion = near.length === 0 ? "" : ` Did you mean ${near.map((id) => `ai("${id}")`).join(" or ")}? The catalog spells ids as AI Gateway does.`;
		throw new TypeError(`"${specifier}" is not in pi-ai's Cloudflare AI Gateway catalog.${suggestion} Pass the model object instead, e.g. ai(getBuiltinModel("${slug || "anthropic"}", "${vendorId}")) from "@earendil-works/pi-ai/providers/all", or any model from a provider registry you imported.`);
	}
	const model = {
		...entry,
		id: specifier,
		provider: CLOUDFLARE_PROVIDER_ID
	};
	tag(model, MODEL_ROUTING, {
		modelId: entry.id,
		specifier
	});
	if (options !== void 0) tag(model, MODEL_OPTIONS, options);
	return model;
}
//#endregion
//#region src/models/pi-ai/routing.ts
/**
* The path each pi api speaks, relative to the model's own `baseUrl` — exactly
* what pi's SDK clients append to it.
*/
const API_PATHS = {
	"anthropic-messages": "v1/messages",
	"openai-completions": "chat/completions",
	"openai-responses": "responses"
};
/**
* The provider-native `endpoint` for a gateway-shaped base URL, whose path
* already ends at the provider slug, so what follows is the vendor's own path.
*/
const GATEWAY_ENDPOINTS = {
	"anthropic-messages": "v1/messages",
	"openai-completions": "v1/chat/completions",
	"openai-responses": "v1/responses"
};
/**
* The gateway provider slug that serves a model, from its own base URL.
*
* @throws TypeError when the model carries no base URL, or one AI Gateway has
* no provider for.
*/
function gatewaySlugForModel(model) {
	if (model.baseUrl === "") throw new TypeError(`The model "${model.id}" has no baseUrl, so AI Gateway cannot tell which provider serves it.`);
	return gatewaySlugForBaseUrl(model.baseUrl);
}
/** `https://api.groq.com/openai/v1` → `https://api.groq.com/openai/v1/`. */
function asBase(baseUrl) {
	return baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
}
/**
* The provider slug and endpoint one model's request travels on.
*
* @throws TypeError when the model has no base URL, when AI Gateway has no
* provider for it, or when this module knows no path for the model's api.
*/
function routeForModel(model) {
	const provider = gatewaySlugForModel(model);
	if (isGatewayShapedUrl(model.baseUrl)) {
		const endpoint = GATEWAY_ENDPOINTS[model.api];
		if (endpoint === void 0) throw noEndpoint(model.api);
		return {
			endpoint,
			provider
		};
	}
	const path = API_PATHS[model.api];
	if (path === void 0) throw noEndpoint(model.api);
	const resolved = resolveGatewayProvider(new URL(path, asBase(model.baseUrl)).toString());
	if (resolved === void 0) return {
		endpoint: GATEWAY_ENDPOINTS[model.api] ?? path,
		provider
	};
	return {
		endpoint: resolved.endpoint,
		provider: resolved.provider
	};
}
function noEndpoint(api) {
	return /* @__PURE__ */ new TypeError(`No AI Gateway endpoint is known for the "${api}" API.`);
}
//#endregion
//#region src/models/pi-ai/wires/shared.ts
/** Diagnostic type under which gateway correlation ids are recorded. */
const CLOUDFLARE_DIAGNOSTIC = "cloudflare";
/** The diagnostic type under which compat-layer warnings are recorded. */
const COMPAT_DIAGNOSTIC = "cloudflare-compat";
/** Records compat warnings on a message as a `cloudflare-compat` diagnostic. */
/**
* pi-ai types diagnostic details as JSON. These details are plain data this
* module built, so a JSON round trip only drops their undefined fields.
*/
function jsonDetails(value) {
	return JSON.parse(JSON.stringify(value));
}
function recordWarnings(output, warnings) {
	if (warnings.length === 0) return;
	output.diagnostics = [...output.diagnostics ?? [], {
		details: jsonDetails({ warnings }),
		timestamp: Date.now(),
		type: COMPAT_DIAGNOSTIC
	}];
}
/**
* The Workers-AI-only knobs a vendor model cannot honour. They are not sent —
* the vendor's own reasoning follows the model's metadata and the pi
* `reasoning` level — and the drop is recorded rather than silent.
*/
function ignoredWorkersAIKnobs(request) {
	const warnings = [];
	if (request.resolved.reasoningEffort !== void 0) warnings.push({
		feature: "reasoning-effort",
		message: "reasoningEffort is a Workers AI setting and does not apply to a model routed through AI Gateway; use the pi `reasoning` level, which the model's own metadata maps."
	});
	if (request.resolved.chatTemplateKwargs !== void 0) warnings.push({
		feature: "chat-template-kwargs",
		message: "chatTemplateKwargs is a Workers AI setting and was not sent to a model routed through AI Gateway."
	});
	return warnings;
}
function emptyUsage() {
	return {
		cacheRead: 0,
		cacheWrite: 0,
		cost: {
			cacheRead: 0,
			cacheWrite: 0,
			input: 0,
			output: 0,
			total: 0
		},
		input: 0,
		output: 0,
		totalTokens: 0
	};
}
/** The assistant message a stream mutates as events arrive. */
function startMessage(model) {
	return {
		api: model.api,
		content: [],
		model: model.id,
		provider: model.provider,
		role: "assistant",
		stopReason: "stop",
		timestamp: Date.now(),
		usage: emptyUsage()
	};
}
/**
* Maps OpenAI-shaped usage onto pi-ai's. `input` excludes cached tokens, as
* pi-ai's own OpenAI adapter does, and cost comes from the model metadata.
*/
function usageFromOpenAI(model, raw) {
	if (raw === void 0) return emptyUsage();
	const prompt = raw.prompt_tokens ?? 0;
	const completion = raw.completion_tokens ?? 0;
	const cacheRead = raw.prompt_tokens_details?.cached_tokens ?? raw.prompt_cache_hit_tokens ?? 0;
	const cacheWrite = raw.prompt_tokens_details?.cache_write_tokens ?? 0;
	const reasoning = raw.completion_tokens_details?.reasoning_tokens;
	const totalTokens = raw.total_tokens ?? prompt + completion;
	const output = raw.total_tokens === void 0 ? completion : Math.max(completion, totalTokens - prompt);
	const usage = {
		cacheRead,
		cacheWrite,
		cost: {
			cacheRead: 0,
			cacheWrite: 0,
			input: 0,
			output: 0,
			total: 0
		},
		input: Math.max(0, prompt - cacheRead - cacheWrite),
		output,
		...reasoning !== void 0 ? { reasoning } : {},
		totalTokens
	};
	usage.cost = calculateCost(model, usage);
	return usage;
}
function correlationOf(response, transport) {
	const get = (name) => response.headers.get(name) ?? void 0;
	return {
		cacheStatus: get("cf-aig-cache-status"),
		eventId: get("cf-aig-event-id"),
		logId: get("cf-aig-log-id") ?? transport.logIdFallback(response),
		runId: get("cf-aig-run-id"),
		step: get("cf-aig-step"),
		traceId: get("cf-aig-trace-id")
	};
}
function definedEntries$1(record) {
	const result = {};
	for (const [key, value] of Object.entries(record)) if (value !== void 0) result[key] = value;
	return result;
}
/**
* Records where a response came from on the assistant message, as a pi-ai
* diagnostic: the gateway log id and friends, the answering model and the
* gateway id. Callers read it back from `message.diagnostics`.
*/
function attachCorrelation(message, details) {
	const diagnostic = {
		details: jsonDetails(definedEntries$1({ ...details })),
		timestamp: Date.now(),
		type: CLOUDFLARE_DIAGNOSTIC
	};
	message.diagnostics = [...message.diagnostics ?? [], diagnostic];
}
/**
* Wraps a body so a gap longer than `idleMs` between chunks rejects the read
* instead of pending forever. Only an outstanding read is timed, so a slow
* consumer never trips it; `idleMs <= 0` disables the guard.
*/
function withIdleDeadline(body, idleMs) {
	if (idleMs <= 0) return body;
	const reader = body.getReader();
	return new ReadableStream({
		cancel(reason) {
			return reader.cancel(reason);
		},
		async pull(controller) {
			let timer;
			try {
				const result = await Promise.race([reader.read(), new Promise((_, reject) => {
					timer = setTimeout(() => {
						reject(/* @__PURE__ */ new Error(`Model stream timed out: no data received for ${Math.round(idleMs / 1e3)}s.`));
					}, idleMs);
				})]);
				if (result.done) controller.close();
				else controller.enqueue(result.value);
			} catch (error) {
				reader.cancel().catch(() => {});
				controller.error(error);
			} finally {
				clearTimeout(timer);
			}
		}
	});
}
/**
* Iterates the JSON payload of each SSE event. `[DONE]` sentinels and
* unparseable payloads are skipped; the reader is cancelled if iteration
* stops early so the upstream request is not left generating.
*/
async function* sseJson(body, onDone) {
	const reader = body.pipeThrough(sseDataStream()).getReader();
	let finished = false;
	try {
		while (true) {
			const { done, value } = await reader.read();
			if (done) {
				finished = true;
				return;
			}
			if (value === "") continue;
			if (value === "[DONE]") {
				onDone?.();
				continue;
			}
			try {
				yield JSON.parse(value);
			} catch {}
		}
	} finally {
		if (!finished) await reader.cancel().catch(() => {});
		reader.releaseLock();
	}
}
/** `response.headers` as pi-ai's `onResponse` callback wants them. */
function responseInfo(response) {
	return {
		headers: headersToObject(response.headers),
		status: response.status
	};
}
/**
* The Anthropic API version every Messages request must name. pi-ai leaves it
* to the Anthropic SDK, which we replace, so the wire states it here.
*/
const ANTHROPIC_VERSION = "2023-06-01";
/**
* The vendor headers an Anthropic Messages request needs. pi-ai normally gets
* them from the Anthropic SDK's defaults plus its own `createClient`; this
* wire owns the transport instead, so it sets the ones the vendor requires and
* sends the beta features pi-ai chose as `anthropic-beta`, as the SDK would.
* No credential header is sent: the gateway holds the key (unified billing or
* BYOK).
*/
function anthropicVendorHeaders(betas) {
	return {
		accept: "application/json",
		"anthropic-version": ANTHROPIC_VERSION,
		...betas.length > 0 ? { "anthropic-beta": betas.join(",") } : {},
		"content-type": "application/json"
	};
}
/**
* The vendor headers `/run` also carries. The rest (`anthropic-version`,
* `accept`, `content-type`) belong to the gateway there, but `anthropic-beta`
* holds the beta features the body was built for: without it Anthropic refuses
* a body that uses one, or quietly drops the feature. The binding sends
* `extraHeaders` as request headers, and AI Gateway passes this one on.
*/
const RUN_VENDOR_HEADERS = ["anthropic-beta"];
/**
* The headers a `/run` request sends: the vendor headers in
* {@link RUN_VENDOR_HEADERS}, then the caller's own, which win. Names compare
* case-insensitively, so a caller's `Anthropic-Beta` replaces the wire's
* rather than being sent alongside it.
*/
function runHeaders(vendorHeaders, callerHeaders) {
	const callerNames = new Set(Object.keys(callerHeaders).map((name) => name.toLowerCase()));
	const headers = {};
	for (const name of RUN_VENDOR_HEADERS) {
		const value = vendorHeaders[name];
		if (value !== void 0 && !callerNames.has(name)) headers[name] = value;
	}
	return {
		...headers,
		...callerHeaders
	};
}
/** The vendor headers an OpenAI-shaped request needs. */
function openAIVendorHeaders() {
	return {
		accept: "application/json",
		"content-type": "application/json"
	};
}
/**
* The AI Gateway catalog id a model runs under on `/run`, when it has one: the
* `<author>/<model>` specifier it was resolved from, spelled as the gateway's
* catalog spells it (`anthropic/claude-opus-4.8`, not pi's `claude-opus-4-8`).
*/
function catalogIdOf(model) {
	const specifier = specifierOf(model);
	return specifier !== void 0 && isCatalogModelId(specifier) ? specifier : void 0;
}
/**
* Sends a vendor-format request body through AI Gateway, by the binding.
*
* A model from pi's Cloudflare AI Gateway catalog runs on `env.AI.run` under
* its catalog id, in its native `format`: `/run` converts nothing, so the
* answer — streams included — is the shape this wire parses. Any other model
* goes to the gateway's provider-native path with the request unchanged; the
* provider slug comes from the model's base URL and the endpoint from its
* `api`.
*
* The gateway's *own* failures (no such gateway, unified billing unpaid, an
* unknown catalog model) become a `CloudflareAIError`, so fallback and retry
* behave as they do on the Workers AI run path; a vendor's own error response
* on the provider-native path is left for the wire's parser to read.
*/
async function sendGateway(request, transport, body, vendorHeaders, format, onResponse) {
	const { model, options, resolved } = request;
	const catalogId = catalogIdOf(model);
	if (catalogId !== void 0) {
		const { model: _vendorId, ...input } = body;
		const response = await transport.run({
			format,
			gateway: resolved.gateway,
			headers: runHeaders(vendorHeaders, request.headers),
			input,
			model: catalogId,
			signal: options.signal
		});
		onResponse?.(response);
		if (!response.ok) throw await errorFromResponse(response, {
			model: catalogId,
			requestBodyValues: input,
			url: transport.url
		});
		return response;
	}
	const route = routeForModel(model);
	const response = await transport.native({
		body: JSON.stringify(body),
		endpoint: route.endpoint,
		gateway: resolved.gateway,
		headers: {
			...vendorHeaders,
			...request.headers
		},
		method: "POST",
		provider: route.provider,
		signal: options.signal
	});
	onResponse?.(response);
	return await liftGatewayError(response, {
		model: wireModelId(model),
		requestBodyValues: body,
		url: transport.url
	});
}
/**
* Turns the gateway's own error envelope into a `CloudflareAIError`. Only a
* JSON body can be one, so a streaming response is passed straight through and
* never buffered.
*/
async function liftGatewayError(response, context) {
	if (!(response.headers.get("content-type") ?? "").includes("application/json")) return response;
	const text = await response.text().catch(() => "");
	let parsed;
	try {
		parsed = text === "" ? void 0 : JSON.parse(text);
	} catch {
		parsed = void 0;
	}
	if (isGatewayErrorEnvelope(parsed)) throw errorFromGatewayEnvelope(parsed, {
		...context,
		responseBody: text,
		responseHeaders: headersToObject(response.headers),
		status: response.status
	});
	return new Response(text, response);
}
/** The correlation details every wire records, with the routing it used. */
function correlationDetails(request, response, transport) {
	const { model, resolved } = request;
	const specifier = specifierOf(model);
	return {
		...correlationOf(response, transport),
		gateway: resolved.gateway.id,
		model: model.id,
		...specifier !== void 0 ? { specifier } : {},
		...model.api === "cloudflare-ai" ? {} : { provider: gatewaySlugForModel(model) }
	};
}
//#endregion
//#region src/models/pi-ai/errors.ts
/** Diagnostic type under which a failed dispatch is recorded. */
const CLOUDFLARE_ERROR_DIAGNOSTIC = "cloudflare-error";
function definedEntries(record) {
	const result = {};
	for (const [key, value] of Object.entries(record)) if (value !== void 0) result[key] = value;
	return result;
}
/**
* Terminates a stream with a pi-ai `error` event built from a thrown value.
* `aborted` when the caller's signal fired or the error is an abort; `error`
* otherwise, with the failed dispatch recorded as a diagnostic.
*/
function failStream(stream, message, error, signal) {
	const aborted = signal?.aborted === true || isAbortError(error);
	const reason = aborted ? "aborted" : "error";
	message.stopReason = reason;
	message.errorMessage = error instanceof Error ? error.message : JSON.stringify(error);
	if (!aborted) {
		const details = {};
		if (error instanceof CloudflareAIError) Object.assign(details, definedEntries({
			code: error.code,
			logId: error.logId,
			model: error.model,
			status: error.status
		}));
		message.diagnostics = [...message.diagnostics ?? [], {
			details: jsonDetails(details),
			error: {
				message: message.errorMessage,
				name: error instanceof Error ? error.name : "Error",
				...error instanceof CloudflareAIError ? { code: error.code } : {}
			},
			timestamp: Date.now(),
			type: CLOUDFLARE_ERROR_DIAGNOSTIC
		}];
	}
	stream.push({
		error: message,
		reason,
		type: "error"
	});
	stream.end(message);
}
/** Throws the typed error for a non-2xx response. */
async function assertOk(response, context) {
	if (response.ok) return;
	throw await errorFromResponse(response, context);
}
//#endregion
//#region src/models/pi-ai/wires/chat-completions.ts
/**
* Chat-completions wire: Workers AI on the run path, and any pi-ai model whose
* `api` is `openai-completions`, through AI Gateway.
*
* The request is shaped with pi-ai's own message converter so replay rules
* (tool-call id normalization, reasoning replay, role bridging) match pi's
* other OpenAI-compatible providers. Workers AI then runs through the shared
* compat layer, which absorbs its native events, heartbeats, per-delta usage
* and per-family request quirks so the parser below only ever sees strict
* OpenAI chunks. A vendor model brings its own `compat` profile and needs none
* of that: its bytes are already what its author says they are.
*/
/**
* pi-ai's message converter wants a fully resolved compat profile. This is
* the strict OpenAI profile: replayed reasoning goes to `reasoning_content`,
* `max_tokens` is the cap, and nothing vendor-specific is applied here — the
* shared compat layer handles that per model afterwards.
*/
const STRICT_OPENAI_COMPAT = {
	chatTemplateArgs: {},
	chatTemplateKwargs: {},
	maxTokensField: "max_tokens",
	openRouterRouting: {},
	requiresAssistantAfterToolResult: false,
	requiresReasoningContentOnAssistantMessages: false,
	requiresThinkingAsText: false,
	requiresToolResultName: false,
	sendSessionAffinityHeaders: false,
	sessionAffinityFormat: "openai",
	supportsDeveloperRole: false,
	supportsFinishReason: true,
	supportsLongCacheRetention: false,
	supportsOpenAIGrammarTools: false,
	supportsReasoningEffort: true,
	supportsStore: false,
	supportsStrictMode: false,
	supportsUsageInStreaming: true,
	thinkingFormat: "openai",
	vercelGatewayRouting: {},
	zaiToolStream: false
};
/**
* The fields an OpenAI-compatible endpoint puts reasoning in, in pi-ai's own
* order (`dist/api/openai-completions.js`). The first non-empty one wins and
* the rest are skipped: some endpoints send the same text under two spellings,
* and reading both would double the thinking block.
*/
const REASONING_FIELDS = [
	"reasoning_content",
	"reasoning",
	"reasoning_text"
];
function mapFinishReason(reason) {
	switch (reason) {
		case "stop":
		case "eos": return "stop";
		case "length":
		case "model_length": return "length";
		case "tool_calls":
		case "function_call": return "toolUse";
		default: return "error";
	}
}
function convertTools(tools) {
	return tools.map((tool) => ({
		function: {
			description: tool.description,
			name: tool.name,
			parameters: tool.parameters
		},
		type: "function"
	}));
}
/** Whether a tool-call chunk is the null finalizer some vLLM parsers emit. */
function isFinalizer(call) {
	const fn = call.function;
	return call.index === void 0 && (call.id === null || call.id === void 0) && (fn?.name === null || fn?.name === void 0) && (fn?.arguments === null || fn?.arguments === void 0 || fn?.arguments === "");
}
/**
* Incremental state for one streamed completion: the blocks being built on
* the assistant message and the events that announce them.
*/
var CompletionAssembler = class {
	constructor(model, output, stream) {
		this.model = model;
		this.output = output;
		this.stream = stream;
		this.openToolCalls = /* @__PURE__ */ new Map();
		this.toolCallArguments = /* @__PURE__ */ new Map();
		this.closedToolCalls = /* @__PURE__ */ new Set();
		this.blocks = output.content;
	}
	indexOf(block) {
		return this.blocks.indexOf(block);
	}
	endText() {
		if (this.textBlock === void 0) return;
		this.stream.push({
			content: this.textBlock.text,
			contentIndex: this.indexOf(this.textBlock),
			partial: this.output,
			type: "text_end"
		});
		this.textBlock = void 0;
	}
	endThinking() {
		if (this.thinkingBlock === void 0) return;
		this.stream.push({
			content: this.thinkingBlock.thinking,
			contentIndex: this.indexOf(this.thinkingBlock),
			partial: this.output,
			type: "thinking_end"
		});
		this.thinkingBlock = void 0;
	}
	pushText(delta) {
		if (delta.length === 0) return;
		this.endThinking();
		if (this.textBlock === void 0) {
			this.textBlock = {
				text: "",
				type: "text"
			};
			this.blocks.push(this.textBlock);
			this.stream.push({
				contentIndex: this.indexOf(this.textBlock),
				partial: this.output,
				type: "text_start"
			});
		}
		this.textBlock.text += delta;
		this.stream.push({
			contentIndex: this.indexOf(this.textBlock),
			delta,
			partial: this.output,
			type: "text_delta"
		});
	}
	pushThinking(delta, signature) {
		if (delta.length === 0) return;
		this.endText();
		if (this.thinkingBlock === void 0) {
			this.thinkingBlock = {
				thinking: "",
				thinkingSignature: signature,
				type: "thinking"
			};
			this.blocks.push(this.thinkingBlock);
			this.stream.push({
				contentIndex: this.indexOf(this.thinkingBlock),
				partial: this.output,
				type: "thinking_start"
			});
		}
		this.thinkingBlock.thinking += delta;
		this.stream.push({
			contentIndex: this.indexOf(this.thinkingBlock),
			delta,
			partial: this.output,
			type: "thinking_delta"
		});
	}
	closeToolCall(index) {
		const block = this.openToolCalls.get(index);
		if (block === void 0 || this.closedToolCalls.has(index)) return;
		this.closedToolCalls.add(index);
		const args = this.toolCallArguments.get(index);
		this.toolCallArguments.delete(index);
		Object.assign(block, (args ?? new ToolCallArgumentsStream()).finish());
		this.stream.push({
			contentIndex: this.indexOf(block),
			partial: this.output,
			toolCall: block,
			type: "toolcall_end"
		});
	}
	pushToolCalls(calls) {
		this.endThinking();
		this.endText();
		for (const call of calls) {
			if (isFinalizer(call)) {
				if (this.lastToolIndex !== void 0) this.closeToolCall(this.lastToolIndex);
				continue;
			}
			const name = call.function?.name ?? void 0;
			const args = call.function?.arguments ?? "";
			const index = typeof call.index === "number" ? call.index : this.lastToolIndex ?? 0;
			if (this.closedToolCalls.has(index)) continue;
			let block = this.openToolCalls.get(index);
			if (block === void 0) {
				if (this.lastToolIndex !== void 0 && this.lastToolIndex !== index) this.closeToolCall(this.lastToolIndex);
				block = {
					arguments: {},
					id: call.id ?? crypto.randomUUID(),
					name: name ?? "",
					type: "toolCall"
				};
				this.openToolCalls.set(index, block);
				this.toolCallArguments.set(index, new ToolCallArgumentsStream());
				this.blocks.push(block);
				this.stream.push({
					contentIndex: this.indexOf(block),
					partial: this.output,
					type: "toolcall_start"
				});
			} else {
				if (block.name === "" && name) block.name = name;
				if (call.id) block.id = call.id;
			}
			this.lastToolIndex = index;
			if (args.length > 0) {
				let stream = this.toolCallArguments.get(index);
				if (stream === void 0) {
					stream = new ToolCallArgumentsStream();
					this.toolCallArguments.set(index, stream);
				}
				block.arguments = stream.append(args);
				this.stream.push({
					contentIndex: this.indexOf(block),
					delta: args,
					partial: this.output,
					type: "toolcall_delta"
				});
			}
		}
	}
	applyFinishReason(reason) {
		if (reason === null || reason === void 0) return false;
		this.output.stopReason = mapFinishReason(reason);
		if (this.output.stopReason === "error") this.output.errorMessage = `The model stopped with finish_reason "${reason}".`;
		return true;
	}
	/** Applies one strict chunk (or a whole normalized body); true if it carried a finish reason. */
	apply(chunk) {
		if (chunk.error !== void 0) {
			this.output.stopReason = "error";
			this.output.errorMessage = chunk.error.message ?? "The model reported an error.";
			return true;
		}
		const choice = chunk.choices?.[0];
		if (chunk.id !== void 0) this.output.responseId ??= chunk.id;
		if (typeof chunk.model === "string" && chunk.model.length > 0 && chunk.model !== this.model.id) this.output.responseModel ??= chunk.model;
		if (chunk.usage !== void 0 && chunk.usage !== null) this.output.usage = usageFromOpenAI(this.model, chunk.usage);
		const finished = this.applyFinishReason(choice?.finish_reason);
		const delta = choice?.delta ?? choice?.message;
		if (delta !== void 0) {
			for (const field of REASONING_FIELDS) {
				const value = delta[field];
				if (typeof value === "string" && value.length > 0) {
					this.pushThinking(value, field);
					break;
				}
			}
			if (typeof delta.content === "string") this.pushText(delta.content);
			if (Array.isArray(delta.tool_calls)) {
				const complete = choice?.delta === void 0;
				this.pushToolCalls(complete ? delta.tool_calls.map((call, position) => ({
					...call,
					index: typeof call.index === "number" ? call.index : position
				})) : delta.tool_calls);
			}
		}
		return finished;
	}
	/** Closes open blocks and settles the stop reason. */
	finish(sawFinishReason, sawDone) {
		for (const index of this.openToolCalls.keys()) this.closeToolCall(index);
		this.endThinking();
		this.endText();
		const hasToolCalls = this.blocks.some((b) => b.type === "toolCall");
		if (this.output.stopReason === "error") return;
		if (!sawFinishReason) if (sawDone) this.output.stopReason = hasToolCalls ? "toolUse" : "stop";
		else {
			this.output.stopReason = "error";
			this.output.errorMessage = this.blocks.length > 0 ? "The stream ended without the model finishing its answer." : "The stream ended without any output from the model.";
		}
		else if (this.output.stopReason === "stop" && hasToolCalls) this.output.stopReason = "toolUse";
	}
};
/**
* Puts the reasoning knobs on a chat-completions body, and reports the ones a
* model cannot take.
*
* Workers AI is the one model family with knobs of ours: `reasoning_effort` on
* its own three-level scale and `chat_template_kwargs`, both of which the
* compat layer re-checks against the quirk table afterwards. For every other
* model the decision is the model author's, read off the merged compat profile
* exactly as pi-ai's own `openai-completions` implementation reads it: the
* effort is emitted only when the model reasons, declares
* `supportsReasoningEffort` and asks for OpenAI's `reasoning_effort` shape,
* and it is remapped through the model's own `thinkingLevelMap`.
*/
function applyReasoning(strict, context) {
	const { chatTemplateKwargs, compat, effort, model, workersAI } = context;
	if (workersAI) {
		if (effort !== void 0) strict.reasoning_effort = effort;
		if (chatTemplateKwargs !== void 0) strict.chat_template_kwargs = chatTemplateKwargs;
		return [];
	}
	const ignored = [];
	const thinkingFormat = compat.thinkingFormat;
	if (typeof effort === "string") {
		if (model.reasoning === true && compat.supportsReasoningEffort !== false && thinkingFormat === "openai") strict.reasoning_effort = model.thinkingLevelMap?.[effort] ?? effort;
		else if (thinkingFormat !== "openai") ignored.push({
			feature: "reasoning-effort",
			message: `This model declares the "${thinkingFormat}" thinking format, which this wire does not build; its reasoning level was dropped. Reach it through pi-ai's own provider if you need that shape.`
		});
	}
	if (context.workersAIEffort === null) ignored.push({
		feature: "reasoning-off",
		message: "`reasoningEffort: null` is a Workers AI setting; a model routed through AI Gateway decides its own reasoning, so it was dropped."
	});
	else if (context.workersAIEffort !== void 0) ignored.push({
		feature: "reasoning-effort",
		message: "`reasoningEffort` is a Workers AI setting and does not apply to a model routed through AI Gateway; use the pi `reasoning` level, which the model's own metadata maps."
	});
	if (chatTemplateKwargs !== void 0) ignored.push({
		feature: "chat-template-kwargs",
		message: "`chatTemplateKwargs` is a Workers AI setting and does not apply to a model routed through AI Gateway; it was dropped."
	});
	return ignored;
}
/** Streams a chat-completions request and maps it onto pi-ai events. */
function streamCompletions(request, transport) {
	const { model, context, options, resolved } = request;
	const stream = createAssistantMessageEventStream();
	const output = startMessage(model);
	const assembler = new CompletionAssembler(model, output, stream);
	const workersAI = model.api === CLOUDFLARE_AI_API;
	(async () => {
		let response;
		try {
			const compat = workersAI ? STRICT_OPENAI_COMPAT : {
				...STRICT_OPENAI_COMPAT,
				...model.compat ?? {}
			};
			const strict = {
				messages: convertMessages(model, context, compat),
				stream: true,
				stream_options: { include_usage: true }
			};
			if (!workersAI) strict.model = wireModelId(model);
			const tools = getCurrentTools(context.messages);
			if (tools.length > 0) strict.tools = convertTools(tools);
			if (request.toolChoice !== void 0) strict.tool_choice = request.toolChoice;
			if (options.maxTokens !== void 0) strict.max_tokens = options.maxTokens;
			if (options.temperature !== void 0) strict.temperature = options.temperature;
			const ignored = applyReasoning(strict, {
				chatTemplateKwargs: resolved.chatTemplateKwargs,
				compat,
				effort: request.reasoningEffort,
				model,
				workersAI,
				workersAIEffort: resolved.reasoningEffort
			});
			const sampling = options.samplingParams;
			if (sampling !== void 0) Object.assign(strict, sampling);
			const prepared = workersAI ? prepareChatCompletionsRequest(strict, model.id) : {
				body: strict,
				reasoningOff: false,
				warnings: []
			};
			recordWarnings(output, [...ignored, ...prepared.warnings]);
			const overridden = await options.onPayload?.(prepared.body, model);
			const payload = overridden === void 0 ? prepared.body : overridden;
			response = workersAI ? await transport.run({
				format: "chat-completions",
				gateway: resolved.gateway,
				headers: request.headers,
				input: payload,
				model: model.id,
				signal: options.signal
			}) : await sendGateway(request, transport, payload, openAIVendorHeaders(), "chat-completions");
			await options.onResponse?.(responseInfo(response), model);
			attachCorrelation(output, correlationDetails(request, response, transport));
			await assertOk(response, {
				model: model.id,
				requestBodyValues: payload,
				url: transport.url
			});
			if (response.body === null) throw new Error("The model returned an empty response body.");
			stream.push({
				partial: output,
				type: "start"
			});
			if ((response.headers.get("content-type") ?? "").includes("text/event-stream")) {
				let sawFinishReason = false;
				let sawDone = false;
				const decoded = withIdleDeadline(response.body, request.streamIdleTimeoutMs).pipeThrough(sseDataStream());
				const reader = (workersAI ? decoded.pipeThrough(normalizeChatCompletionsStream(model.id, { reasoningOff: prepared.reasoningOff })) : decoded).getReader();
				try {
					for (;;) {
						const { done, value } = await reader.read();
						if (done) break;
						if (value === "[DONE]") {
							sawDone = true;
							continue;
						}
						let parsed;
						try {
							parsed = JSON.parse(value);
						} catch {
							continue;
						}
						if (parsed === null || typeof parsed !== "object") continue;
						sawFinishReason = assembler.apply(parsed) || sawFinishReason;
					}
				} finally {
					reader.releaseLock();
				}
				assembler.finish(sawFinishReason, sawDone);
			} else {
				const body = await response.json();
				const json = workersAI ? normalizeChatCompletion(body, model.id) : body;
				assembler.finish(assembler.apply(json), true);
			}
			if (options.signal?.aborted) throw new Error("Request was aborted.");
			if (output.stopReason === "error") throw new Error(output.errorMessage ?? "The model reported an error stop reason.");
			stream.push({
				message: output,
				reason: output.stopReason,
				type: "done"
			});
			stream.end(output);
		} catch (error) {
			if (response?.body !== null && response?.body?.locked === false) response.body.cancel().catch(() => {});
			failStream(stream, output, error, options.signal);
		}
	})();
	return stream;
}
//#endregion
//#region src/models/pi-ai/models/image.ts
/**
* The `api` marker on image models from this provider.
*/
const CLOUDFLARE_AI_IMAGES_API = "cloudflare-ai-images";
const IMAGE_OPTIONS = Symbol.for("agents.models.pi-ai.image-options");
/** Builds the pi image model for a catalog id. */
function buildImagesModel(modelId, options) {
	const model = {
		api: CLOUDFLARE_AI_IMAGES_API,
		baseUrl: "",
		cost: {
			cacheRead: 0,
			cacheWrite: 0,
			input: 0,
			output: 0
		},
		id: modelId,
		input: ["text", "image"],
		name: options?.name ?? modelId,
		output: ["image"],
		provider: CLOUDFLARE_PROVIDER_ID,
		type: "image"
	};
	if (options !== void 0) Object.defineProperty(model, IMAGE_OPTIONS, {
		configurable: true,
		enumerable: true,
		value: options,
		writable: false
	});
	return model;
}
function imageOptionsOf(model) {
	const options = model[IMAGE_OPTIONS];
	return options !== null && typeof options === "object" ? options : void 0;
}
function base64ToBytes(base64) {
	const binary = atob(base64);
	const bytes = new Uint8Array(binary.length);
	for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
	return bytes;
}
function bytesToBase64(bytes) {
	let binary = "";
	const chunk = 8192;
	for (let i = 0; i < bytes.length; i += chunk) binary += String.fromCharCode(...bytes.subarray(i, i + chunk));
	return btoa(binary);
}
/** Reads an image out of the run path's response, whichever shape it used. */
async function readImage(response) {
	const contentType = response.headers.get("content-type") ?? "";
	if (contentType.includes("application/json")) {
		const body = unwrapEnvelope(await response.json());
		const record = body !== null && typeof body === "object" ? body : {};
		if (typeof record.image === "string") return {
			data: record.image,
			mimeType: imageMediaTypeOf(record.image)
		};
		throw new Error("The image model returned JSON without an image field.");
	}
	const data = bytesToBase64(new Uint8Array(await response.arrayBuffer()));
	return {
		data,
		mimeType: contentType.split(";")[0] || imageMediaTypeOf(data)
	};
}
/**
* Generates images with a Cloudflare catalog model. Text input parts join into
* the prompt; the first image part, when present, is passed as the source
* image for image-to-image models. Failures are reported through
* `stopReason`/`errorMessage`, as pi-ai's images contract expects.
*/
async function generateImages(config, model, context, options) {
	const modelOptions = imageOptionsOf(model);
	const resolved = resolveOptions(config.providerGateway, modelOptions, void 0);
	const prompt = context.input.filter((part) => part.type === "text").map((part) => part.text).join("\n");
	const source = context.input.find((part) => part.type === "image");
	const setting = (key) => options?.[key] !== void 0 ? options[key] : modelOptions?.[key];
	const built = buildImageInput(model.id, prompt, {
		...setting("width") === void 0 ? {} : { width: setting("width") },
		...setting("height") === void 0 ? {} : { height: setting("height") },
		...setting("steps") === void 0 ? {} : { steps: setting("steps") },
		...setting("guidance") === void 0 ? {} : { guidance: setting("guidance") },
		...setting("negativePrompt") === void 0 ? {} : { negativePrompt: setting("negativePrompt") },
		...setting("seed") === void 0 ? {} : { seed: setting("seed") },
		...source === void 0 ? {} : { sourceImage: true }
	});
	const input = built.input;
	const warnings = built.warnings;
	if (!built.minimal && source !== void 0 && source.type === "image") input.image = Array.from(base64ToBytes(source.data));
	const result = {
		api: model.api,
		model: model.id,
		output: [],
		provider: model.provider,
		stopReason: "stop",
		timestamp: Date.now(),
		usage: emptyUsage()
	};
	try {
		const response = await config.transport.run({
			gateway: resolved.gateway,
			headers: resolved.headers,
			input,
			model: model.id,
			signal: options?.signal
		});
		await assertOk(response, {
			model: model.id,
			requestBodyValues: input,
			url: config.transport.url
		});
		const image = await readImage(response);
		result.output.push({
			data: image.data,
			mimeType: image.mimeType,
			type: "image"
		});
		const requestId = response.headers.get("cf-aig-log-id");
		if (requestId !== null) result.responseId = requestId;
	} catch (error) {
		result.stopReason = options?.signal?.aborted ? "aborted" : "error";
		result.errorMessage = error instanceof Error ? error.message : JSON.stringify(error);
	}
	if (warnings.length > 0) result.diagnostics = [{
		details: { warnings },
		timestamp: Date.now(),
		type: COMPAT_DIAGNOSTIC
	}];
	return result;
}
//#endregion
//#region src/models/pi-ai/settings.ts
/** The pi thinking levels, for reading a caller's explicit effort. */
const THINKING_LEVELS = [
	"minimal",
	"low",
	"medium",
	"high",
	"xhigh"
];
function isThinkingLevel(value) {
	return typeof value === "string" && THINKING_LEVELS.includes(value);
}
/**
* Resolves a pi thinking level onto the effort a request carries.
*
* Workers AI is the one catalog with an effort scale of ours: its quirk table
* declares three levels, so the run path collapses onto them. Every other
* model keeps its own level name — the wire then applies the model's own
* `thinkingLevelMap`, the way pi-ai's implementations do — because a table of
* ours has no business deciding a vendor's efforts.
*/
function effortForLevel$1(model, level) {
	if (level === void 0 || !model.reasoning) return void 0;
	const clamped = clampThinkingLevel(model, level);
	if (clamped === "off") return void 0;
	if (model.api !== "cloudflare-ai") return clamped;
	switch (clamped) {
		case "minimal":
		case "low": return "low";
		case "medium": return "medium";
		case "high":
		case "xhigh":
		case "max": return "high";
		default: return clamped;
	}
}
/** Scalars only: the gateway rejects nested metadata. */
function scalarMetadata(metadata) {
	if (metadata === void 0) return void 0;
	const result = {};
	for (const [key, value] of Object.entries(metadata)) if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") result[key] = value;
	return Object.keys(result).length > 0 ? result : void 0;
}
/** pi headers allow `null` to suppress a default; those are dropped here. */
function stringHeaders(headers) {
	if (headers === void 0) return void 0;
	const result = {};
	for (const [key, value] of Object.entries(headers)) if (typeof value === "string") result[key] = value;
	return Object.keys(result).length > 0 ? result : void 0;
}
/** The per-call layer of options, read off pi-ai's call options. */
function callLayer(options) {
	const layer = {};
	const headers = stringHeaders(options.headers);
	if (headers !== void 0) layer.headers = headers;
	const metadata = scalarMetadata(options.metadata);
	if (metadata !== void 0) layer.metadata = metadata;
	if (options.sessionId !== void 0) layer.sessionAffinity = options.sessionId;
	return layer;
}
function toolChoiceOf(options) {
	const raw = options.toolChoice;
	if (raw === "auto" || raw === "none" || raw === "required") return raw;
	if (raw === "any") return "required";
	if (raw !== null && typeof raw === "object") {
		const record = raw;
		const name = typeof record.function?.name === "string" ? record.function.name : typeof record.name === "string" ? record.name : void 0;
		if (name !== void 0) return {
			function: { name },
			type: "function"
		};
	}
}
function buildRequest(model, context, options, simple, config) {
	const modelOptions = optionsOf(model);
	const resolved = resolveOptions(config.providerGateway, modelOptions, callLayer(options));
	const headers = { ...resolved.headers };
	if (resolved.sessionAffinity !== void 0 && model.api === "cloudflare-ai") headers["x-session-affinity"] = resolved.sessionAffinity;
	const explicitEffort = options.reasoningEffort;
	const reasoningEffort = resolved.reasoningEffort !== void 0 && model.api === "cloudflare-ai" ? resolved.reasoningEffort : isThinkingLevel(explicitEffort) ? effortForLevel$1(model, explicitEffort) : explicitEffort === "off" ? void 0 : typeof explicitEffort === "string" ? explicitEffort : simple ? effortForLevel$1(model, options.reasoning) : void 0;
	return {
		context,
		fallback: modelOptions?.fallback ?? [],
		headers,
		model,
		options,
		reasoningEffort,
		resolved,
		simple,
		streamIdleTimeoutMs: modelOptions?.streamIdleTimeoutMs ?? 3e5,
		toolChoice: toolChoiceOf(options)
	};
}
//#endregion
//#region src/models/pi-ai/wires/anthropic.ts
/**
* Anthropic Messages wire: pi-ai's own Anthropic implementation, over AI
* Gateway — `/run` for a catalog model, the provider-native path otherwise.
*
* Everything about the wire — request shaping, thinking, tool use, stream
* parsing, thinking-signature replay — is pi-ai's. It accepts a `client` whose
* `messages.create(...).asResponse()` returns the raw HTTP response; the one
* built here routes that call through the gateway instead of the Anthropic
* SDK, and sets the vendor headers the SDK would otherwise have added.
*
* The model object is never rewritten: `api`, `provider` and `id` reach pi's
* implementation exactly as they will be recorded on the assistant message, so
* a later turn replaying a thinking block is recognised as the same model.
*/
/**
* Maps a pi thinking level onto an Anthropic effort the way pi-ai's own
* `streamSimple` does: the model's `thinkingLevelMap` first, then the level's
* own name. The map is the model author's; nothing here decides it.
*/
function effortForLevel(model, level) {
	const mapped = model.thinkingLevelMap?.[level];
	if (typeof mapped === "string") return mapped;
	switch (level) {
		case "minimal":
		case "low": return "low";
		case "medium": return "medium";
		default: return "high";
	}
}
/**
* pi-ai's `streamSimple` thinking rules, applied here because that entry point
* insists on a credential and this transport has none. Which branch a model
* takes is the model's own `compat.forceAdaptiveThinking`, not a rule of ours.
*/
function thinkingOptions(model, options) {
	const level = options.reasoning;
	if (level === void 0 || !model.reasoning) return { thinkingEnabled: false };
	if (model.compat?.forceAdaptiveThinking === true) return {
		effort: effortForLevel(model, level),
		thinkingEnabled: true
	};
	const { maxTokens, thinkingBudget } = adjustMaxTokensForThinking(options.maxTokens, model.maxTokens, level, options.thinkingBudgets);
	return {
		maxTokens,
		thinkingBudgetTokens: thinkingBudget,
		thinkingEnabled: true
	};
}
/** Streams an Anthropic request through pi-ai's implementation. */
function streamAnthropic(request, transport) {
	const { model, context, options } = request;
	const outer = createAssistantMessageEventStream();
	const placeholder = startMessage(model);
	let correlation;
	let transportError;
	const create = (params) => ({ async asResponse() {
		const { betas, ...vendorParams } = params;
		const body = {
			...vendorParams,
			model: wireModelId(model)
		};
		try {
			const response = await sendGateway(request, transport, body, anthropicVendorHeaders(Array.isArray(betas) ? betas : []), "messages", (raw) => {
				correlation = correlationDetails(request, raw, transport);
			});
			await assertOk(response, {
				model: model.id,
				requestBodyValues: body,
				url: transport.url
			});
			if (response.body === null) return response;
			return new Response(withIdleDeadline(response.body, request.streamIdleTimeoutMs), response);
		} catch (error) {
			transportError = error;
			throw error;
		}
	} });
	const client = {
		beta: { messages: { create } },
		messages: { create }
	};
	const anthropicOptions = {
		...options,
		...request.simple ? thinkingOptions(model, options) : {},
		client,
		...request.toolChoice !== void 0 ? { toolChoice: typeof request.toolChoice === "string" ? request.toolChoice === "required" ? "any" : request.toolChoice : {
			name: request.toolChoice.function.name,
			type: "tool"
		} } : {}
	};
	(async () => {
		try {
			const inner = anthropicMessagesApi().stream(model, context, anthropicOptions);
			for await (const event of inner) {
				if (event.type === "error" && transportError !== void 0) {
					failFrom(event.error);
					return;
				}
				if (event.type === "done" || event.type === "error") {
					const message = event.type === "done" ? event.message : event.error;
					if (correlation !== void 0) attachCorrelation(message, correlation);
					recordWarnings(message, ignoredWorkersAIKnobs(request));
				}
				outer.push(event);
			}
			if (transportError !== void 0) {
				failFrom(placeholder);
				return;
			}
			outer.end(await inner.result());
		} catch (error) {
			failStream(outer, placeholder, transportError ?? error, options.signal);
		}
	})();
	/** Ends the stream with the transport's own error, keeping any content. */
	function failFrom(message) {
		if (correlation !== void 0) attachCorrelation(message, correlation);
		failStream(outer, message, transportError, options.signal);
	}
	return outer;
}
//#endregion
//#region src/models/pi-ai/wires/responses.ts
/**
* OpenAI Responses wire: pi-ai's own Responses converters and stream
* processor, over AI Gateway: `/run` for a catalog model, the provider-native
* path otherwise.
*
* Only the transport differs from talking to OpenAI directly: the body goes to
* the gateway, which forwards it to `api.openai.com/v1/responses` with the key
* it holds, and the raw SSE `Response` comes back. The model object is passed
* through untouched, so the encrypted reasoning items pi replays on a later
* turn are recognised as the same model's.
*/
/**
* Providers whose tool-call ids carry OpenAI `call|item` pairing across
* turns. This provider sanitizes ids instead, like pi-ai's own gateway
* provider, so the set is empty.
*/
const TOOL_CALL_ID_PROVIDERS = /* @__PURE__ */ new Set();
const TERMINAL_EVENTS = /* @__PURE__ */ new Set([
	"response.completed",
	"response.incomplete",
	"response.failed"
]);
/** A level through the model's own map, or the level's own name. */
function mappedEffort(model, effort) {
	return model.thinkingLevelMap?.[effort] ?? effort;
}
/** Streams a Responses request and maps it onto pi-ai events. */
function streamResponses(request, transport) {
	const { model, context, options, resolved } = request;
	const stream = createAssistantMessageEventStream();
	const output = startMessage(model);
	const observed = {
		endedCleanly: false,
		sawTerminalEvent: false
	};
	(async () => {
		let response;
		try {
			const responsesModel = model;
			const body = {
				input: convertResponsesMessages(responsesModel, context, TOOL_CALL_ID_PROVIDERS),
				model: wireModelId(model),
				store: false,
				stream: true
			};
			const tools = getCurrentTools(context.messages);
			if (tools.length > 0) body.tools = convertResponsesTools(tools);
			if (request.toolChoice !== void 0) body.tool_choice = typeof request.toolChoice === "string" ? request.toolChoice : {
				name: request.toolChoice.function.name,
				type: "function"
			};
			if (options.maxTokens !== void 0) body.max_output_tokens = Math.max(options.maxTokens, 16);
			if (options.temperature !== void 0) body.temperature = options.temperature;
			const cacheKey = clampOpenAIPromptCacheKey(resolved.sessionAffinity ?? options.sessionId);
			if (cacheKey !== void 0) body.prompt_cache_key = cacheKey;
			if (model.reasoning === true) {
				body.include = ["reasoning.encrypted_content"];
				const effort = request.reasoningEffort;
				if (typeof effort === "string") body.reasoning = {
					effort: mappedEffort(model, effort),
					summary: "auto"
				};
				else if (model.thinkingLevelMap?.off !== null) body.reasoning = { effort: model.thinkingLevelMap?.off ?? "none" };
			}
			const sampling = options.samplingParams;
			if (sampling !== void 0) Object.assign(body, sampling);
			const overridden = await options.onPayload?.(body, model);
			const payload = overridden === void 0 ? body : overridden;
			response = await sendGateway(request, transport, payload, openAIVendorHeaders(), "responses");
			await options.onResponse?.(responseInfo(response), model);
			attachCorrelation(output, correlationDetails(request, response, transport));
			recordWarnings(output, ignoredWorkersAIKnobs(request));
			await assertOk(response, {
				model: model.id,
				requestBodyValues: payload,
				url: transport.url
			});
			if (response.body === null) throw new Error("The model returned an empty response body.");
			stream.push({
				partial: output,
				type: "start"
			});
			await processResponsesStream(observe(sseJson(withIdleDeadline(response.body, request.streamIdleTimeoutMs))), output, stream, responsesModel);
			if (options.signal?.aborted) throw new Error("Request was aborted.");
			if (output.stopReason === "aborted" || output.stopReason === "error") throw new Error(output.errorMessage ?? "The model reported an error stop reason.");
			if (observed.endedCleanly && !observed.sawTerminalEvent) throw new Error("The stream ended without a terminal response event.");
			stream.push({
				message: output,
				reason: output.stopReason,
				type: "done"
			});
			stream.end(output);
		} catch (error) {
			if (response?.body !== null && response?.body?.locked === false) response.body.cancel().catch(() => {});
			failStream(stream, output, error, options.signal);
		}
	})();
	return stream;
	async function* observe(events) {
		for await (const event of events) {
			const type = event?.type;
			if (typeof type === "string" && TERMINAL_EVENTS.has(type)) observed.sawTerminalEvent = true;
			yield event;
		}
		observed.endedCleanly = true;
	}
}
//#endregion
//#region src/models/pi-ai/models/fallback.ts
/**
* Client-side model fallback for pi-ai streams.
*
* A leg is abandoned when its stream terminates with an `error` event before
* it produced any content; the next model is tried with the same context and
* options. Once a leg has emitted content it is committed to, and a later
* error propagates. `start` and the block starts that may follow it carry no
* content, so they are held back until a leg proves itself.
*/
/** Diagnostic type under which abandoned legs are recorded. */
const FALLBACK_DIAGNOSTIC = "cloudflare-fallback";
/**
* Events that announce output without carrying any. A leg that emits only
* these and then fails has produced nothing, so they are held back with
* `start` until the leg proves itself.
*/
const PREAMBLE_EVENTS = /* @__PURE__ */ new Set([
	"start",
	"text_start",
	"thinking_start",
	"toolcall_start"
]);
function isAbandonable(event) {
	return event.type === "error" && event.reason === "error";
}
/**
* Runs legs in order, committing to the first one that produces output.
*
* The returned stream always ends. A leg that throws, a stream that rejects,
* a leg that starts answering and then ends without a `done` or `error`
* event, and a last leg that ends without producing anything all end it
* with an `error` event, so a caller awaiting the result is never left
* waiting.
*/
function streamWithFallback(legs, signal) {
	const first = legs[0];
	if (first === void 0) throw new Error("streamWithFallback needs at least one leg.");
	const outer = createAssistantMessageEventStream();
	const attempts = [];
	let ended = false;
	const end = (message) => {
		ended = true;
		outer.end(message);
	};
	const recordAttempts = (message) => {
		message.diagnostics = [...message.diagnostics ?? [], {
			details: jsonDetails({ attempts }),
			timestamp: Date.now(),
			type: FALLBACK_DIAGNOSTIC
		}];
	};
	(async () => {
		let current = first;
		let lastError;
		try {
			for (const [index, leg] of legs.entries()) {
				current = leg;
				const isLast = index === legs.length - 1;
				const inner = leg.start();
				let pending = [];
				let committed = false;
				let finished = false;
				for await (const event of inner) {
					if (!committed && PREAMBLE_EVENTS.has(event.type)) {
						pending.push(event);
						continue;
					}
					if (!committed) {
						if (event.type === "error" && isAbandonable(event) && !isLast) {
							attempts.push({
								errorMessage: event.error.errorMessage,
								model: leg.model.id
							});
							lastError = event.error;
							break;
						}
						committed = true;
						for (const held of pending) outer.push(held);
						pending = [];
					}
					if ((event.type === "done" || event.type === "error") && attempts.length > 0) recordAttempts(event.type === "done" ? event.message : event.error);
					outer.push(event);
					if (event.type === "done" || event.type === "error") finished = true;
				}
				if (finished) {
					end(await inner.result());
					return;
				}
				if (committed) throw new Error(`${leg.model.id} ended its stream without a done or error event.`);
			}
			if (lastError !== void 0) {
				recordAttempts(lastError);
				outer.push({
					error: lastError,
					reason: "error",
					type: "error"
				});
				end(lastError);
				return;
			}
			throw new Error(`${current.model.id} ended its stream without output.`);
		} catch (error) {
			if (ended) return;
			const message = startMessage(current.model);
			if (attempts.length > 0) recordAttempts(message);
			ended = true;
			failStream(outer, message, error, signal);
		}
	})();
	return outer;
}
//#endregion
//#region src/models/pi-ai/models/language.ts
/**
* The pi-ai language dispatcher: picks the wire from the model's own `api`
* marker and runs the request through the transport, with client-side fallback
* across models.
*
* The `api` is the model author's declaration, not a guess of ours: Workers AI
* models carry {@link CLOUDFLARE_AI_API} and run on `env.AI.run`; every other
* model carries whatever protocol its registry says it speaks, and goes to AI
* Gateway in that protocol: `/run` for a catalog model, the provider-native
* path otherwise.
*/
/** A stream that fails immediately, for a model this provider cannot route. */
function failedStream(request, error) {
	const stream = createAssistantMessageEventStream();
	failStream(stream, startMessage(request.model), error, request.options.signal);
	return stream;
}
function dispatchOne(request, transport) {
	switch (request.model.api) {
		case CLOUDFLARE_AI_API:
		case "openai-completions": return streamCompletions(request, transport);
		case "anthropic-messages": return streamAnthropic(request, transport);
		case "openai-responses": return streamResponses(request, transport);
		default: return failedStream(request, /* @__PURE__ */ new TypeError(`agents/models/pi-ai cannot route the "${request.model.api}" API. AI Gateway serves anthropic-messages, openai-responses and openai-completions models; Workers AI models use "${CLOUDFLARE_AI_API}".`));
	}
}
/**
* The options a fallback leg inherits from the chain: the transport bag, and
* nothing that describes a model.
*
* `name`, `cost`, `contextWindow`, `maxTokens`, `reasoning` and `input` were
* written about the model they were written on — a leg has its own, from pi's
* registry — and inheriting them bills the leg's answer at the primary's
* prices. The leg's own `fallback` is dropped for the same reason `ai()`
* expands a chain once: a leg is already part of one.
*/
function legOptions(options) {
	if (options === void 0) return void 0;
	const { fallback: _fallback, name: _name, cost: _cost, contextWindow: _contextWindow, maxTokens: _maxTokens, reasoning: _reasoning, input: _input, ...rest } = options;
	return rest;
}
/** A leg may be given as a Workers AI id or as a model of any kind. */
function legModel(leg, options) {
	if (typeof leg === "string") return isRunPathLanguageModelId(leg) ? buildWorkersAIModel(leg, options) : buildGatewayModel(leg, options);
	const own = optionsOf(leg);
	const inherited = gatewayLayerOf(options);
	return withOptions(leg, mergeModelOptions(own, Object.keys(inherited).length > 0 ? inherited : void 0));
}
/**
* Builds pi-ai's `stream` and `streamSimple` for one provider instance. Any
* model reaches them — this provider's own, or one the caller built from their
* registry and handed to `ai(model)` — and dispatches on its `api`.
*/
function createStreams(config) {
	const run = (model, context, options, simple) => {
		const callOptions = options ?? {};
		const primary = buildRequest(model, context, callOptions, simple, config);
		if (primary.fallback.length === 0) return dispatchOne(primary, config.transport);
		const shared = legOptions(optionsOf(model));
		return streamWithFallback([{
			model,
			start: () => dispatchOne(primary, config.transport)
		}, ...primary.fallback.map((leg) => {
			const legTarget = legModel(leg, shared);
			return {
				model: legTarget,
				start: () => dispatchOne(buildRequest(legTarget, context, callOptions, simple, config), config.transport)
			};
		})], callOptions.signal);
	};
	return {
		stream: (model, context, options) => run(model, context, options, false),
		streamSimple: (model, context, options) => run(model, context, options, true)
	};
}
//#endregion
//#region src/models/pi-ai/index.ts
/**
* `agents/models/pi-ai` — Cloudflare's AI as a pi-ai provider, with the same
* shape as `agents/models/ai-sdk`.
*
* One rule decides everything here: **Workers AI is the catalog Cloudflare
* keeps; every other vendor is pi-ai's.** So there are two call forms, and no
* third:
*
* ```ts
* import { createAI } from "agents/models/pi-ai";
* import { getBuiltinModel } from "@earendil-works/pi-ai/providers/all";
*
* const ai = createAI({ binding: env.AI, id: "prod" });
*
* ai("@cf/zai-org/glm-5.3-flash");                     // Workers AI: ours
* ai(getBuiltinModel("anthropic", "claude-opus-4-8")); // theirs, our transport
* ai("anthropic/claude-opus-4-8");                     // same, via pi's catalog
* ```
*
* Workers AI runs on `env.AI.run` with Cloudflare's own compat layer. Every
* other model goes to AI Gateway in its own protocol — `/run` for a model
* from the gateway's catalog, the provider-native path otherwise — and the
* gateway calls the vendor with the key it holds, so no
* provider key, and no vendor wire format, lives in your Worker. The model's
* metadata (`api`, `compat`, `thinkingLevelMap`, cost, limits) is whatever its
* own registry says, and pi-ai's converters and stream processors do the rest.
*
* @module
*/
/** The prefixes a bare vendor id is most often written with, for the error. */
const VENDOR_HINTS = {
	anthropic: "anthropic",
	google: "google",
	openai: "openai"
};
/**
* A string that is neither a Workers AI id nor a `<slug>/<id>` this provider
* can resolve. There is no guessing here by design: a model we cannot name
* exactly is the caller's to construct.
*/
function unroutableString(specifier) {
	const hint = VENDOR_HINTS[specifier.split("/")[0] ?? ""];
	return /* @__PURE__ */ new TypeError(`ai("${specifier}") is not a model this provider knows. Workers AI ids start with "@cf/", AI Gateway dynamic routes with "dynamic/". A third-party model comes from its own pi-ai registry: ai(getBuiltinModel(${hint === void 0 ? "\"<provider>\"" : `"${hint}"`}, "<model id>")) from "@earendil-works/pi-ai/providers/all", or any Model object you built yourself.`);
}
function buildModel(target, options) {
	for (const leg of options?.fallback ?? []) {
		if (typeof leg !== "string" || isRunPathLanguageModelId(leg)) continue;
		if (!leg.includes("/")) throw unroutableString(leg);
		buildGatewayModel(leg, void 0);
	}
	if (typeof target !== "string") return withOptions(target, options);
	if (isRunPathLanguageModelId(target)) return buildWorkersAIModel(target, options);
	if (!target.includes("/")) throw unroutableString(target);
	return buildGatewayModel(target, options);
}
/**
* Creates a pi-ai provider over Cloudflare's AI.
*
* Workers AI and catalog models go through `env.AI.run`, other model objects
* through the binding's `fetch` 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 may be given flat
* (`{ binding, id: "prod", cacheTtl: 60 }`) or nested under `gateway`;
* per-model options and per-call pi-ai options (`headers`, `metadata`,
* `sessionId`) override them in that order.
*
* ```ts
* const ai = createAI({ binding: env.AI, id: "prod" });
* ai("@cf/zai-org/glm-5.3-flash");
* ai(anthropicModel, { fallback: ["@cf/zai-org/glm-5.3-flash"] });
* new Agent({ streamFn: ai.streamFn, initialState: { model: ai(openaiModel) } });
* ```
*/
function createAI(settings) {
	const transport = createTransport(settings);
	const streams = createStreams({
		providerGateway: settings,
		transport
	});
	const model = ((target, options) => buildModel(target, options));
	const provider = createProvider({
		api: streams,
		auth: { apiKey: {
			name: "Cloudflare (the AI binding)",
			resolve: async () => ({ auth: {} })
		} },
		id: CLOUDFLARE_PROVIDER_ID,
		models: [...workersAIModelIds().map((id) => buildWorkersAIModel(id, void 0)), ...gatewayModelIds().map((id) => buildGatewayModel(id, void 0))],
		name: "Cloudflare"
	});
	const stream = (target, context, options) => streams.stream(target, normalizeContext(context), options);
	const streamSimple = (target, context, options) => streams.streamSimple(target, normalizeContext(context), options);
	return Object.assign(model, {
		complete: (target, context, options) => stream(target, context, options).result(),
		completeSimple: (target, context, options) => streamSimple(target, context, options).result(),
		generateImages: (target, context, options) => generateImages({
			providerGateway: settings,
			transport
		}, target, context, options),
		images: (modelId, options) => buildImagesModel(modelId, options),
		model,
		provider,
		stream,
		streamFn: streamSimple,
		streamSimple
	});
}
//#endregion
export { CLOUDFLARE_AI_API, CLOUDFLARE_AI_IMAGES_API, CLOUDFLARE_DIAGNOSTIC, CLOUDFLARE_ERROR_DIAGNOSTIC, CLOUDFLARE_PROVIDER_ID, CloudflareAIError, FALLBACK_DIAGNOSTIC, GATEWAY_PROVIDER_NAMES, createAI };

//# sourceMappingURL=index.js.map