Skip to main content
Technology & EngineeringLlm Integration283 lines

Vercel AI SDK

Vercel AI SDK for streaming LLM responses in React, Next.js, and edge runtimes

Quick Summary24 lines
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 lines
Paste into your CLAUDE.md or agent config

Vercel 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 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.

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 generateText in user-facing route handlers: Calling generateText instead of streamText in 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. streamText should be the default for interactive use.

  • Not setting maxSteps for tool-using routes: Defining tools on a streamText call without setting maxSteps. 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) require maxSteps to be set higher.

  • Managing message state manually alongside useChat: Maintaining a separate useState for messages while also using useChat, 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: Using useChat or useCompletion in 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: Calling streamText and returning result.toDataStreamResponse() but then trying to consume the response as JSON on the client instead of using useChat. 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 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.
  • Returning result.toDataStreamResponse() without await where 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

Get CLI access →