vercelGatewayText calls many providers through one API key and one URL. You pick the provider per request, and you can set a fallback order.
npm i @tanstack/ai-vercel-gatewaypnpm add @tanstack/ai-vercel-gatewayyarn add @tanstack/ai-vercel-gatewaybun add @tanstack/ai-vercel-gatewayPut the key in the environment of your server:
AI_GATEWAY_API_KEY=vck_...The adapters read AI_GATEWAY_API_KEY. If it is not set, they read VERCEL_OIDC_TOKEN. To pass the key in code, use the create* factory of the same adapter:
import { createVercelGatewayText } from "@tanstack/ai-vercel-gateway";
const adapter = createVercelGatewayText(
"anthropic/claude-sonnet-5.5",
process.env.AI_GATEWAY_API_KEY!,
);Model ids use the creator/model form:
import {
chat,
chatParamsFromRequest,
toServerSentEventsResponse,
} from "@tanstack/ai";
import { vercelGatewayText } from "@tanstack/ai-vercel-gateway";
export async function POST(request: Request) {
const { messages, threadId, runId } = await chatParamsFromRequest(request);
const stream = chat({
adapter: vercelGatewayText("anthropic/claude-sonnet-5.5"),
messages,
threadId,
runId,
});
return toServerSentEventsResponse(stream);
}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 through the gateway. To change the provider, change the model id, for example to openai/gpt-6.1-sol.
Put the routing options in modelOptions.gateway:
import { chat } from "@tanstack/ai";
import { vercelGatewayText } from "@tanstack/ai-vercel-gateway";
const stream = chat({
adapter: vercelGatewayText("anthropic/claude-sonnet-5.5"),
messages: [{ role: "user", content: "Hello" }],
threadId: "thread-1",
modelOptions: {
gateway: {
order: ["anthropic", "openai"],
models: ["anthropic/claude-sonnet-5.5", "openai/gpt-6.1-sol"],
sort: "cost",
},
},
});order and only take catalog provider ids, such as "anthropic". models takes catalog chat model ids. See How routing goes out.
Read usage.cost on the last RUN_FINISHED event. The gateway reports this cost. TanStack AI does not calculate it from token counts.
onFinish in middleware gets the same usage. A cost of 0 is present. If the gateway sends no cost, usage.cost is absent.
import { chat, type RunFinishedEvent, type StreamChunk } from "@tanstack/ai";
import { vercelGatewayText } from "@tanstack/ai-vercel-gateway";
function isRunFinished(chunk: StreamChunk): chunk is RunFinishedEvent {
return "finishReason" in chunk;
}
for await (const chunk of chat({
adapter: vercelGatewayText("anthropic/claude-sonnet-5.5"),
messages: [{ role: "user", content: "Say OK." }],
threadId: "thread-1",
})) {
if (isRunFinished(chunk)) {
console.log(chunk.usage?.cost);
}
}import { embed } from "@tanstack/ai";
import { vercelGatewayEmbedding } from "@tanstack/ai-vercel-gateway";
const result = await embed({
adapter: vercelGatewayEmbedding("openai/text-embedding-3-small"),
input: "a red guitar",
});
console.log(result.embeddings[0]?.vector);See the Embeddings guide.
The image adapter makes images from text. It calls POST /v1/images/generations. It does not edit images.
import { generateImage } from "@tanstack/ai";
import { vercelGatewayImage } from "@tanstack/ai-vercel-gateway";
const result = await generateImage({
adapter: vercelGatewayImage("openai/gpt-image-2.5-flare"),
prompt: "a red guitar",
threadId: "thread-1",
});See Image Generation.
import { summarize } from "@tanstack/ai";
import { vercelGatewaySummarize } from "@tanstack/ai-vercel-gateway";
const result = await summarize({
adapter: vercelGatewaySummarize("anthropic/claude-sonnet-5.5"),
text: "The Fender Stratocaster is a versatile electric guitar.",
threadId: "thread-1",
stream: false,
});Sometimes you need an answer that your code can branch on, not chat text: a queue name, an urgency level, or a yes or no. Use vercelGatewayDecider with decide() to ask Jev typed questions:
import { decide, choice, score, boolean } from "@tanstack/ai";
import { vercelGatewayDecider } from "@tanstack/ai-vercel-gateway";
const ticket = {
subject: "Charged twice for the same invoice",
body: "Please refund the extra payment.",
};
const result = await decide({
adapter: vercelGatewayDecider("typesafe-ai/jev"),
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?",
}),
},
});
console.log(result.queue.value); // "billing"
console.log(result.urgency.value); // "medium"
console.log(result.refund.value); // trueSee the Evaluate guide for the question helpers, the result shape, abort, and middleware.
When the Gateway sends an error response, the thrown error has the HTTP status code in status. A rate limit (429) needs a retry, but a bad key (401) does not:
import { decide, boolean } from "@tanstack/ai";
import { vercelGatewayDecider } from "@tanstack/ai-vercel-gateway";
try {
await decide({
adapter: vercelGatewayDecider("typesafe-ai/jev"),
state: { body: "Please refund the extra payment." },
questions: {
refund: boolean({ instructions: "Is the customer asking for a refund?" }),
},
});
} catch (error) {
if (error instanceof Error && "status" in error && error.status === 429) {
// Rate limited. Wait, then call decide() again.
}
}This package does not do video, speech, transcription, or rerank. Use a dedicated adapter for these.
Each chat model has its own modelOptions keys and input types from the catalog. For example:
Set reasoning on chat(). The types take the levels of each model:
import { chat } from "@tanstack/ai";
import { vercelGatewayText } from "@tanstack/ai-vercel-gateway";
const stream = chat({
adapter: vercelGatewayText("anthropic/claude-opus-5", { api: "chat" }),
messages: [{ role: "user", content: "Plan a database migration." }],
threadId: "thread-1",
reasoning: { level: "high", budgetTokens: 8000 },
});What goes to the gateway:
See Reasoning.
The catalog is a closed list from GET /v1/models. A daily CI job updates it. The package exports the lists:
| Constant | Holds |
|---|---|
| VERCEL_GATEWAY_CHAT_MODELS | Chat models, for example anthropic/claude-sonnet-5.5 and openai/gpt-6.1-sol |
| VERCEL_GATEWAY_EMBEDDING_MODELS | Embedding models, for example openai/text-embedding-3-small and voyage/voyage-4 |
| VERCEL_GATEWAY_IMAGE_MODELS | Image models, for example openai/gpt-image-2.5-flare and bfl/flux-3-image |
| VERCEL_GATEWAY_PROVIDERS | Provider ids for order and only |
The evaluate model is typesafe-ai/jev.
The default text adapter uses the OpenAI Responses API at https://ai-gateway.vercel.sh/v1. If a model must use Chat Completions, pass { api: "chat" }:
import { chat } from "@tanstack/ai";
import { vercelGatewayText } from "@tanstack/ai-vercel-gateway";
const stream = chat({
adapter: vercelGatewayText("openai/gpt-5.5", { api: "chat" }),
messages: [{ role: "user", content: "Hello" }],
threadId: "thread-1",
});Need a header on each request, or a log of each model call? The chat adapters on this page support wrapFetch.
The adapter sends modelOptions.gateway as providerOptions.gateway. Do not put gateway at the top level of the request body.
vercelGatewayByok from @tanstack/ai-vercel-gateway/byok declares the gateway for the BYOK store, with AI_GATEWAY_API_KEY and VERCEL_OIDC_TOKEN as its env vars. See Bring Your Own Key.
Each short factory reads AI_GATEWAY_API_KEY, then VERCEL_OIDC_TOKEN. Each create* factory takes an explicit API key. Both take model as the first argument.
| Factories | Creates |
|---|---|
| vercelGatewayText / createVercelGatewayText | Chat adapter (Responses by default, Chat Completions with api: "chat") |
| vercelGatewayResponsesText / createVercelGatewayResponsesText | Chat adapter for the Responses API |
| vercelGatewaySummarize / createVercelGatewaySummarize | Summarization adapter |
| vercelGatewayEmbedding / createVercelGatewayEmbedding | Embedding adapter |
| vercelGatewayImage / createVercelGatewayImage | Image adapter |
| vercelGatewayDecider / createVercelGatewayDecider | Evaluate adapter |