- OpenAIUpdated
- OpenAI: AdvancedNew
- AnthropicUpdated
- Anthropic: AdvancedNew
- Google GeminiUpdated
- Google Gemini: AdvancedNew
- Google Vertex AIUpdated
- OllamaUpdated
- Grok (xAI)Updated
- GroqUpdated
- MistralUpdated
- CohereUpdated
- TypeSafeUpdated
- OllayaUpdated
- ElevenLabsUpdated
- fal.aiUpdated
- ReactorUpdated
- World LabsUpdated
- OpenRouter AdapterUpdated
- OpenRouter: AdvancedNew
- Perplexity SearchUpdated
- Vercel AI GatewayUpdated
- Lovable AI GatewayUpdated
- OpenAI-CompatibleUpdated
- OpenAI-Compatible: AdvancedNew
- LLM GatewayUpdated
- CloudflareUpdated
- Claude CodeUpdated
- CodexUpdated
- OpenCodeUpdated
- Grok BuildUpdated
- ACP-CompatibleUpdated
- Amazon BedrockUpdated
- BytePlusUpdated
- BytePlus: AdvancedNew
Cloudflare
The Cloudflare adapter calls Workers AI, and it can put AI Gateway in front of another provider.
- You have a Cloudflare Worker and want an AI route with no API keys to manage.
- You have a server somewhere else and want to call Workers AI models.
- You use OpenAI or Anthropic and want AI Gateway caching and logs in front of them.
1. Install#
npm i @tanstack/ai-cloudflarepnpm add @tanstack/ai-cloudflareyarn add @tanstack/ai-cloudflarebun add @tanstack/ai-cloudflare2. Connect to Cloudflare#
Inside a Worker, use the env.AI binding. You need no token and no account id. Add the binding to wrangler.jsonc:
{
"ai": { "binding": "AI" },
}On any other server, the adapter calls the Cloudflare REST API. Put your account id and an API token with Workers AI access in the environment of your server:
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.
3. Stream a chat from the server#
From a Worker, pass env.AI as the binding:
import {
chat,
chatParamsFromRequest,
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, threadId, runId } = await chatParamsFromRequest(request);
const stream = chat({
adapter: createCloudflareText("@cf/zai-org/glm-5.3-flash", {
binding: env.AI,
}),
messages,
threadId,
runId,
});
return toServerSentEventsResponse(stream);
},
};Run wrangler dev to start the route.
From any other server, cloudflareText reads CLOUDFLARE_ACCOUNT_ID and CLOUDFLARE_API_TOKEN:
import {
chat,
chatParamsFromRequest,
toServerSentEventsResponse,
} from "@tanstack/ai";
import { cloudflareText } from "@tanstack/ai-cloudflare";
export async function POST(request: Request) {
const { messages, threadId, runId } = await chatParamsFromRequest(request);
const stream = chat({
adapter: cloudflareText("@cf/zai-org/glm-5.3-flash"),
messages,
threadId,
runId,
});
return toServerSentEventsResponse(stream);
}To pass the values in code, use createCloudflareText:
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!,
});4. Render the chat#
The client is the same for every provider. Point useChat at your route:
import { useState } from "react";
import { useChat, fetchServerSentEvents } from "@tanstack/ai-react";
export function Chat() {
const [input, setInput] = useState("");
const { messages, sendMessage } = useChat({
connection: fetchServerSentEvents("/api/chat"),
threadId: "support-chat",
});
return (
<form
onSubmit={(event) => {
event.preventDefault();
sendMessage(input);
setInput("");
}}
>
{messages.map((message) => (
<p key={message.id}>
{message.parts
.map((part) => (part.type === "text" ? part.content : ""))
.join("")}
</p>
))}
<input value={input} onChange={(event) => setInput(event.target.value)} />
</form>
);
}For Vue, Solid, Svelte, Angular, and the other frameworks, see Quick Start.
You now have a streaming chat on Workers AI. Tools and Structured Outputs work the same as with every adapter.
Route through AI Gateway#
AI Gateway gives you caching, logs, rate limits, and one bill for all providers. To send requests through it, add gateway to any config. Use "default" for the gateway that Cloudflare makes 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. Cloudflare bills them:
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" },
});Keep your existing provider adapter#
Do you use @tanstack/ai-openai with your own OpenAI key? cloudflareGateway() returns the baseURL and the 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-6.1-sol", process.env.OPENAI_API_KEY!, {
baseURL: gateway.baseURL,
defaultHeaders: gateway.headers,
});- The first argument is the provider slug from your gateway dashboard, for example openai, anthropic, or groq.
- If the gateway has authentication on, also pass cfApiKey. 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. Then the Cloudflare, OpenAI, Anthropic, and Groq models in its picker all go through your gateway.
Claude and GPT from a Worker#
Inside a Worker, the AI Gateway anthropic/... and openai/... models use the API of their own provider, not Chat Completions. Keep the provider adapter, and give it cloudflareBindingFetch as its fetch. The binding signs the requests, so the Worker needs no provider key:
import {
chat,
chatParamsFromRequest,
toServerSentEventsResponse,
} from "@tanstack/ai";
import { createAnthropicChat } from "@tanstack/ai-anthropic";
import { cloudflareBindingFetch } 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, threadId, runId } = await chatParamsFromRequest(request);
// The SDK needs a key value. The binding does not use it.
const adapter = createAnthropicChat("claude-opus-5-5", "cloudflare-binding", {
fetch: cloudflareBindingFetch({
binding: env.AI,
vendor: "anthropic",
gateway: { id: "default" },
}),
});
const stream = chat({ adapter, messages, threadId, runId, reasoning: "high" });
return toServerSentEventsResponse(stream);
},
};- vendor: "anthropic" sends Anthropic Messages requests to anthropic/<model>. Use it with createAnthropicChat.
- vendor: "openai" sends OpenAI Responses requests to openai/<model>. Use it with createOpenaiChat.
- The adapter keeps all of its options: chat({ reasoning }), tools, cache_control for prompt caching, and Anthropic betas (sent as the anthropic-beta header).
- For @cf/... models and other gateway vendors, keep createCloudflareText with binding: env.AI.
Bring your own key#
Two different things have this name. Both work.
Cloudflare credentials of a user, from the browser#
A user brings two values: the API token, and the account that it belongs to.
- cloudflareByok and cloudflareAccountByok are two entries in the TanStack BYOK store. Each one has its own env fallback.
- cloudflareByok declares the account id as a companion. Register both, and a send for cloudflare carries both headers.
- Import them from @tanstack/ai-cloudflare/byok, not from the main entry.
On the client:
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,
chatParamsFromRequest,
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, threadId, runId } = await chatParamsFromRequest(request);
const stream = chat({
adapter: createCloudflareText("@cf/zai-org/glm-5.3-flash", {
accountId,
apiKey,
}),
messages,
threadId,
runId,
});
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, with the alias default. Then a provider/model id with gateway: { id } uses that key, not Unified Billing. You change no code.
Evaluate#
Sometimes you need an answer that your code can branch on, not chat text. Use cloudflareDecider("typesafe/jev") with decide(). To send the run through AI Gateway, add gateway: { id: "default" } to the adapter config.
From a Worker, pass the binding:
import { decide, choice, score, boolean } from "@tanstack/ai";
import { createCloudflareDecider } from "@tanstack/ai-cloudflare";
import type { Ai } from "@cloudflare/workers-types";
interface Env {
AI: Ai;
}
const ticket = {
subject: "Charged twice for the same invoice",
body: "Please refund the extra payment.",
};
export async function evaluateTicket(env: Env) {
const result = await decide({
adapter: createCloudflareDecider("typesafe/jev", {
binding: env.AI,
gateway: { id: "default" },
}),
state: ticket,
questions: {
queue: choice({
instructions: "Which team should handle this ticket?",
options: {
billing: "Payments, invoices, refunds",
tech: "Bugs, outages, integrations",
sales: "Pricing, upgrades, new accounts",
},
}),
urgency: score({
instructions: "How urgent is this ticket?",
levels: ["low", "medium", "high"],
}),
refund: boolean({
instructions: "Is the customer asking for a refund?",
}),
},
});
return result;
}From any other server, cloudflareDecider reads CLOUDFLARE_ACCOUNT_ID and CLOUDFLARE_API_TOKEN:
import { decide, boolean } from "@tanstack/ai";
import { cloudflareDecider } from "@tanstack/ai-cloudflare";
const result = await decide({
adapter: cloudflareDecider("typesafe/jev"),
state: "Please refund the extra payment.",
questions: {
refund: boolean({
instructions: "Is the customer asking for a refund?",
}),
},
});
console.log(result.refund.value);See the Evaluate guide for the question helpers, the result shape, abort, and middleware.
Summarize, embed, and media#
Every activity takes the same config as chat:
- Inside a Worker: { binding }.
- On any other server: { accountId, apiKey }.
- Both take an optional gateway.
The samples below use the factories that read the environment.
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...",
threadId: "thread-1",
stream: false,
});
console.log(result.summary);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"],
});
console.log(result.embeddings[0]?.vector);Images#
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",
threadId: "thread-1",
modelOptions: { steps: 4 },
});
console.log(result.images[0]?.b64Json);Text 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.",
threadId: "thread-1",
voice: "luna",
});
console.log(result.audio); // 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,
threadId: "thread-1",
});
console.log(result.text, result.segments, result.words);Pass the audio as a File, a Blob, an ArrayBuffer, or a base64 string. Whisper models take base64, and @cf/deepgram/nova-3 takes the raw bytes. The adapter sends each model the format that it needs.
Models and options#
Models#
These ids work:
- Any id from the Workers AI catalog.
- Any provider/model id from the AI Gateway catalog, when a gateway is set.
Ids in the bundled catalog get editor autocomplete.
Model options#
Sampling options go in modelOptions. The thinking level goes in reasoning:
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." }],
threadId: "thread-1",
modelOptions: {
temperature: 0.3,
max_tokens: 512,
},
reasoning: "low",
});Reasoning#
reasoning goes out as reasoning_effort. off sends null, which turns thinking off. Reasoning models stream their thinking as reasoning_content, which you get as REASONING_* events.
Some models and their levels:
| Model | Levels |
|---|---|
| @cf/zai-org/glm-5.3, @cf/zai-org/glm-5.3-flash | low, high, max |
| @cf/deepseek-ai/deepseek-v4-pro-0813, @cf/deepseek-ai/deepseek-v4-flash-0731 | off, low, high, max |
| @cf/qwen/qwen3.8-27b | off, low, medium, xhigh |
| @cf/openai/gpt-oss-120b, @cf/openai/gpt-oss-20b | low, medium, high |
The types know the levels of each model. A model with no reasoning data takes no reasoning. See Reasoning.
Advanced#
Client options on the REST path#
- The text and summarize adapters also take the OpenAI SDK client options: baseURL, defaultHeaders, fetch, timeout, and maxRetries.
- The other adapters call the native /ai/run endpoint and take only fetch.
Change the HTTP requests#
Need a header on each request, or a log of each model call? The chat adapter supports wrapFetch on the REST path. In binding mode, the wrapper still runs. But the binding reads only the request body, so it drops the URL and the headers that the wrapper sets.
Tools and prompts added during a conversation#
Two kinds of adapter send the full tools and system prompts on every call:
- An adapter with a gateway baseURL from cloudflareGateway().
- An adapter with cloudflareBindingFetch.
This is also true on a model with a mid-conversation channel. So AI Gateway gets the same request as on any other model. To send tools and prompts added during a conversation through the channel, set midConversationChannels: true:
import { createAnthropicChat } from "@tanstack/ai-anthropic";
import { cloudflareBindingFetch } from "@tanstack/ai-cloudflare";
import type { Ai } from "@cloudflare/workers-types";
export function gatewayClaude(env: { AI: Ai }) {
return createAnthropicChat("claude-opus-5-5", "cloudflare-binding", {
fetch: cloudflareBindingFetch({
binding: env.AI,
vendor: "anthropic",
gateway: { id: "default" },
}),
midConversationChannels: true,
});
}