You have a Cloudflare Worker and want an AI route without managing API keys. Or you have a server elsewhere and want to call Workers AI models. Or you already use OpenAI or Anthropic and want AI Gateway caching and logs in front of them. This adapter does all three.
npm i @tanstack/ai-cloudflarepnpm add @tanstack/ai-cloudflareyarn add @tanstack/ai-cloudflarebun add @tanstack/ai-cloudflarePass env.AI as the binding. No token, no account id.
{
"ai": { "binding": "AI" },
}import { chat, toServerSentEventsResponse } from "@tanstack/ai";
import { createCloudflareText } from "@tanstack/ai-cloudflare";
import type { Ai } from "@cloudflare/workers-types";
interface Env {
AI: Ai;
}
export default {
async fetch(request: Request, env: Env) {
const { messages } = await request.json();
const stream = chat({
adapter: createCloudflareText("@cf/zai-org/glm-5.3-flash", {
binding: env.AI,
}),
messages,
});
return toServerSentEventsResponse(stream);
},
};That is a working AI route. Run wrangler dev and post { "messages": [...] } to it.
Outside a Worker, the adapter talks to the Cloudflare REST API. You need your account id and an API token with Workers AI access.
import { chat } from "@tanstack/ai";
import { createCloudflareText } from "@tanstack/ai-cloudflare";
const adapter = createCloudflareText("@cf/zai-org/glm-5.3-flash", {
accountId: process.env.CLOUDFLARE_ACCOUNT_ID!,
apiKey: process.env.CLOUDFLARE_API_TOKEN!,
});
const stream = chat({
adapter,
messages: [{ role: "user", content: "Hello!" }],
});cloudflareText(model) does the same and reads CLOUDFLARE_ACCOUNT_ID and CLOUDFLARE_API_TOKEN from the environment for you.
The text and summarize REST config also accepts the OpenAI SDK client options (baseURL, defaultHeaders, fetch, timeout, maxRetries). The other adapters call the native /ai/run endpoint and take fetch only.
AI Gateway gives you caching, logs, rate limits, and one bill across providers. Add gateway to any config to send requests through it. Use "default" for the gateway Cloudflare creates for your account.
import { createCloudflareText } from "@tanstack/ai-cloudflare";
import type { Ai } from "@cloudflare/workers-types";
interface Env {
AI: Ai;
}
export function makeAdapter(env: Env) {
return createCloudflareText("@cf/zai-org/glm-5.3-flash", {
binding: env.AI,
gateway: { id: "default", cacheTtl: 3600, skipCache: false },
});
}With a gateway you can also run third-party models by their catalog id, billed through Cloudflare:
import { createCloudflareText } from "@tanstack/ai-cloudflare";
const adapter = createCloudflareText("openai/gpt-5.5", {
accountId: process.env.CLOUDFLARE_ACCOUNT_ID!,
apiKey: process.env.CLOUDFLARE_API_TOKEN!,
gateway: { id: "default" },
});Already using @tanstack/ai-openai with your own OpenAI key? cloudflareGateway() returns the baseURL and headers that point that adapter at your gateway. The rest of your code stays the same.
import { createOpenaiChat } from "@tanstack/ai-openai";
import { cloudflareGateway } from "@tanstack/ai-cloudflare";
const gateway = cloudflareGateway("openai", {
accountId: process.env.CLOUDFLARE_ACCOUNT_ID!,
gatewayId: "prod",
cacheTtl: 300,
});
const adapter = createOpenaiChat("gpt-5.5", process.env.OPENAI_API_KEY!, {
baseURL: gateway.baseURL,
defaultHeaders: gateway.headers,
});The first argument is the provider slug from your gateway dashboard (openai, anthropic, groq, and so on). If the gateway has authentication turned on, pass cfApiKey too. That token needs the AI Gateway Run permission.
The ts-react-chat example does this. Set CLOUDFLARE_ACCOUNT_ID, CLOUDFLARE_API_TOKEN, and CLOUDFLARE_AI_GATEWAY_ID in its .env, and the Cloudflare, OpenAI, Anthropic, and Groq models in its picker all go through your gateway.
Two different things go by this name. Both work.
A user's Cloudflare credentials from the browser. A user brings two values: the API token and the account it belongs to. cloudflareByok and cloudflareAccountByok are two entries in the TanStack BYOK store, each with its own env fallback. cloudflareByok declares the account id as a companion, so register both and a send for cloudflare carries both headers. Import them from @tanstack/ai-cloudflare/byok, not from the main entry.
import { defineByok, defaultByokStorage } from "@tanstack/ai-client/byok";
import {
cloudflareAccountByok,
cloudflareByok,
} from "@tanstack/ai-cloudflare/byok";
export const byok = defineByok({
storage: defaultByokStorage(),
providers: [cloudflareByok, cloudflareAccountByok],
});On the relay, read both values in one call:
import { chat, toServerSentEventsResponse } from "@tanstack/ai";
import { createCloudflareText } from "@tanstack/ai-cloudflare";
import {
cloudflareAccountByok,
cloudflareByok,
} from "@tanstack/ai-cloudflare/byok";
import { byokMissing, getByokKeys } from "@tanstack/ai/byok/server";
export async function POST(request: Request) {
const { apiKey, accountId } = getByokKeys(request, {
apiKey: cloudflareByok,
accountId: cloudflareAccountByok,
});
if (!apiKey) return byokMissing(cloudflareByok);
if (!accountId) return byokMissing(cloudflareAccountByok);
const { messages } = await request.json();
const stream = chat({
adapter: createCloudflareText("@cf/zai-org/glm-5.3-flash", {
accountId,
apiKey,
}),
messages,
});
return toServerSentEventsResponse(stream);
}See Bring Your Own Key for the client store and the save dialog.
Your provider keys stored in AI Gateway. Add an OpenAI or Anthropic key under Provider Keys in the gateway dashboard, alias default. From then on, a provider/model id with gateway: { id } uses that key instead of Unified Billing. No code changes.
Any id from the Workers AI catalog works, and so does any provider/model id from the AI Gateway catalog when a gateway is set. Ids in the bundled catalog get editor autocomplete.
Tools and outputSchema work the same as with every other adapter:
import { chat, toServerSentEventsResponse, toolDefinition } from "@tanstack/ai";
import { createCloudflareText } from "@tanstack/ai-cloudflare";
import { z } from "zod";
import type { Ai } from "@cloudflare/workers-types";
interface Env {
AI: Ai;
}
const getWeather = toolDefinition({
name: "get_weather",
description: "Get the current weather",
inputSchema: z.object({ location: z.string() }),
}).server(async ({ location }) => {
return { location, temperature: 21, conditions: "sunny" };
});
export default {
async fetch(request: Request, env: Env) {
const { messages } = await request.json();
const stream = chat({
adapter: createCloudflareText("@cf/zai-org/glm-5.3-flash", {
binding: env.AI,
}),
messages,
tools: [getWeather],
});
return toServerSentEventsResponse(stream);
},
};Sampling and reasoning controls go in modelOptions. Reasoning models stream their thinking as reasoning_content, which shows up as REASONING_* events.
import { chat } from "@tanstack/ai";
import { cloudflareText } from "@tanstack/ai-cloudflare";
const stream = chat({
adapter: cloudflareText("@cf/zai-org/glm-5.3-flash"),
messages: [{ role: "user", content: "Summarize this in one sentence." }],
modelOptions: {
temperature: 0.3,
max_tokens: 512,
reasoning_effort: "low",
chat_template_kwargs: { enable_thinking: false },
},
});Every activity takes the same config as chat: { binding } inside a Worker, { accountId, apiKey } elsewhere, plus an optional gateway. The examples below use the env-reading factories.
Summarize:
import { summarize } from "@tanstack/ai";
import { cloudflareSummarize } from "@tanstack/ai-cloudflare";
const result = await summarize({
adapter: cloudflareSummarize("@cf/zai-org/glm-5.3-flash"),
text: "Long article text...",
stream: false,
});Embeddings:
import { embed } from "@tanstack/ai";
import { cloudflareEmbedding } from "@tanstack/ai-cloudflare";
const result = await embed({
adapter: cloudflareEmbedding("@cf/baai/bge-m3"),
input: ["a story about a llama", "a story about a cloud"],
});
// result.embeddings[0].vectorImages:
import { generateImage } from "@tanstack/ai";
import { cloudflareImage } from "@tanstack/ai-cloudflare";
const result = await generateImage({
adapter: cloudflareImage("@cf/black-forest-labs/flux-1-schnell"),
prompt: "a red bicycle on a beach",
modelOptions: { steps: 4 },
});
// result.images[0].b64JsonText to speech:
import { generateSpeech } from "@tanstack/ai";
import { cloudflareTTS } from "@tanstack/ai-cloudflare";
const result = await generateSpeech({
adapter: cloudflareTTS("@cf/deepgram/aura-2-en"),
text: "Hello from the edge.",
voice: "luna",
});
// result.audio is base64 MP3Transcription:
import { generateTranscription } from "@tanstack/ai";
import { cloudflareTranscription } from "@tanstack/ai-cloudflare";
import { audioBuffer } from "./audio";
const result = await generateTranscription({
adapter: cloudflareTranscription("@cf/openai/whisper-large-v3-turbo"),
audio: audioBuffer,
});
// result.text, result.segments, result.wordsWhisper models take the audio as base64. @cf/deepgram/nova-3 takes the raw bytes. The adapter handles both, so pass a File, Blob, ArrayBuffer, or base64 string.
For the REST path:
CLOUDFLARE_ACCOUNT_ID=your-account-id
CLOUDFLARE_API_TOKEN=your-api-tokenGet the account id from wrangler whoami. Create the token in the Cloudflare dashboard under Workers AI, then Use REST API.