Infrastructure

Bottom Line First

The Vercel AI SDK is the right default choice if you are building a Next.js or React application that needs to talk to an LLM. It is well-designed, actively maintained, and removes a genuine amount of boilerplate around streaming, tool use, and structured output. Its multi-provider abstraction is one of the better ones in the TypeScript ecosystem.

But it is not a neutral infrastructure layer. It is opinionated about the full-stack React model, and the further you get from that model — background workers, non-Node runtimes, complex agent loops, fine-grained observability — the more you will be fighting the grain. For a pure API service or a Python-adjacent team, reach for the provider SDKs directly and save yourself the abstraction.

With that said: for its target use case, it is good. Here is what it actually does and where the edges are.


What the SDK Is

The Vercel AI SDK (package: ai) is an open-source TypeScript library for building LLM-powered applications. It ships two main entry points:

  • ai — core server-side primitives: generateText, streamText, Output
  • @ai-sdk/react — React hooks: useChat, useCompletion, experimental_useObject

Provider packages (@ai-sdk/openai, @ai-sdk/anthropic, @ai-sdk/google, etc.) sit alongside these and implement a shared interface. The SDK owns the protocol layer; providers own the API translation.

Install it:

npm install ai @ai-sdk/openai

The design goal is explicit: make it easy to wire a React frontend to an LLM backend without caring which model or provider is behind it. It succeeds at that goal.


Core Primitives

generateText

The simplest call — sends a prompt, waits for the full response, returns it:

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

const { text, usage } = await generateText({
  model: openai('gpt-4o'),
  prompt: 'Explain vector embeddings in two sentences.',
});

console.log(text);
console.log(usage); // { promptTokens, completionTokens, totalTokens }

generateText also accepts a messages array for multi-turn conversations, system for a system prompt, and maxTokens, temperature, and other standard parameters. Nothing surprising here.

streamText

The streaming equivalent. This is where the SDK earns its keep for UI work:

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

const result = streamText({
  model: anthropic('claude-sonnet-4-6'),
  prompt: 'Write a step-by-step guide to setting up a Next.js project.',
});

for await (const chunk of result.textStream) {
  process.stdout.write(chunk);
}

// Or consume the full text after streaming
const fullText = await result.text;
const usage = await result.usage;

The result object is a promise-like with multiple async iterators: textStream, fullStream (typed chunks including tool calls and finish events), and toDataStreamResponse() for HTTP streaming. That last one is what connects server to client.

Structured Output (Output API)

Structured output using Zod schemas. This is genuinely useful — you define a schema, the SDK handles the prompt engineering and JSON parsing, and you get a typed object back:

import { generateText, Output } from 'ai';
import { openai } from '@ai-sdk/openai';
import { z } from 'zod';

const { output } = await generateText({
  model: openai('gpt-4o'),
  output: Output.object({
    schema: z.object({
      sentiment: z.enum(['positive', 'negative', 'neutral']),
      confidence: z.number().min(0).max(1),
      summary: z.string().max(200),
    }),
  }),
  prompt: 'Analyze the sentiment of: "The onboarding flow was confusing but the product itself is great."',
});

// output is fully typed — no casting needed
console.log(output.sentiment); // 'mixed' or one of the enum values

Under the hood, the SDK uses the provider’s native structured output API when available (OpenAI’s response_format: json_schema, Anthropic’s tool forcing trick) and falls back to JSON mode + validation otherwise. It also handles retry on parse failure, which you would otherwise have to wire yourself.

streamText with Output.object() gives you a streaming version where partial objects arrive incrementally — useful for long-form structured generation where you want to start rendering before the full object is complete.


Multi-Provider Support

Switching providers is a one-line change:

// OpenAI
import { openai } from '@ai-sdk/openai';
const model = openai('gpt-4o');

// Anthropic
import { anthropic } from '@ai-sdk/anthropic';
const model = anthropic('claude-opus-4-8');

// Google
import { google } from '@ai-sdk/google';
const model = google('gemini-2.5-flash');

// All use the same generateText/streamText interface

First-party providers: OpenAI, Anthropic, Google (Gemini), Mistral, Cohere, Amazon Bedrock, Azure OpenAI, xAI, Together, Fireworks. Community providers cover more exotic options.

The abstraction is clean at the happy path. Where it gets messier: provider-specific features that do not map onto the shared interface. Vision inputs, audio, extended thinking, prompt caching configuration — these require provider-specific options that you pass through an escape hatch. It works, but the clean one-line swap breaks down when you need to use features that are not in the common API surface.


Streaming to the Client: useChat and useCompletion

This is where the SDK genuinely earns its abstraction costs. useChat manages the full round-trip of a chat interface — message history, in-flight state, streaming display, and error handling:

// 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'),
    messages,
  });

  return result.toDataStreamResponse();
}
// components/Chat.tsx
'use client';
import { useChat } from '@ai-sdk/react';
import { useState } from 'react';

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

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    if (!input.trim()) return;
    sendMessage({ text: input });
    setInput('');
  };

  return (
    <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>
      ))}
      <form onSubmit={handleSubmit}>
        <input
          value={input}
          onChange={(e) => setInput(e.target.value)}
          disabled={status === 'streaming' || status === 'submitted'}
        />
        <button type="submit" disabled={status !== 'ready'}>Send</button>
      </form>
    </div>
  );
}

That is a working streaming chat UI in roughly 40 lines across two files. The SDK handles the streaming protocol, progressive rendering, message state, and loading states. Compare that to writing your own ReadableStream + EventSource plumbing and you start to see the value.

useCompletion is the same idea for single-prompt completions rather than conversations.


Tool Use and Function Calling

The SDK has a first-class tools API that works across all providers that support function calling:

import { streamText, tool } from 'ai';
import { openai } from '@ai-sdk/openai';
import { z } from 'zod';

