Practical examples
Three examples, shallow to deep: streaming chat, a multi-tool assistant, and headless structured extraction. Models use Gateway openai/gpt-4.1-mini (IDs change—use one your account allows). Dependencies: Installation.
Example 1: App Router streaming chat
Goal: a conversation on the Next.js home page; secrets stay in the route handler.
// app/api/chat/route.ts
import {
convertToModelMessages,
createUIMessageStreamResponse,
streamText,
toUIMessageStream,
type UIMessage,
} from 'ai';
export const maxDuration = 30;
export async function POST(req: Request) {
const { messages }: { messages: UIMessage[] } = await req.json();
const result = streamText({
model: 'openai/gpt-4.1-mini',
instructions: 'Reply concisely in the user language.',
messages: await convertToModelMessages(messages),
onError({ error }) {
console.error(error);
},
});
return createUIMessageStreamResponse({
stream: toUIMessageStream({ stream: result.stream }),
});
}
// app/page.tsx
'use client';
import { useChat } from '@ai-sdk/react';
import { useState } from 'react';
export default function Chat() {
const [input, setInput] = useState('');
const { messages, sendMessage, status } = useChat();
return (
<div>
{messages.map((m) => (
<div key={m.id}>
{m.role}: {m.parts.map((p, i) => (p.type === 'text' ? <span key={i}>{p.text}</span> : null))}
</div>
))}
<form
onSubmit={(e) => {
e.preventDefault();
sendMessage({ text: input });
setInput('');
}}
>
<input value={input} onChange={(e) => setInput(e.currentTarget.value)} disabled={status !== 'ready'} />
</form>
</div>
);
}
Check: pnpm run dev, send “Describe the AI SDK in one sentence”, and tokens should appear live. A reload clears client state (no persistence yet). You can put a chatId in DefaultChatTransport body for server storage—that is product code, not an SDK requirement. More on status / stop: useChat.
Example 2: weather + unit conversion
Add tools to the example 1 streamText call and set stopWhen: isStepCount(5), or the model stops after the tool and never writes a sentence. Full tool() definitions: Tools.
import { isStepCount, streamText, tool } from 'ai';
import { z } from 'zod';
const tools = {
weather: tool({
description: 'Get the weather in a location (fahrenheit)',
inputSchema: z.object({ location: z.string().describe('City name') }),
execute: async ({ location }) => ({ location, temperature: 72 }),
}),
convertFahrenheitToCelsius: tool({
description: 'Convert fahrenheit to celsius',
inputSchema: z.object({ temperature: z.number() }),
execute: async ({ temperature }) => ({
celsius: Math.round((temperature - 32) * (5 / 9)),
}),
}),
};
const result = streamText({
model: 'openai/gpt-4.1-mini',
messages, // already convertToModelMessages
stopWhen: isStepCount(5),
tools,
});
On the client, JSON.stringify tool-weather / tool-convertFahrenheitToCelsius or draw cards. Ask “What is the temperature in Shanghai in celsius?” You should see two tool parts, then a sentence. Keep real weather API keys on the server; validate location instead of concatenating it into a URL.
Scripts, webhooks, and queue workers—no useChat.
import { generateText, NoObjectGeneratedError, Output } from 'ai';
import { z } from 'zod';
try {
const { output } = await generateText({
model: 'openai/gpt-4.1-mini',
output: Output.object({
schema: z.object({
title: z.string().describe('Short title'),
tags: z.array(z.string()).describe('Up to 5 tags'),
summary: z.string().describe('Two sentences'),
}),
}),
prompt: `Extract metadata from:\n${articleText}`,
});
console.log(output.title, output.tags);
} catch (error) {
if (NoObjectGeneratedError.isInstance(error)) {
console.error(error.cause, error.text);
} else {
throw error;
}
}
For progressive display, use streamText + partialOutputStream, or useObject on the client. Local models: swap model for an Ollama compatible instance (see Providers). Small models follow JSON poorly—fall back to a larger cloud model.
Anti-patterns
Next