Skip to content

The Vercel AI SDK End to End

10 min read · updated August 4, 2026

The Vercel AI SDK’s real product is not generateText. It is the streaming protocol between your server route and your React component — the wire format that carries text deltas, tool calls, tool results and partial objects, and the hook that reassembles them into rendered state. That is the piece you would otherwise spend a fortnight building badly.

Two halves, and the wire between them

The SDK ships as a server-side core and a set of UI bindings, and they are usually installed as separate packages. The core runs in a Node or edge runtime and speaks to providers. The UI bindings run in the browser and speak to your own route. Between them is a streaming response, and understanding that response is what stops the SDK from being magic.

browser                     your route (server)              provider
  |                              |                              |
  |-- POST /api/chat ----------->|                              |
  |   { messages: [...] }        |-- streamed request --------->|
  |                              |                              |
  |<-- streamed parts -----------|<-- token deltas -------------|
  |    text deltas               |    tool call requests        |
  |    tool call + result        |                              |
  |    finish + usage            |    (tool executed here)      |
  |                              |                              |
  hook reassembles into
  messages[] with parts

Three consequences follow. Tool execution happens on your server, not in the browser, so tools can hold secrets and touch your database. Every part of the stream is typed, so the client can render a tool call as a spinner and swap it for a result without you inventing an event protocol. And because the transport is an ordinary HTTP response, it works through the same infrastructure as everything else you deploy — no websocket, no separate service.

A model is a value

The provider design is the cleanest thing in the library and it has been stable across versions. Each provider package exports a factory; calling it with a model name gives you a value you pass to any of the core functions. Nothing about the call site changes when the model does.

import { openai } from "@ai-sdk/openai";
import { anthropic } from "@ai-sdk/anthropic";

const fast    = openai("<a chat model id>");
const careful = anthropic("<a chat model id>");

const model = process.env.CAREFUL === "1" ? careful : fast;

Because a model is a plain value, model choice belongs in configuration, and any OpenAI-compatible endpoint — a gateway, a local Ollama server, a vLLM instance — is reachable by pointing a compatible provider at a different base URL. That is the escape hatch worth knowing about before you need it.

The server half

The route handler is short by design: parse the messages from the request, call the streaming function, return its response. The core functions have kept their names — generateText and streamText for prose, generateObject and streamObject for schema-constrained output — across every version people are realistically running.

// app/api/chat/route.ts — structure is stable; the exact helper that
// converts the result into a Response has been renamed between majors,
// so check the one your installed version exports.

export const maxDuration = 60;   // streaming routes outlive the default

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

  const result = streamText({
    model,
    system: "You are terse. Answer in at most three sentences.",
    messages,
  });

  return result.toDataStreamResponse();   // <- name checked per version
}

Two deployment details cause most of the “it works locally and truncates in production” reports. Serverless functions have a default execution limit shorter than a long generation, so raise it explicitly. And any proxy or CDN in front of the route that buffers responses will hold the whole stream and deliver it at the end, which looks exactly like streaming being broken. If the first token takes as long as the whole answer, suspect buffering before you suspect the SDK.

The client half

The chat hook manages the message array, the input, the in-flight state and the incremental updates. What it gives you that a hand- rolled fetch does not is the reassembly: a message arriving as hundreds of deltas interleaved with tool events becomes one object with an ordered list of parts, re-rendered as it grows.

Render the parts rather than a single content string. A modern assistant message is not one blob of text — it is text, then a tool call, then a tool result, then more text — and code that reads only the text will silently drop everything the tools did. This is the single most common source of “the tool ran but nothing appeared in the UI”.

The hook’s import path and the exact shape of its return value have both changed between major versions of this SDK — the React bindings moved into their own package, and the message structure moved to explicit parts. Both changes were improvements and both broke existing code. Read the migration guide for your major version rather than trusting any tutorial’s import line, this one included.

Tools, and the multi-step loop

A tool is three things: a description the model reads, a schema for its arguments, and a function that runs on your server. The schema is typically a Zod object, and the same schema both constrains the model’s output and types the argument your function receives — which is the ergonomic win over writing JSON Schema by hand.

const tools = {
  getOrderStatus: tool({
    description: "Look up the current status of a customer order by its id.",
    parameters: z.object({
      orderId: z.string().describe("The order id, e.g. ORD-10482"),
    }),
    execute: async ({ orderId }) => db.orders.status(orderId),
  }),
};

The behaviour to configure deliberately is what happens after the tool returns. By default a single call returns the tool result and stops; to have the model see the result and continue — the actual agent loop — you set a maximum number of steps. Leave it at one and you get “the model called the tool and then said nothing”. Set it high and a confused model can run your tool a dozen times in one request. Four to six is a sane bound for a chat interface, and the reasoning behind it is the same as for stopping conditions anywhere else.

Tool descriptions are prompt text and should be written as carefully as your system prompt; the model chooses between tools by reading them. When it picks the wrong one, the fix is nearly always in the descriptions rather than in the system prompt — see tool description design.

Structured output

The object functions take a schema and return typed data rather than prose, and the streaming variant emits progressively more complete partial objects — which is how you render a form that fills itself in field by field rather than appearing all at once after ten seconds.

The two rules that matter are not SDK-specific. Keep the schema shallow, because deeply nested schemas raise the failure rate on every model. And decide what happens when validation fails: the function throws, and “throws” in a request handler means a 500 unless you catch it. One retry with the validation error appended is usually enough; the pattern is the subject of the Instructor page and applies identically here.

What moves between majors

This SDK ships fast, and the churn is concentrated in three places. Knowing which three lets you insulate against them in an afternoon.

AreaDescription
UI bindingsPackage name, hook return shape and message structure have all changed across majors. Insulate by keeping the hook in one component that maps SDK messages to your own view type; the rest of your UI then depends on your type, not theirs.
Response helpersThe helper that converts a stream result into an HTTP Response has been renamed. It appears exactly once per route, so this is the cheapest breakage to absorb — but it is also why a copied route handler from an old post fails with an unhelpful type error.
Provider-specific optionsAnything not in the common surface — reasoning effort, cache control, safety settings — is passed through a provider options bag whose shape follows the provider rather than the SDK. Expect these to move independently of the core.

The stable core is: a model is a value; there are four core functions; tools have a description, a schema and an executor; the server returns a stream. Build on those and a major upgrade is a morning.