Search across all documentation pages
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
npm install ai @ai-sdk/openai @ai-sdk/anthropic// 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"),
messages,
});
return result.toDataStreamResponse();
}// app/chat/page.tsx
"use client";
import { useChat } from "@ai-sdk/react";
export default function Chat() {
const { messages, input, handleInputChange, handleSubmit } = useChat();
return (
<div>
{messages.map((m) => (
<div key={m.id}>
<strong>{m.role}:</strong> {m.content}
</div>
))}
<form onSubmit={handleSubmit}>
<input value={input} onChange={handleInputChange} />
<button type="submit">Send</button>
</form>
</div>
);
}When to reach for this: You need to integrate LLM chat, completions, or tool calling into a React or Next.js app with streaming support and minimal boilerplate.
// app/components/ChatWithTools.tsx
"use client";
import { useChat } from "@ai-sdk/react";
export default function ChatWithTools() {
const { messages, input, handleInputChange, handleSubmit, isLoading } =
useChat({
api: "/api/chat-tools",
onError: (error) => console.error("Chat error:", error),
});
return (
<div className="mx-auto max-w-2xl p-4">
<div className="space-y-4">
{messages.map((message) => (
<div
key={message.id}
className={`rounded-lg p-3 ${
message.role === "user" ? "bg-blue-100 ml-auto" : "bg-gray-100"
}`}
>
<p className="text-sm font-medium">{message.role}</p>
<p>{message.content}</p>
{message.toolInvocations?.map((tool, i) => (
<pre key={i} className="mt-2 text-xs bg-gray-200 p-2 rounded">
{JSON.stringify(tool, null, 2)}
</pre>
))}
</div>
))}
</div>
<form onSubmit={handleSubmit} className="mt-4 flex gap-2">
<input
value={input}
onChange={handleInputChange}
placeholder="Ask a question..."
className="flex-1 border rounded px-3 py-2"
disabled={isLoading}
/>
<button
type="submit"
disabled={isLoading}
className="bg-blue-600 text-white px-4 py-2 rounded disabled:opacity-50"
>
{isLoading ? "Thinking..." : "Send"}
</button>
</form>
</div>
);
}// app/api/chat-tools/route.ts
import { streamText, tool } from "ai";
import { anthropic } from "@ai-sdk/anthropic";
import { z } from "zod";
export async function POST(req: Request) {
const { messages } = await req.json();
const result = streamText({
model: anthropic("claude-sonnet-4-20250514"),
system: "You are a helpful assistant that can look up weather.",
messages,
tools: {
getWeather: tool({
description: "Get the current weather for a location",
parameters: z.object({
location: z.string().describe("City name"),
}),
execute: async ({ location }) => {
// Replace with real API call
return { location, temperature: 72, condition: "sunny" };
},
}),
},
maxSteps: 5,
});
return result.toDataStreamResponse();
}What this demonstrates:
useChat hookmaxStepsstreamText returns a StreamTextResult that converts to a Response via toDataStreamResponse(), sending Server-Sent Events to the clientuseChat manages the full conversation state: message history, input state, loading, error, and abort controllermaxSteps iterationsgenerateText is the non-streaming counterpart for batch processing or server-side generationUsing OpenRouter for model access:
npm install @openrouter/ai-sdk-providerimport { createOpenRouter } from "@openrouter/ai-sdk-provider";
const openrouter = createOpenRouter({
apiKey: process.env.OPENROUTER_API_KEY,
});
const result = streamText({
model: openrouter("anthropic/claude-sonnet-4-20250514"),
messages,
});Non-streaming generation:
import { generateText } from "ai";
import { openai } from "@ai-sdk/openai";
const { text, usage } = await generateText({
model: openai("gpt-4o"),
prompt: "Summarize this article in 3 bullets.",
});Structured output with generateObject:
import { generateObject } from "ai";
import { z } from "zod";
const { object } = await generateObject({
model: openai("gpt-4o"),
schema: z.object({
recipe: z.string(),
ingredients: z.array(z.string()),
steps: z.array(z.string()),
}),
prompt: "Generate a recipe for chocolate chip cookies.",
});useCompletion for single-turn prompts:
"use client";
import { useCompletion } from "@ai-sdk/react";
export default function Completion() {
const { completion, input, handleInputChange, handleSubmit } = useCompletion({
api: "/api/completion",
});
return (
<div>
<p>{completion}</p>
<form onSubmit={handleSubmit}>
<input value={input} onChange={handleInputChange} />
</form>
</div>
);
}Message type from ai includes id, role, content, and optional toolInvocationsexecute functionsuseChat returns strongly typed messages: Message[]providerOptions fieldimport type { Message } from "ai";
// Tool result types are inferred from the Zod schema
const weatherTool = tool({
parameters: z.object({ city: z.string() }),
execute: async ({ city }) => {
// city is typed as string
return { temp: 72 }; // return type is inferred
},
});Missing API keys - The SDK throws at runtime if the provider's API key env var is not set. Fix: Set OPENAI_API_KEY, ANTHROPIC_API_KEY, etc. in .env.local. Each provider has its own expected env var name.
useChat not updating - If messages don't stream in, the API route may not be returning a data stream response. Fix: Always return result.toDataStreamResponse(), not result.text or a plain JSON response.
Tool calls not executing - Tools defined but maxSteps not set defaults to 1, so multi-step tool use won't work. Fix: Set maxSteps: 5 (or higher) to allow the model to process tool results and continue.
CORS errors in development - API routes must be in the same Next.js app. Fix: Use Next.js API routes (app/api/) rather than calling an external server directly from useChat.
Large bundle from provider imports - Importing multiple providers increases client bundle size. Fix: Provider imports are server-only; keep them in API routes or server actions, never in "use client" files.
| Library | Best For | Trade-off |
|---|---|---|
| Vercel AI SDK | Full-stack React/Next.js AI apps | Opinionated, tied to Vercel ecosystem conventions |
| LangChain.js | Complex chains, RAG, agents | Heavier abstraction, steeper learning curve |
| OpenAI SDK directly | Simple OpenAI-only usage | No streaming React hooks, single provider |
| Anthropic SDK directly | Simple Claude-only usage | No streaming React hooks, single provider |
| LlamaIndex.ts | Data indexing and retrieval | Focused on RAG, less on chat UI |
streamText returns a StreamTextResult object.toDataStreamResponse() to convert it into a Response with Server-Sent EventsuseChat hook on the client consumes this stream and updates messages in real timeresult.text or plain JSON -- the hook expects the data stream protocolimport { openai } from "@ai-sdk/openai";
import { anthropic } from "@ai-sdk/anthropic";
// Just change the model line:
const result = streamText({
model: anthropic("claude-sonnet-4-20250514"),
messages,
});The unified interface means only the model parameter changes.
maxSteps controls how many tool-call-and-result rounds the model can performmaxSteps: 5 or higher for multi-step tool useimport { tool } from "ai";
import { z } from "zod";
const weatherTool = tool({
description: "Get weather for a city",
parameters: z.object({
city: z.string().describe("City name"),
}),
execute: async ({ city }) => {
return { temp: 72, condition: "sunny" };
},
});Tool parameter types are inferred from the Zod schema automatically.
result.toDataStreamResponse(), not a JSON responseapi prop in useChat points to the correct routestreamText sends tokens to the client as they are generated (streaming)generateText waits for the full response and returns it at oncestreamText for chat UIs where you want real-time token displaygenerateText for batch processing or server-side generation where streaming is not neededimport { generateObject } from "ai";
import { z } from "zod";
const { object } = await generateObject({
model: openai("gpt-4o"),
schema: z.object({
title: z.string(),
tags: z.array(z.string()),
}),
prompt: "Generate metadata for a blog post about React.",
});useCompletion manages a single prompt/response cycle, not a conversationcompletion (the response text), input, and form handlersapi to a route that uses streamText with a prompt instead of messages@ai-sdk/openai, @ai-sdk/anthropic) are server-only"use client" file bundles them into the client JavaScriptuseChat and useCompletion are the only AI SDK imports safe for client componentsuseChat returns messages: Message[] where Message includes id, role, content, and optional toolInvocationsMessage from "ai" if you need to type props that accept messagesimport type { Message } from "ai";import { createOpenRouter } from "@openrouter/ai-sdk-provider";
const openrouter = createOpenRouter({
apiKey: process.env.OPENROUTER_API_KEY,
});
const result = streamText({
model: openrouter("anthropic/claude-sonnet-4-20250514"),
messages,
});Reviewed by Chris St. John·Last updated Jul 16, 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