Vercel AI SDK
Vercel AI SDK for streaming LLM responses in React, Next.js, and edge runtimes
You are an expert in the Vercel AI SDK for building streaming AI interfaces in React and Next.js applications. ## Key Points - Use `streamText` for all user-facing responses to minimize time-to-first-token. - Define tools with Zod schemas for automatic validation of LLM-generated parameters. - Set `maxSteps` on tool-calling routes to allow the model to chain multiple tool invocations. - Use `toDataStreamResponse()` which handles SSE encoding and backpressure automatically. - Deploy chat routes on edge runtime (`export const runtime = "edge"`) for lower latency. - Use `useChat`'s `onError` callback to surface API errors to the user gracefully. - Leverage `generateObject` with Zod schemas when you need structured, typed responses. - Forgetting `"use client"` on components that use `useChat` or `useCompletion` hooks. - Not passing `messages` from the request body to `streamText`, resulting in loss of conversation context. - Using `generateText` instead of `streamText` in route handlers, which blocks until the full response is ready. - Not setting `maxSteps` when using tools, so the model only gets one tool call before responding. - Mixing up `@ai-sdk/react` (hooks) with `ai` (server utilities) in imports. ## Quick Example ```bash npm install ai @ai-sdk/openai @ai-sdk/anthropic ```
skilldb get llm-integration-skills/Vercel AI SDKFull skill: 283 linesVercel AI SDK — LLM Integration
You are an expert in the Vercel AI SDK for building streaming AI interfaces in React and Next.js applications.
Overview
The Vercel AI SDK (ai package) provides a unified interface for streaming LLM responses to the frontend. It includes React hooks (useChat, useCompletion), server-side helpers for multiple providers, and built-in support for tool calling, structured output, and edge runtime deployment. It works with OpenAI, Anthropic, Google, and other providers through a common interface.
Core Concepts
Installation
npm install ai @ai-sdk/openai @ai-sdk/anthropic
Provider Setup
import { openai } from "@ai-sdk/openai";
import { anthropic } from "@ai-sdk/anthropic";
// Models are referenced by provider function
const model = openai("gpt-4o");
// or
const model = anthropic("claude-sonnet-4-20250514");
generateText — Non-Streaming
import { generateText } from "ai";
const { text, usage } = await generateText({
model: openai("gpt-4o"),
system: "You are a helpful assistant.",
prompt: "Explain monads in simple terms.",
});
console.log(text);
console.log(`Tokens used: ${usage.totalTokens}`);
streamText — Streaming
import { streamText } from "ai";
const result = streamText({
model: openai("gpt-4o"),
prompt: "Write a short story about a robot.",
});
for await (const textPart of result.textStream) {
process.stdout.write(textPart);
}
generateObject — Structured Output
import { generateObject } from "ai";
import { z } from "zod";
const { object } = await generateObject({
model: openai("gpt-4o"),
schema: z.object({
recipe: z.object({
name: z.string(),
ingredients: z.array(z.object({
name: z.string(),
amount: z.string(),
})),
steps: z.array(z.string()),
}),
}),
prompt: "Generate a recipe for chocolate chip cookies.",
});
console.log(object.recipe.name);
Implementation Patterns
Next.js Route Handler (App Router)
// app/api/chat/route.ts
import { streamText } from "ai";
import { openai } from "@ai-sdk/openai";
export async function POST(req: Request) {
const { messages } = await req.json();
const result = streamText({
model: openai("gpt-4o"),
system: "You are a helpful assistant.",
messages,
});
return result.toDataStreamResponse();
}
React Chat Component with useChat
// components/Chat.tsx
"use client";
import { useChat } from "@ai-sdk/react";
export function Chat() {
const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat({
api: "/api/chat",
});
return (
<div>
<div className="messages">
{messages.map((m) => (
<div key={m.id} className={m.role === "user" ? "user" : "assistant"}>
<strong>{m.role}:</strong> {m.content}
</div>
))}
</div>
<form onSubmit={handleSubmit}>
<input
value={input}
onChange={handleInputChange}
placeholder="Type a message..."
disabled={isLoading}
/>
<button type="submit" disabled={isLoading}>
Send
</button>
</form>
</div>
);
}
Tool Calling
// app/api/chat/route.ts
import { streamText, tool } from "ai";
import { openai } from "@ai-sdk/openai";
import { z } from "zod";
export async function POST(req: Request) {
const { messages } = await req.json();
const result = streamText({
model: openai("gpt-4o"),
messages,
tools: {
getWeather: tool({
description: "Get the current weather for a location",
parameters: z.object({
location: z.string().describe("City name"),
}),
execute: async ({ location }) => {
// Call a real weather API
return { temperature: 72, condition: "sunny", location };
},
}),
},
maxSteps: 5, // Allow multi-step tool use
});
return result.toDataStreamResponse();
}
Displaying Tool Invocations on the Client
"use client";
import { useChat } from "@ai-sdk/react";
export function ChatWithTools() {
const { messages, input, handleInputChange, handleSubmit } = useChat();
return (
<div>
{messages.map((m) => (
<div key={m.id}>
<strong>{m.role}:</strong>
{m.parts.map((part, i) => {
if (part.type === "text") return <span key={i}>{part.text}</span>;
if (part.type === "tool-invocation") {
return (
<div key={i} className="tool-call">
<code>{part.toolInvocation.toolName}</code>
{part.toolInvocation.state === "result" && (
<pre>{JSON.stringify(part.toolInvocation.result, null, 2)}</pre>
)}
</div>
);
}
return null;
})}
</div>
))}
<form onSubmit={handleSubmit}>
<input value={input} onChange={handleInputChange} />
<button type="submit">Send</button>
</form>
</div>
);
}
useCompletion for Single-Turn
"use client";
import { useCompletion } from "@ai-sdk/react";
export function Summarizer() {
const { completion, input, handleInputChange, handleSubmit, isLoading } =
useCompletion({ api: "/api/summarize" });
return (
<div>
<form onSubmit={handleSubmit}>
<textarea value={input} onChange={handleInputChange} />
<button type="submit" disabled={isLoading}>Summarize</button>
</form>
{completion && <div className="result">{completion}</div>}
</div>
);
}
Best Practices
- Use
streamTextfor all user-facing responses to minimize time-to-first-token. - Define tools with Zod schemas for automatic validation of LLM-generated parameters.
- Set
maxStepson tool-calling routes to allow the model to chain multiple tool invocations. - Use
toDataStreamResponse()which handles SSE encoding and backpressure automatically. - Deploy chat routes on edge runtime (
export const runtime = "edge") for lower latency. - Use
useChat'sonErrorcallback to surface API errors to the user gracefully. - Leverage
generateObjectwith Zod schemas when you need structured, typed responses.
Core Philosophy
The Vercel AI SDK exists to eliminate the boilerplate that every LLM-powered web application has to write: SSE encoding on the server, stream parsing on the client, message state management in React, and tool call round-trips. By providing streamText on the server and useChat on the client, the SDK handles the plumbing so you can focus on the prompt, the tools, and the user experience. If you find yourself writing SSE parsing code or managing message arrays manually in a Next.js app, you are likely reimplementing what the SDK already provides.
The provider abstraction (@ai-sdk/openai, @ai-sdk/anthropic) is a genuine interoperability layer. Switching from OpenAI to Anthropic requires changing a single import and model identifier; the rest of the code -- streaming, tool calling, structured output -- works identically. This is not just a convenience for prototyping; it is an architectural decision that prevents vendor lock-in and makes it practical to evaluate different models on the same application without code changes.
useChat is not just a convenience hook -- it is the state management layer for conversational AI interfaces. It handles message history, loading state, error state, abort, and the data stream protocol in one package. Building this from scratch requires managing at least five pieces of state and a streaming fetch call with cleanup. The hook abstracts this correctly; the most common bugs come from trying to manage parts of this state manually while also using the hook, creating conflicts between two sources of truth.
Anti-Patterns
-
Using
generateTextin user-facing route handlers: CallinggenerateTextinstead ofstreamTextin an API route that serves a chat interface. This blocks until the full response is generated, adding seconds of latency before the user sees any output.streamTextshould be the default for interactive use. -
Not setting
maxStepsfor tool-using routes: Defining tools on astreamTextcall without settingmaxSteps. The default is 1, meaning the model gets a single tool call before being forced to respond. Multi-step tasks (look up data, then compute, then answer) requiremaxStepsto be set higher. -
Managing message state manually alongside
useChat: Maintaining a separateuseStatefor messages while also usinguseChat, which has its own internal message state. This creates two sources of truth that inevitably diverge, producing duplicated or missing messages. -
Forgetting
"use client"on hook components: UsinguseChatoruseCompletionin a component without the"use client"directive. These hooks use browser APIs and React state; they cannot run in a Server Component. The error message from Next.js can be cryptic. -
Returning
toDataStreamResponse()from a non-streaming context: CallingstreamTextand returningresult.toDataStreamResponse()but then trying to consume the response as JSON on the client instead of usinguseChat. The data stream protocol is a custom format designed for the SDK's client-side hooks; it is not plain JSON or standard SSE.
Common Pitfalls
- Forgetting
"use client"on components that useuseChatoruseCompletionhooks. - Not passing
messagesfrom the request body tostreamText, resulting in loss of conversation context. - Using
generateTextinstead ofstreamTextin route handlers, which blocks until the full response is ready. - Not setting
maxStepswhen using tools, so the model only gets one tool call before responding. - Mixing up
@ai-sdk/react(hooks) withai(server utilities) in imports. - Returning
result.toDataStreamResponse()withoutawaitwhere needed; the function is synchronous but the stream is not. - Not handling the loading state in the UI, leaving users with no feedback during generation.
Install this skill directly: skilldb add llm-integration-skills
Related Skills
Anthropic API
Anthropic Claude API integration for messages, streaming, and tool use
Embeddings
Text embeddings and semantic search with vector databases for LLM applications
Function Calling
Function/tool calling patterns for connecting LLMs to external APIs and data sources
Langchain
LangChain orchestration for chains, agents, memory, and retrieval workflows
Openai API
OpenAI API integration patterns for chat completions, embeddings, and assistants
Rag Pipeline
Building retrieval-augmented generation pipelines with document ingestion, retrieval, and synthesis