Vercel AI SDK + AI Gateway Integration Guide
What is the Vercel AI SDK?
Typed wrapper + streaming primitives + AI Gateway routing + React hooks.
Built around the `LanguageModelV2` interface so providers are interchangeable. `streamText({ model, messages, tools })` returns a typed stream you can pipe into a server response or pass to `useChat`. Multi-step tool calls auto-loop until the model stops calling tools. `generateObject({ schema })` uses provider-native structured outputs where available, falls back to retry-on-Zod-validate elsewhere. AI Gateway sits in front of providers for per-call routing, fallback, and aggregated billing — great for cost optimisation. For codeAmani, the SDK is the abstraction layer; AI Gateway is the cost lever.
Five AI SDK primitives
Same verbs across every provider. Swap models with one string.
██╗ ██╗███████╗██████╗ ██████╗███████╗██╗ █████╗ ██╗ ███████╗██████╗ ██╗ ██╗
██║ ██║██╔════╝██╔══██╗██╔════╝██╔════╝██║ ██╔══██╗██║ ██╔════╝██╔══██╗██║ ██╔╝
██║ ██║█████╗ ██████╔╝██║ █████╗ ██║ ███████║██║ ███████╗██║ ██║█████╔╝
╚██╗ ██╔╝██╔══╝ ██╔══██╗██║ ██╔══╝ ██║ ██╔══██║██║ ╚════██║██║ ██║██╔═██╗
╚████╔╝ ███████╗██║ ██║╚██████╗███████╗███████╗ ██║ ██║██║ ███████║██████╔╝██║ ██╗
╚═══╝ ╚══════╝╚═╝ ╚═╝ ╚═════╝╚══════╝╚══════╝ ╚═╝ ╚═╝╚═╝ ╚══════╝╚═════╝ ╚═╝ ╚═╝Vercel AI SDK + AI Gateway Integration Guide
Focus: One TypeScript interface (
generateText/streamText/generateObject) over every provider codeAmani uses — Anthropic, OpenAI, Gemini, DeepSeek, HuggingFace, ElevenLabs — with AI Gateway adding a single API key, automatic fallback chains, and built-in cost/latency observability. This replaces the hand-rolled routing inAI_WORKFLOWS.md.
Overview
Here is the big picture — one interface, many providers, all routed through a single Gateway:
Two complementary pieces:
- AI SDK (
ai+@ai-sdk/*provider packages) — a unified, framework-agnostic API. Swap providers by changing onemodelvalue; the rest of the code is identical. - AI Gateway — a Vercel-hosted unified endpoint. When you pass
modelas a string inprovider/modelform (e.g."anthropic/claude-sonnet-4-20250514"), the SDK routes through the Gateway using oneAI_GATEWAY_API_KEY— no per-provider key wrangling. The Gateway adds cost/latency/usage dashboards, auth, and model fallbacks.
Net effect for codeAmani: the AI routing policy (Claude primary, OpenAI for structured output, HF/others as fallbacks) becomes config, not app code.
Official Documentation
| Resource | URL |
|---|---|
| AI SDK intro | https://ai-sdk.dev/docs/introduction |
| Getting started (Node) | https://ai-sdk.dev/docs/getting-started/nodejs |
| Providers list | https://ai-sdk.dev/providers/ai-sdk-providers |
| AI Gateway docs | https://vercel.com/docs/ai-gateway |
Gateway auth (AI_GATEWAY_API_KEY) | https://vercel.com/docs/ai-gateway/authentication |
| Gateway provider options (fallbacks) | https://vercel.com/docs/ai-gateway/models-and-providers/provider-options |
1. Install
npm install ai # core
# Optional explicit providers (only if NOT using the string/Gateway form):
npm install @ai-sdk/anthropic @ai-sdk/openai @ai-sdk/google @ai-sdk/deepseek2. Credentials
# Gateway path (recommended) — one key for all providers:
AI_GATEWAY_API_KEY=...
# OR direct-provider path — one key each:
ANTHROPIC_API_KEY=...
OPENAI_API_KEY=...
GOOGLE_GENERATIVE_AI_API_KEY=...On Vercel, the Gateway can also authenticate via the deployment's OIDC token (
VERCEL_OIDC_TOKEN) with no key at all. Keep every key server-side.
3. Generate text (Gateway via model string)
import { generateText } from "ai";
const { text } = await generateText({
model: "anthropic/claude-sonnet-4-20250514", // string -> routed through AI Gateway
prompt: "Explain M-Pesa STK Push in one sentence.",
});Switching providers is a one-line change — "openai/gpt-5", "google/gemini-2.5-flash",
"deepseek/deepseek-chat". The surrounding code never changes.
4. Stream + structured output
import { streamText, generateObject } from "ai";
import { z } from "zod";
const result = streamText({ model: "google/gemini-2.5-flash", prompt });
for await (const chunk of result.textStream) process.stdout.write(chunk);
const { object } = await generateObject({
model: "openai/gpt-5",
schema: z.object({ amount: z.number(), phone: z.string() }),
prompt: "Extract the payment amount and phone from: 'Send 500 to 0712345678'",
});4a. useChat — streaming chat UI (AI SDK v5)
For a React chat UI, the useChat hook (from @ai-sdk/react) handles message state,
streaming, and input wiring; the matching route handler runs streamText on the server and
returns result.toUIMessageStreamResponse(). This targets AI SDK v5 — the message shape
is UIMessage (rendered via message.parts, not a flat content string), and the client
posts through a DefaultChatTransport. Keep the API route server-side so your
AI_GATEWAY_API_KEY never reaches the browser.
// app/(public)/chat/page.tsx
"use client";
import { useChat } from "@ai-sdk/react";
import { DefaultChatTransport } from "ai";
import { useState } from "react";
export default function Chat() {
const { messages, sendMessage } = useChat({
transport: new DefaultChatTransport({ api: "/api/chat" }),
});
const [input, setInput] = useState("");
return (
<div>
{messages.map((m) => (
<div key={m.id}>
<strong>{m.role}: </strong>
{m.parts.map((part, i) =>
part.type === "text" ? <span key={i}>{part.text}</span> : null,
)}
</div>
))}
<form
onSubmit={(e) => {
e.preventDefault();
if (!input.trim()) return;
sendMessage({ text: input });
setInput("");
}}
>
<input
value={input}
placeholder="Uliza chochote..."
onChange={(e) => setInput(e.target.value)}
/>
</form>
</div>
);
}// app/api/chat/route.ts
import { streamText, convertToModelMessages, type UIMessage } from "ai";
export const maxDuration = 30; // allow streaming responses up to 30s
export async function POST(req: Request) {
const { messages }: { messages: UIMessage[] } = await req.json();
const result = streamText({
model: "anthropic/claude-sonnet-4-20250514", // string -> AI Gateway
system: "You are a helpful assistant for Kenyan SMEs.",
messages: await convertToModelMessages(messages),
});
return result.toUIMessageStreamResponse();
}Gotcha: in v5 the helper is toUIMessageStreamResponse(), not the v4 toDataStreamResponse(),
and you must pass await convertToModelMessages(messages) to streamText — handing the raw
UIMessage[] (with parts) straight to the model throws. The client reads message.parts,
so there is no message.content string to render.
5. Fallback chains (the replacement for hand-rolled routing)
You can let the Gateway handle failover automatically — here is the order it walks:
const { text } = await generateText({
model: "anthropic/claude-sonnet-4-20250514", // primary
prompt,
providerOptions: {
gateway: {
models: ["openai/gpt-5", "google/gemini-2.5-flash"], // tried in order if primary fails
// order: ["anthropic", "vertex"], // or pin provider routing order
},
},
});Cost, latency, and tokens-per-model are visible in the AI Gateway dashboard — no custom metrics code needed.
codeAmani notes
- Replaces
AI_WORKFLOWS.mdplumbing: the provider routing + fallback chains currently hand-rolled there map directly toproviderOptions.gateway.models/order. Keep the policy (Claude primary, OpenAI for structured/function calling, HF for open models) but let the Gateway execute it. ElevenLabs (voice) and DeepSeek are reachable through the same interface — see theeleven-labsanddeepseekguides. - Security:
AI_GATEWAY_API_KEY(or per-provider keys) stay in.env.local/ Vercel env; call only from server actions / API routes, never client components. - African market: prefer
streamTextso 2G/3G users see tokens immediately; pick smaller fallback models (e.g.gemini-2.5-flash) to cut cost + latency on spiky SME traffic, and use the Gateway dashboard to watch per-model spend. - Edge-safe: the SDK runs on Vercel Edge — pairs well with the Upstash cache/queue for memoizing expensive completions.