· ai-tools / vercel-ai-sdk / typescript
How to use the Vercel AI SDK — streaming, tools, and agents
AI SDK 6 gives you a single API across 20+ providers, typed streaming, and a ToolLoopAgent class for multi-step agentic loops. Here is how to use it.
By Ethan
1,303 words · 7 min read
The Vercel AI SDK is the fastest path from zero to a streaming AI feature in TypeScript. It unifies 20+ model providers behind one API, replaces manual SSE handling with a single streamText call, and ships end-to-end type safety through tool calls and agent loops. Use it unless you’re deploying a single-provider app and want the smallest possible bundle.
Who this is for
TypeScript developers adding AI to a Node.js script or Next.js app, and not wanting to hand-roll streaming or tool-call plumbing. If you need bleeding-edge features from a specific provider the day they ship, stick to that provider’s native SDK.
Vercel AI SDK project setup
The core package is ai. Add your provider separately.
Node.js:
mkdir my-ai-app && cd my-ai-app
pnpm init
pnpm add ai zod dotenv
pnpm add -D @types/node tsx typescript
Next.js App Router:
pnpm create next-app@latest my-ai-app
cd my-ai-app
pnpm add ai @ai-sdk/react zod
Provider packages are separate installs — for example, @ai-sdk/anthropic, @ai-sdk/openai, or @ai-sdk/google. This keeps your bundle lean: you only ship the provider you actually use.
Set your API key in .env:
ANTHROPIC_API_KEY=sk-ant-...
If you use Cursor as your editor, AI completions fire inside route handlers and tool schemas as you type — useful when the AI SDK’s type signatures are new to you.
Streaming text
Node.js
import { streamText } from 'ai';
import { anthropic } from '@ai-sdk/anthropic';
const result = streamText({
model: anthropic('claude-sonnet-4-5'),
prompt: 'Explain async/await in 3 sentences.',
});
for await (const textPart of result.textStream) {
process.stdout.write(textPart);
}
streamText returns a result object immediately. The actual text arrives on result.textStream as an async iterable. No callbacks, no manual buffer management.
Failure mode: if ANTHROPIC_API_KEY is unset, the call throws at runtime — not at import time. Set the env var before running.
Next.js App Router route
// app/api/chat/route.ts
import { streamText, UIMessage, convertToModelMessages } from 'ai';
import { anthropic } from '@ai-sdk/anthropic';
export async function POST(req: Request) {
const { messages }: { messages: UIMessage[] } = await req.json();
const result = streamText({
model: anthropic('claude-sonnet-4-5'),
messages: await convertToModelMessages(messages),
});
return result.toUIMessageStreamResponse();
}
UIMessage is the v5+ type for messages coming from the React hook. convertToModelMessages maps them to what the model expects. If you see Message in old examples, that’s the deprecated v4 shape.
React client with useChat
// app/page.tsx
'use client';
import { useChat } from '@ai-sdk/react';
export default function Chat() {
const { messages, input, handleInputChange, handleSubmit } = useChat();
return (
<div>
{messages.map(message => (
<div key={message.id}>
{message.role === 'user' ? 'User: ' : 'AI: '}
{message.parts
.filter(part => part.type === 'text')
.map((part, i) => (
<span key={i}>{part.text}</span>
))}
</div>
))}
<form onSubmit={handleSubmit}>
<input value={input} onChange={handleInputChange} />
<button type="submit">Send</button>
</form>
</div>
);
}
useChat handles the message state, the HTTP POST to /api/chat, and the streaming response — all three. The only thing you write is the markup.
Failure mode: useChat defaults to POST /api/chat. If your route is at a different path, pass api as an option: useChat({ api: '/api/my-chat' }).
Tool calls
Tools let the model take actions — fetch data, run calculations, call external APIs — before returning its final answer. You define the schema; the model decides when to call.
import { generateText, tool, stepCountIs } from 'ai';
import { anthropic } from '@ai-sdk/anthropic';
import { z } from 'zod';
const { text, steps } = await generateText({
model: anthropic('claude-sonnet-4-5'),
tools: {
getWeather: tool({
description: 'Get current weather for a city.',
inputSchema: z.object({ location: z.string() }),
execute: async ({ location }) => ({
location,
temperature: 68,
condition: 'sunny',
}),
}),
},
stopWhen: stepCountIs(5),
prompt: 'What should I wear today in NYC? Check the weather first.',
});
console.log(text);
console.log(`Completed in ${steps.length} steps`);
Key API notes for v5/v6 — these break silently on the wrong version:
| Use this | Not this |
|---|---|
inputSchema: | parameters: |
stopWhen: stepCountIs(N) | maxSteps: N |
output: on tool result | result: |
steps gives you the full trace: what the model called, what returned, and in what order. Useful for debugging.
Failure mode: if execute throws, the SDK surfaces the error as a tool-call failure message. The model usually recovers by trying a different approach, but wrap execute in a try/catch and return a structured error object if you want predictable fallback behavior.
Multi-step agents
For tasks that require multiple tool calls in a loop — research, code generation, data pipeline work — ToolLoopAgent gives you a reusable agent you define once.
import { ToolLoopAgent, tool, stepCountIs } from 'ai';
import { anthropic } from '@ai-sdk/anthropic';
import { z } from 'zod';
const agent = new ToolLoopAgent({
model: anthropic('claude-sonnet-4-5'),
instructions: 'You are a research assistant. Be concise.',
tools: {
search: tool({
description: 'Search for information on a topic.',
inputSchema: z.object({ query: z.string() }),
execute: async ({ query }) => ({
results: [`Fact about "${query}"`],
}),
}),
},
stopWhen: stepCountIs(20),
});
// One-shot generation
const result = await agent.generate({
prompt: 'Compare RSC vs Client Components in React.',
});
console.log(result.text);
// Streaming
const stream = agent.stream({
prompt: 'Explain the Node.js event loop.',
});
for await (const chunk of stream.textStream) {
process.stdout.write(chunk);
}
ToolLoopAgent is new in AI SDK 6. The loop runs until stopWhen fires or the model signals it’s done. You get .generate() for a single response and .stream() when you want to pipe output as it arrives.
Stop conditions
Three built-in options:
| Condition | Use when |
|---|---|
stepCountIs(N) | You want a hard cap on LLM calls |
hasToolCall('toolName') | Stop once a specific action fires |
isLoopFinished() | Let the model run to natural completion |
Default is stepCountIs(20). Override it if your task is simple (set lower to save tokens) or open-ended (set higher).
Human-in-the-loop
Mark any tool with needsApproval: true to pause the loop before that call executes:
import { ToolLoopAgent, tool, stepCountIs } from 'ai';
import { anthropic } from '@ai-sdk/anthropic';
import { z } from 'zod';
const agent = new ToolLoopAgent({
model: anthropic('claude-sonnet-4-5'),
instructions: 'You are a deployment assistant.',
tools: {
deploy: tool({
description: 'Deploy the app to production.',
inputSchema: z.object({ env: z.string() }),
needsApproval: true,
execute: async ({ env }) => ({ deployed: true, env }),
}),
},
stopWhen: stepCountIs(10),
});
The agent pauses at the approval gate and surfaces the pending tool call for your UI to confirm. This is new in AI SDK 6.
For a complete production walkthrough covering memory, tool chaining, and observability, see how to build an AI agent in TypeScript.
Choosing a provider
The model string is the only thing that changes between providers:
| Provider | Install | Example model |
|---|---|---|
| Anthropic | @ai-sdk/anthropic | anthropic('claude-sonnet-4-5') |
| OpenAI | @ai-sdk/openai | openai('gpt-4o') |
@ai-sdk/google | google('gemini-2.0-flash') | |
| Mistral | @ai-sdk/mistral | mistral('mistral-large-latest') |
| Groq | @ai-sdk/groq | groq('llama-3.3-70b-versatile') |
| Vercel AI Gateway | built-in | gateway('anthropic/claude-sonnet-4-5') |
Swapping providers is a one-line change. If you’re deploying on Vercel, the AI Gateway adds caching, rate-limit management, and a single billing surface across providers. For a full breakdown of pricing and performance at scale, see our Vercel platform review.
Debugging with DevTools
AI SDK 6 ships @ai-sdk/devtools. After installing, a local inspector at localhost:4983 shows every LLM call in the session: prompt, tokens, tool invocations, latency. Run it during development to catch prompt issues and runaway tool loops before they cost you.
pnpm add -D @ai-sdk/devtools
Import it at the top of your dev entry point — it auto-registers and does nothing in production builds.