const result = streamText({
  model: openai('gpt-4o'),
  tools: {
    getWeather: tool({
      description: 'Get the current weather for a location',
      parameters: z.object({
        location: z.string().describe('City and country, e.g. "London, UK"'),
        unit: z.enum(['celsius', 'fahrenheit']).default('celsius'),
      }),
      execute: async ({ location, unit }) => {
        // Call your actual weather API here
        return { temperature: 18, condition: 'Cloudy', unit };
      },
    }),
  },
  prompt: 'What is the weather like in Tokyo right now?',
});

The execute function runs server-side automatically. For agentic loops where the model should call multiple tools in sequence, set maxSteps:

const result = streamText({
  model: openai('gpt-4o'),
  tools: { /* ... */ },
  maxSteps: 10, // model can call tools up to 10 times before returning
  prompt: 'Research the top 3 LLM providers and compare their pricing.',
});

Tool calls and results flow through the fullStream async iterator as typed events, so you can observe the agent’s actions in real time without waiting for the final response.

For client-side tools (things that need browser APIs or user confirmation), the SDK supports addToolResult on useChat — the model emits a tool call, the client executes it locally, and the result is sent back to continue generation.


React Server Components Integration

The SDK has experimental RSC support via @ai-sdk/rsc that lets you stream React components directly from the server as part of a generation:

// app/actions.ts
'use server';
import { streamUI } from '@ai-sdk/rsc';
import { openai } from '@ai-sdk/openai';

export async function generateUI(prompt: string) {
  const result = await streamUI({
    model: openai('gpt-4o'),
    prompt,
    text: ({ content }) => <p>{content}</p>,
    tools: {
      showWeather: {
        description: 'Display a weather card',
        parameters: z.object({ city: z.string(), temp: z.number() }),
        generate: async ({ city, temp }) => (
          <WeatherCard city={city} temperature={temp} />
        ),
      },
    },
  });

  return result.value; // A React node that streams in
}

This is clever and the demos look great, but be honest with yourself about whether you need it. The @ai-sdk/rsc API is still marked experimental, the mental model adds complexity, and for most applications useChat with tool results rendered in JSX is simpler and more debuggable. Use RSC streaming when you have genuinely heterogeneous UI that maps cleanly to model-driven tool calls.


Real Limitations and Gotchas

Runtime requirements. The streaming APIs depend on the Web Streams API and Response objects from the Fetch API. This works cleanly in Next.js App Router, Vercel Edge Functions, and Cloudflare Workers. It works in Node 18+ with some caveats. It does not work in older Node versions or environments without Web Streams support. This is usually fine, but worth checking before you commit.

Observability is manual. The SDK does not give you built-in token usage aggregation, latency tracking, or error rate visibility. You get usage on each response and that is it. For anything beyond a prototype you will want to instrument this yourself — wrap streamText calls, log to your own store, or integrate a tool like Langfuse or Braintrust. The SDK exposes onFinish and onChunkFinish callbacks that are your hooks for this.

Multi-step agent loops have limits. maxSteps handles simple agentic patterns, but for anything with branching logic, parallel tool calls, or state that persists across multiple model calls, the SDK does not provide an orchestration layer. You are writing that yourself. This is not a criticism — it is scoped correctly — but if you need a proper agent framework, look at LangGraph or build your own loop with generateText as the primitive.

The RSC API is genuinely experimental. Breaking changes have happened and will happen. Do not build production features on @ai-sdk/rsc unless you are comfortable tracking main.

Provider feature parity is uneven. Features like extended thinking (Anthropic), image generation, or audio I/O may not be in the shared interface. You will use provider-specific options for these, which means your code is no longer provider-agnostic for that feature.

Error handling is basic. The SDK throws typed errors (AI_APICallError, AI_InvalidResponseDataError, etc.) but retry logic, circuit breakers, and fallback models are your problem. A pattern that works well: wrap your LLM calls in a thin service layer that handles retries with exponential backoff and falls back to a secondary model after N failures.

Streaming and request cancellation. If the user navigates away mid-stream, you need to handle abort signals yourself. Pass signal: request.signal to streamText and wire it through. Easy to miss, and leaving orphaned LLM requests is a cost problem at scale.


When to Use It vs. When to Reach for Something Else

Use the Vercel AI SDK when:

  • You are building a Next.js or React application
  • You want streaming chat or completion UI without writing stream-handling plumbing
  • You need to swap providers without rewriting your application layer
  • Structured output with Zod schemas is a fit for your use case
  • Your team is TypeScript-first

Reach for something else when:

  • You are building a Python service — use the Anthropic or OpenAI Python SDKs directly
  • You need a full agent orchestration framework — look at LangGraph, CrewAI, or build your own
  • You have serious observability requirements from day one — integrate a dedicated LLM observability layer alongside whatever SDK you use
  • You are running in a non-standard runtime environment that does not support Web Streams
  • You need deep provider-specific features on day one and provider portability is not a goal

Verdict

The Vercel AI SDK gets the fundamentals right: the API surface is small, the types are accurate, the streaming protocol is solid, and the provider abstraction is clean enough to be useful without being so thick that it hides what is happening. The useChat + Route Handler pattern is probably the fastest path from zero to a working streaming chat UI in the TypeScript ecosystem right now.

The honest caveat is that it is Vercel’s SDK, optimized for Vercel’s deployment target and Vercel’s full-stack React model. That is not a problem if that is where you are building. If you are not, you are adding abstraction without getting all the benefits, and you will spend time working around edge cases that matter in your environment but not in theirs.

For its target use case — React/Next.js applications that need LLM integration — it is the right starting point. Add your own observability layer on top, keep your business logic decoupled from the SDK primitives, and you will be in good shape.