David John Thammineni
AI Engineering15 Feb 20242 min read

The Vercel AI SDK: Streaming Chat UIs in an Afternoon

The Vercel AI SDK: Streaming Chat UIs in an Afternoon

I've written the SSE-parsing, state-appending, abort-handling chat plumbing from scratch at least four times. The Vercel AI SDK packages exactly that layer — and having migrated a production copilot to it, I'm not writing it a fifth time.

The whole frontend

"use client";
import { useChat } from "ai/react";

export function Copilot() {
  const { messages, input, handleInputChange, handleSubmit,
          isLoading, stop } = useChat({ api: "/api/chat" });

  return (
    <div>
      {messages.map((m) => (
        <Message key={m.id} role={m.role} content={m.content} />
      ))}
      <form onSubmit={handleSubmit}>
        <input value={input} onChange={handleInputChange} />
        {isLoading && <button type="button" onClick={stop}>Stop</button>}
      </form>
    </div>
  );
}

Streaming state, message history, input binding, cancellation — the hook owns all of it. Messages update token-by-token as the stream arrives.

The whole backend

// app/api/chat/route.ts
import { openai } from "@ai-sdk/openai";
import { streamText } from "ai";

export async function POST(req: Request) {
  const { messages } = await req.json();
  const result = await streamText({
    model: openai("gpt-4-turbo"),
    system: "You are a support copilot for Acme. Cite doc links.",
    messages,
  });
  return result.toAIStreamResponse();
}

The provider abstraction is the strategic part: openai(...) swaps for anthropic(...) or a local model with one line, and the wire protocol stays identical. Model routing stops being a frontend concern entirely.

Production notes from the migration

Persist on finish: the onFinish callback server-side is where completed messages go to the database — don't trust the client to report back. Tool calls stream too: streamText surfaces tool invocations mid-stream, so you can render "Searching orders…" states as the model works. Rate-limit by user before the model call, not after — streaming responses make post-hoc limits awkward. And keep your system prompt server-side always; the client sends only user messages.

The hand-rolled version taught me how streaming works. The SDK version is 80% less code and handles the edge cases (chunk boundaries, abort mid-tool-call) that mine didn't. Learn it once by hand, then use the library.

Keep Reading