AI Integration · AI Tooling
Vercel AI SDK 7: Streaming, Tool Calls, and Agents in Code
AI SDK 7 replaces maxSteps with stopWhen: isStepCount(), renames system to instructions, and moves structured output into generateText. Working code for each.
Anurag Verma
12 min read · Updated Oct 19, 2026
Sponsored
If you learned the Vercel AI SDK from a 2024 or early 2025 tutorial, most of what you remember has been renamed. maxSteps is gone, system is now instructions, generateObject is deprecated, and useChat no longer owns your input field. This post is the current version of the patterns, with code written against AI SDK 7.
What version of the AI SDK is current?
The ai package is on the 7.x line in October 2026. Vercel announced AI SDK 7 on June 25, 2026. It needs Node.js 22 or later and ships as ESM only. Multi-step tool calls use stopWhen: isStepCount(n) instead of maxSteps, and structured output runs through generateText with Output.object().
The npm registry shows latest at 7.0.143 as of mid October, with 6.x and 5.x still getting maintenance releases under the ai-v6 and ai-v5 tags. So if you’re stuck on an older major for a while, you aren’t abandoned. But every snippet below assumes 7.
npm install ai @ai-sdk/react zod
Two upgrade requirements bite before any code runs. AI SDK 7 drops Node 18 and 20, and it removes CommonJS support, so require('ai') throws. Fix both before you touch the API changes.
The renames that matter most
Here is the short list of changes you’ll actually hit if you’re coming from 4.x, which is what most older tutorials (including the first version of this post) were written against.
| You wrote (4.x) | You write now (7.x) | Changed in |
|---|---|---|
maxSteps: 5 | stopWhen: isStepCount(5) | 5.0 (as stepCountIs), renamed in 7.0 |
parameters: z.object(...) in tool() | inputSchema: z.object(...) | 5.0 |
system: '...' | instructions: '...' | 7.0 (system still works, deprecated) |
generateObject({ schema }) | generateText({ output: Output.object({ schema }) }) | deprecated in 6.0 |
result.toDataStreamResponse() | createUIMessageStreamResponse({ stream: toUIMessageStream({ stream: result.stream }) }) | 7.0 helpers |
import { useChat } from 'ai/react' | import { useChat } from '@ai-sdk/react' | 5.0 |
usage.promptTokens | usage.inputTokens | 5.0 |
onFinish / onStepFinish | onEnd / onStepEnd (old names are aliases) | 7.0 |
The official route is one major at a time. The 6-to-7 hop has a codemod (npx @ai-sdk/codemod v7), and the migration guide is blunt that it doesn’t cover everything. Plan to read the diff.
Model strings and providers
In AI SDK 7 you can pass a plain provider/model string, which routes through Vercel’s AI Gateway. Or you import a provider package and call the model directly.
// Through the AI Gateway: a plain string
const model = 'anthropic/claude-sonnet-5.5';
// Direct to a provider: install @ai-sdk/openai
import { openai } from '@ai-sdk/openai';
const direct = openai('gpt-6-astra');
Both forms plug into the same model option, so swapping providers is still a one-line change at the point where you construct the model. The model IDs above are the ones the official docs use in October 2026. Check the provider’s current list before you ship, because model names change faster than the SDK does.
A few provider-level renames landed in 7.0 too. The Google provider’s createGoogleGenerativeAI is now createGoogle (the google export is unchanged), and xai(modelId) now uses xAI’s Responses API, with xai.chat(modelId) for the old Chat Completions path.
generateText for single-turn work
import { generateText } from 'ai';
const { text, usage } = await generateText({
model: 'anthropic/claude-sonnet-5.5',
instructions: 'You are a concise technical writer.',
prompt: 'Explain database indexing in two sentences.',
});
console.log(text);
console.log(usage.inputTokens, usage.outputTokens, usage.totalTokens);
usage has the same shape across providers: inputTokens, outputTokens, totalTokens, plus inputTokenDetails (cache reads and writes) and outputTokenDetails (reasoning tokens). In 7.0, usage on a multi-step call is the total across every step. If you want the last step only, read finalStep.usage. That one is easy to miss when you’re computing cost per request.
streamText in a Next.js route
For anything a person watches, stream. The route handler converts the UI messages coming from the browser into model messages, then wraps the stream in a UI message response.
// app/api/chat/route.ts
import {
streamText,
convertToModelMessages,
createUIMessageStreamResponse,
toUIMessageStream,
type UIMessage,
} from 'ai';
export async function POST(req: Request) {
const { messages }: { messages: UIMessage[] } = await req.json();
const result = streamText({
model: 'anthropic/claude-sonnet-5.5',
instructions: 'You are a helpful assistant.',
messages: await convertToModelMessages(messages),
});
return createUIMessageStreamResponse({
stream: toUIMessageStream({ stream: result.stream }),
});
}
Two details here are new enough to trip people up. convertToModelMessages is async as of 6.0, so it needs the await. And in 7.0 the methods on the result (result.toUIMessageStreamResponse() and friends) are deprecated in favor of stateless helpers imported from ai. The old methods still run, but new code should use the helpers.
If you’re streaming outside a route, you can still loop over text directly:
const result = streamText({
model: 'anthropic/claude-sonnet-5.5',
prompt: 'Explain how HTTPS certificates work.',
});
for await (const chunk of result.textStream) {
process.stdout.write(chunk);
}
const finalText = await result.text;
result.stream (formerly fullStream) carries every event type, including tool calls and reasoning. result.textStream carries text deltas only.
useChat on the client
useChat moved to @ai-sdk/react and stopped managing input state. You own the input with useState and call sendMessage. Messages are arrays of typed parts, which is how tool calls and text share one message.
'use client';
import { useState } from 'react';
import { useChat } from '@ai-sdk/react';
export function ChatInterface() {
const [input, setInput] = useState('');
const { messages, sendMessage, status } = useChat();
return (
<div>
{messages.map((message) => (
<div key={message.id} className={message.role}>
{message.parts.map((part, i) =>
part.type === 'text' ? <span key={`${message.id}-${i}`}>{part.text}</span> : null,
)}
</div>
))}
<form
onSubmit={(e) => {
e.preventDefault();
sendMessage({ text: input });
setInput('');
}}
>
<input value={input} onChange={(e) => setInput(e.target.value)} />
<button type="submit" disabled={status !== 'ready'}>Send</button>
</form>
</div>
);
}
status is one of 'submitted', 'streaming', 'ready' or 'error', which replaces the old isLoading boolean. The hook posts to /api/chat by default. Point it elsewhere with a DefaultChatTransport. It also returns stop, regenerate, setMessages and addToolOutput, the last of which matters once you have tools that run in the browser. Tool calls show up as parts named after the tool (a getWeather tool produces tool-getWeather parts), so you can render them with their own UI.
Tool calls and stopWhen
This is where the old maxSteps advice lived. The behavior it controlled hasn’t changed: by default the SDK runs one step, so if the model calls a tool, you get the tool result back and no final answer. To let the model read the result and keep going, set a stop condition.
import { generateText, tool, isStepCount } from 'ai';
import { z } from 'zod';
const result = await generateText({
model: 'anthropic/claude-sonnet-5.5',
tools: {
getWeather: tool({
description: 'Get the current weather for a city',
inputSchema: z.object({
city: z.string().describe('The city name'),
unit: z.enum(['celsius', 'fahrenheit']).default('celsius'),
}),
execute: async ({ city, unit }) => {
// Call your real weather API here
return { city, temperature: 22, condition: 'partly cloudy', unit };
},
}),
searchProducts: tool({
description: 'Search the product catalog',
inputSchema: z.object({
query: z.string(),
maxResults: z.number().int().min(1).max(20).default(5),
}),
execute: async ({ query, maxResults }) => {
const results = await db.products.search(query, { limit: maxResults });
return results.map((p) => ({ id: p.id, name: p.name, price: p.price }));
},
}),
},
toolChoice: 'auto',
stopWhen: isStepCount(5),
prompt: 'What is the weather in Berlin, and find me 3 waterproof jackets under 100 euros?',
});
console.log(result.text);
const allToolCalls = result.steps.flatMap((step) => step.toolCalls);
isStepCount was called stepCountIs in versions 5 and 6, so expect both spellings in the wild. Other built-in conditions are hasToolCall('toolName'), which stops as soon as a specific tool runs, and isLoopFinished(). Three to five steps is a sensible ceiling for a chat feature. Higher than that and you’re building an agent, which has its own class.
toolChoice accepts 'auto' (the default), 'required', 'none', or { type: 'tool', toolName } to force one specific tool.
One 7.0 change to watch for in tool code: experimental_context became context, and tools that need per-call data now declare a contextSchema and receive values through toolsContext, keyed by tool name. If your tools read shared request state, read that section of the migration guide before running the codemod.
ToolLoopAgent for longer loops
If you’re writing the same tools and stopWhen config in several routes, the SDK has a class for it. ToolLoopAgent (it replaced Experimental_Agent in 6.0) bundles a model, instructions and tools, and defaults to a 20-step limit instead of one.
import { ToolLoopAgent, createAgentUIStreamResponse } from 'ai';
const supportAgent = new ToolLoopAgent({
model: 'anthropic/claude-sonnet-5.5',
instructions: 'You answer order and shipping questions.',
tools: { /* getOrder, getShipment, ... */ },
});
// app/api/chat/route.ts
export async function POST(request: Request) {
const { messages } = await request.json();
return createAgentUIStreamResponse({ agent: supportAgent, uiMessages: messages });
}
The loop ends early when the model stops calling tools, when a tool has no execute function, or when a tool call needs approval. In 7.0, approval is configured with toolApproval on the call or the agent, and the per-tool needsApproval flag is deprecated. AI SDK 7 also added WorkflowAgent in @ai-sdk/workflow for runs that need to survive process restarts and deploys. I’d only reach for that when an agent run is long enough that a deploy in the middle is a real possibility.
Structured output without generateObject
generateObject still exists, but it has been deprecated since 6.0 and the docs say it will be removed. The replacement is the output option on generateText:
import { generateText, Output } from 'ai';
import { z } from 'zod';
const { output } = await generateText({
model: 'anthropic/claude-sonnet-5.5',
output: Output.object({
schema: z.object({
summary: z.string(),
sentiment: z.enum(['positive', 'negative', 'neutral']),
topics: z.array(z.string()).max(5),
}),
}),
prompt:
'Analyze this customer review: "Delivery was fast but the packaging was damaged and one item was missing."',
});
console.log(output.sentiment);
output is typed from your schema. If the final step doesn’t end with a stop finish reason, reading output throws AI_NoOutputGeneratedError, so wrap it if the model might run out of tokens. The 7.0 release also renamed experimental_output to output, which is why older snippets look different.
This change is better than it first looks. Having one function for text, tools and structured data means you can combine tool calls with a typed final answer in a single call, which used to take two.
For reliability tradeoffs between native structured-output modes and tool-based extraction, see our guide to structured outputs and JSON tool calls.
Streaming partial objects to the browser
For long structured responses, stream the object as it fills in. On the server, use streamText with Output.object and return a text stream response. On the client, use useObject.
// app/api/report/route.ts
import { streamText, Output, createTextStreamResponse, toTextStream } from 'ai';
import { reportSchema } from './schema';
export async function POST(req: Request) {
const { prompt } = await req.json();
const result = streamText({
model: 'anthropic/claude-sonnet-5.5',
output: Output.object({ schema: reportSchema }),
prompt,
});
return createTextStreamResponse({ stream: toTextStream({ stream: result.stream }) });
}
'use client';
import { useObject } from '@ai-sdk/react';
import { reportSchema } from './api/report/schema';
export function ReportGenerator() {
const { object, submit, isLoading } = useObject({ api: '/api/report', schema: reportSchema });
return (
<div>
<button onClick={() => submit({ prompt: 'Generate Q1 summary' })} disabled={isLoading}>
Generate
</button>
{object?.title && <h1>{object.title}</h1>}
{object?.sections?.map((section, i) => (
<section key={i}>
<h2>{section?.heading}</h2>
<p>{section?.content}</p>
</section>
))}
</div>
);
}
Partial objects can’t be validated against the schema while they’re incomplete, so every field access needs optional chaining. If you’d rather iterate on the server, streamText also returns partialOutputStream.
Where the sharp edges still are
Retries are partly handled. generateText and streamText retry failed calls twice by default (maxRetries, set it to 0 to turn retries off). That covers transient errors. It won’t help with a hard quota or a provider outage, so a fallback model is still something you wire up yourself.
Context windows are your job. useChat keeps the whole conversation in state and sends it on every request, and the SDK doesn’t trim it. Long sessions eventually exceed the model’s context, so keep the last N messages or summarize older ones in the route before calling streamText.
Multi-step usage changed meaning in 7.0. usage, toolCalls and content now accumulate across steps. Code that assumed usage meant “the last call” will over-count after the upgrade. Use finalStep where you meant one step.
System prompts from the client are blocked now. AI SDK 7 rejects system messages inside messages by default. That’s a good security default, since it stops users from injecting a system prompt through chat history. If you replay trusted history from your own database, allowSystemInMessages: true turns it back on. Don’t set it for anything a user can edit.
Cost tracking still needs a home. usage is per call. Aggregating it per user or per feature, and enforcing a budget, is outside the SDK. Telemetry did get simpler in 7.0: register @ai-sdk/otel once at startup and every call is traced unless you opt out. If you’re trying to bring costs down rather than just measure them, our post on caching LLM responses covers the strategies that matter.
When to use it
The SDK is a good default for AI features in a JavaScript or TypeScript app: streaming chat, structured extraction, tool-using assistants, and agents with a bounded loop. The provider abstraction is real, and the 7.0 reasoning option (one setting translated into each provider’s own config) removes one more place where provider differences leaked into your code.
It’s a weaker fit for Python backends, and for orchestration where you need explicit graphs, checkpointing and branching you can inspect step by step. For that, look at LangGraph, CrewAI, or AutoGen.
The honest caveat is churn. Three majors shipped between July 2025 and June 2026 (5.0, 6.0 and 7.0). Each had a codemod and a clear guide, but if you build on this SDK, budget an upgrade every year or so and keep your AI calls behind a thin internal wrapper so the renames land in one file.
Frequently asked questions
- What is the latest version of the Vercel AI SDK?
- As of October 2026 the `ai` package is on the 7.x line. AI SDK 7.0 was announced on June 25, 2026. The 5.x and 6.x lines still publish maintenance releases under separate npm tags, but `npm install ai` gives you 7.x.
- What replaced maxSteps in the AI SDK?
- The `stopWhen` option. In AI SDK 7 you write `stopWhen: isStepCount(5)` (it was `stepCountIs` in versions 5 and 6). Other built-in conditions include `hasToolCall(...)` and `isLoopFinished()`. Without `stopWhen`, `generateText` and `streamText` stop after one step.
- Is generateObject deprecated?
- Yes. The AI SDK 6 migration guide deprecated `generateObject` and `streamObject` and says they will be removed in a future version. Use `generateText` or `streamText` with `output: Output.object({ schema })` instead.
- How do I upgrade from AI SDK 4 or 5 to 7?
- Go one major at a time using the official migration guides, run `npx @ai-sdk/codemod v7` for the last hop, and review what the codemod missed. Upgrade Node to 22 first and convert any `require()` calls, since AI SDK 7 is ESM only.
- Is the AI SDK a good fit for Python backends?
- No. It targets JavaScript and TypeScript runtimes. For Python, use the provider SDKs directly or a Python agent framework.
Sources
- Vercel blog: AI SDK 7
- AI SDK docs: Migrate AI SDK 6.x to 7.0
- AI SDK docs: Migrate AI SDK 5.x to 6.0
- AI SDK docs: Next.js App Router quickstart
- AI SDK docs: Tools and tool calling
- AI SDK docs: Generating structured data
- AI SDK docs: Building agents (ToolLoopAgent)
- AI SDK reference: useChat
- AI SDK reference: streamText
- AI SDK reference: generateText
- AI SDK docs: Object generation with useObject
- npm registry: ai package versions
Sponsored
More from this category
More from AI Integration
Sponsored
Discussion
Join the conversation.
Comments are powered by GitHub Discussions. Sign in with your GitHub account to leave a comment.
Sponsored