streamText, generateObject, useChat, and provider switching in under 30 minutes

What the Vercel AI SDK is

The Vercel AI SDK is a TypeScript library that makes it easy to add streaming LLM features to web applications — especially Next.js apps. It abstracts over multiple LLM providers (OpenAI, Anthropic, Google, Mistral, and more), provides React hooks for building chat UIs, and handles the streaming plumbing that would otherwise require significant boilerplate.

It has two main layers: the AI SDK Core (server-side, framework-agnostic) and the AI SDK UI (client-side React hooks). This guide covers both.

Installation

npm install ai @ai-sdk/openai
# Or for Anthropic:
npm install ai @ai-sdk/anthropic
# Or Google:
npm install ai @ai-sdk/google
 

Core: streamText — streaming text from the server

streamText is the main server-side function. It streams tokens from any supported LLM and returns a response that can be piped to the client.

// app/api/chat/route.ts
import { streamText, convertToModelMessages, UIMessage } from 'ai';
import { openai } from '@ai-sdk/openai';

export async function POST(req: Request) {
  const { messages }: { messages: UIMessage[] } = await req.json();

  const result = streamText({
    model: openai('gpt-4o-mini'),
    system: 'You are a helpful assistant.',
    messages: convertToModelMessages(messages),  // UI -> model messages
    maxOutputTokens: 1024,
    temperature: 0.7,
  });

  return result.toUIMessageStreamResponse();  // streams to client
}

UI: useChat — the React hook

useChat connects your React component to the streaming API route above. It manages the message list and the streaming for you. In AI SDK v5 the hook no longer owns the input box, so you keep the input value in your own useState and read a status string instead of an isLoading boolean.

// app/page.tsx
'use client';
import { useChat } from '@ai-sdk/react';
import { DefaultChatTransport } from 'ai';
import { useState } from 'react';

export default function ChatPage() {
  const { messages, sendMessage, status } = useChat({
    transport: new DefaultChatTransport({ api: '/api/chat' }),
  });
  const [input, setInput] = useState('');

  return (
    <div>
      <div>
        {messages.map(m => (
          <div key={m.id}>
            <strong>{m.role}:</strong>{' '}
            {m.parts.map((part, i) =>
              part.type === 'text' ? <span key={i}>{part.text}</span> : null
            )}
          </div>
        ))}
        {status === 'streaming' && <div>Thinking...</div>}
      </div>
      <form onSubmit={e => { e.preventDefault(); sendMessage({ text: input }); setInput(''); }}>
        <input value={input} onChange={e => setInput(e.target.value)} placeholder='Ask anything...' />
        <button type='submit' disabled={status !== 'ready'}>Send</button>
      </form>
    </div>
  );
}
useChat appends each exchange to the messages array and re-renders as tokens arrive. It manages the streaming and message history for you; in v5 you supply the input value yourself and read the current status ('ready', 'submitted', 'streaming', or 'error') instead of an isLoading boolean.

generateObject — structured data from LLMs

generateObject returns a typed, validated Zod object instead of text. Use it whenever you need structured output from an LLM.

// app/api/extract/route.ts
import { generateObject } from 'ai';
import { openai } from '@ai-sdk/openai';
import { z } from 'zod';
 
const ProductSchema = z.object({
  name: z.string(),
  price: z.number(),
  category: z.enum(['electronics', 'clothing', 'food', 'other']),
  inStock: z.boolean(),
  tags: z.array(z.string()),
});
 
export async function POST(req: Request) {
  const { description } = await req.json();
 
  const { object } = await generateObject({
    model: openai('gpt-4o'),
    schema: ProductSchema,
    prompt: `Extract product details from: ${description}`,
  });
 
  return Response.json(object);  // fully typed and validated
}
 

useObject — streaming structured data to the client

useObject is the client-side counterpart to generateObject with streamObject on the server. It delivers partial objects as they stream, updating the UI progressively.

// Server: app/api/review/route.ts
import { streamObject } from 'ai';
import { openai } from '@ai-sdk/openai';
import { z } from 'zod';

const ReviewSchema = z.object({
  rating: z.number().min(1).max(5),
  summary: z.string(),
  pros: z.array(z.string()),
  cons: z.array(z.string()),
});

export async function POST(req: Request) {
  const { text } = await req.json();
  const result = streamObject({
    model: openai('gpt-4o'),
    schema: ReviewSchema,
    prompt: `Analyse this product review: ${text}`,
  });
  return result.toTextStreamResponse();
}

// Client: components/Review.tsx
// 'use client';
// import { experimental_useObject as useObject } from '@ai-sdk/react';
// const { object, submit, isLoading } = useObject({ api: '/api/review', schema: ReviewSchema });
// object.rating arrives as soon as that field is generated

Provider switching

The SDK uses a unified interface across providers. Switching is a one-line change.

import { openai }    from '@ai-sdk/openai';
import { anthropic } from '@ai-sdk/anthropic';
import { google }    from '@ai-sdk/google';
 
// Same API, different providers
const gpt4o   = openai('gpt-4o');
const claude  = anthropic('claude-opus-4-8');
const gemini  = google('gemini-2.0-flash');
 
// Swap the model variable — everything else stays the same
const result = streamText({
  model: process.env.USE_CLAUDE ? claude : gpt4o,
  messages,
});
 

Edge Runtime compatibility

AI SDK routes can run on the Edge Runtime, but it is opt-in via export const runtime = 'edge' and only works when your chosen provider and other dependencies are edge-compatible. When they are, add the export to the route:

export const runtime = 'edge';  // add to any route.ts
 
// Edge Runtime limitations to be aware of:
// - No Node.js built-ins (fs, path, etc.)
// - 25MB response limit on Vercel free tier
// - Cold starts are faster but warm memory is limited
 

generateText — non-streaming server calls

Use generateText when you do not need streaming — batch processing, background jobs, or when the response feeds into another step.

import { generateText } from 'ai';
import { anthropic } from '@ai-sdk/anthropic';

const { text, usage } = await generateText({
  model: anthropic('claude-haiku-4-5-20251001'),  // cheapest model for batch work
  prompt: 'Classify this support ticket as: billing, technical, or general: ' + ticket,
  maxOutputTokens: 10,
});

console.log(text);         // 'billing'
console.log(usage.totalTokens);  // track costs

Quick reference

Function / Hook Use case Streaming
streamText Server: stream text to client Yes
generateText Server: get text (no stream needed) No
streamObject Server: stream structured JSON Yes
generateObject Server: get validated object No
useChat Client: chat UI with history Yes
useCompletion Client: single-turn completion Yes
useObject Client: streaming structured data Yes