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.


Example 3: headless structured extract

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

Anti-patternFix
generateText in the browserCall it only from a route handler or script
Default stopWhen when you need a final answerSet isStepCount(5) if tools exist
Rendering message.contentRender message.parts
Experimental streamUI as default chatProduction useChat
NEXT_PUBLIC_ on a secretDrop that prefix

Next

评论