Skip to main content
Technology & EngineeringAi Llm Services351 lines

Vercel AI SDK

"Vercel AI SDK: unified LLM interface, useChat/useCompletion hooks, streaming, tool calling, multi-provider, RSC integration"

Quick Summary18 lines
The Vercel AI SDK is a **unified interface** for building AI-powered applications across any LLM provider. It abstracts away provider differences so you can swap between OpenAI, Anthropic, Google, Groq, and others with a single line change. Build with three layers: **AI SDK Core** (server-side generation), **AI SDK UI** (React/Svelte/Vue hooks for streaming UI), and **AI SDK RSC** (React Server Components integration). Use the SDK to avoid vendor lock-in and to build streaming-first UIs with minimal boilerplate.

## Key Points

- **Use `streamText` + `toDataStreamResponse()`** for all chat endpoints — streaming is the default expected UX.
- **Define tools with Zod schemas** — the SDK validates parameters and provides full TypeScript types.
- **Set `maxSteps`** when using tools to allow multi-turn tool execution loops. Without it, the model cannot call tools iteratively.
- **Use `generateObject`/`streamObject`** for structured extraction — it is more reliable than parsing JSON from text.
- **Use `@ai-sdk/react`'s `useChat`** for chat UIs — it handles streaming, message management, loading states, and errors.
- **Create custom provider instances** with `createOpenAI()` to connect to any OpenAI-compatible API (Groq, Together, local models).
- **Use `onFinish` callbacks** in `useChat` for analytics, logging, or triggering side effects after a response completes.
- **Handle `isLoading` state** in the UI to disable inputs and show indicators during generation.
- **Using `generateText` for chat endpoints** — always use `streamText` for user-facing responses. Non-streaming creates unacceptable latency.
- **Not setting `maxSteps`** when tools are defined — without it, the model can only call tools once and cannot react to results.
- **Building custom streaming parsing** instead of using `useChat` — the hook handles the AI SDK's streaming protocol automatically.
- **Mixing raw fetch calls with AI SDK hooks** — `useChat` expects the specific data stream format from `toDataStreamResponse()`.
skilldb get ai-llm-services-skills/Vercel AI SDKFull skill: 351 lines
Paste into your CLAUDE.md or agent config

Vercel AI SDK Skill

Core Philosophy

The Vercel AI SDK is a unified interface for building AI-powered applications across any LLM provider. It abstracts away provider differences so you can swap between OpenAI, Anthropic, Google, Groq, and others with a single line change. Build with three layers: AI SDK Core (server-side generation), AI SDK UI (React/Svelte/Vue hooks for streaming UI), and AI SDK RSC (React Server Components integration). Use the SDK to avoid vendor lock-in and to build streaming-first UIs with minimal boilerplate.

Setup

Install the core package and a provider:

// Install: npm install ai @ai-sdk/openai @ai-sdk/anthropic @ai-sdk/google

import { generateText, streamText } from "ai";
import { openai } from "@ai-sdk/openai";

// Basic text generation
const { text } = await generateText({
  model: openai("gpt-4o"),
  prompt: "Explain the observer pattern in one paragraph.",
});

console.log(text);

Swap providers with one line:

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

// Same code, different provider
const { text: claudeText } = await generateText({
  model: anthropic("claude-sonnet-4-20250514"),
  prompt: "Explain the observer pattern in one paragraph.",
});

const { text: geminiText } = await generateText({
  model: google("gemini-2.0-flash"),
  prompt: "Explain the observer pattern in one paragraph.",
});

Key Techniques

Streaming Text

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

const result = streamText({
  model: openai("gpt-4o"),
  prompt: "Write a guide to error handling in TypeScript.",
  maxTokens: 1024,
});

// Stream to stdout
for await (const textPart of result.textStream) {
  process.stdout.write(textPart);
}

// Or get the full result
const fullResult = await result;
console.log("\nTokens:", fullResult.usage);

Streaming in Next.js API Routes

// 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,
    maxTokens: 1024,
  });

  return result.toDataStreamResponse();
}

useChat Hook (React)

// components/Chat.tsx
"use client";

import { useChat } from "@ai-sdk/react";

export function Chat() {
  const { messages, input, handleInputChange, handleSubmit, isLoading, error } =
    useChat({
      api: "/api/chat",
      initialMessages: [],
      onFinish: (message) => {
        console.log("Completed:", message.content);
      },
      onError: (error) => {
        console.error("Chat error:", error);
      },
    });

  return (
    <div>
      <div>
        {messages.map((m) => (
          <div key={m.id}>
            <strong>{m.role}:</strong> {m.content}
          </div>
        ))}
      </div>
      <form onSubmit={handleSubmit}>
        <input
          value={input}
          onChange={handleInputChange}
          placeholder="Say something..."
          disabled={isLoading}
        />
        <button type="submit" disabled={isLoading}>
          Send
        </button>
      </form>
      {error && <div>Error: {error.message}</div>}
    </div>
  );
}

Tool Calling

import { generateText, tool } from "ai";
import { openai } from "@ai-sdk/openai";
import { z } from "zod";

const result = await generateText({
  model: openai("gpt-4o"),
  prompt: "What's the weather in San Francisco?",
  tools: {
    getWeather: tool({
      description: "Get the current weather for a location",
      parameters: z.object({
        city: z.string().describe("City name"),
        unit: z.enum(["celsius", "fahrenheit"]).default("celsius"),
      }),
      execute: async ({ city, unit }) => {
        // Your actual weather API call
        const weather = await fetchWeather(city, unit);
        return weather;
      },
    }),
  },
  maxSteps: 5, // Allow up to 5 tool call rounds
});

console.log(result.text); // Final text after tool execution
console.log(result.steps); // Detailed step-by-step execution log

Streaming Tool Calls with useChat

// 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: {
      searchDocs: tool({
        description: "Search the documentation",
        parameters: z.object({
          query: z.string(),
        }),
        execute: async ({ query }) => {
          return await searchDocumentation(query);
        },
      }),
    },
    maxSteps: 3,
  });

  return result.toDataStreamResponse();
}

// Client side — tool invocations are available on messages
// components/Chat.tsx
"use client";
import { useChat } from "@ai-sdk/react";

export function Chat() {
  const { messages, input, handleInputChange, handleSubmit } = useChat();

  return (
    <div>
      {messages.map((m) => (
        <div key={m.id}>
          <strong>{m.role}:</strong> {m.content}
          {m.toolInvocations?.map((ti) => (
            <div key={ti.toolCallId}>
              Tool: {ti.toolName} | State: {ti.state}
              {ti.state === "result" && <pre>{JSON.stringify(ti.result)}</pre>}
            </div>
          ))}
        </div>
      ))}
      <form onSubmit={handleSubmit}>
        <input value={input} onChange={handleInputChange} />
      </form>
    </div>
  );
}

Structured Output (generateObject)

import { generateObject } from "ai";
import { openai } from "@ai-sdk/openai";
import { z } from "zod";

const { object } = await generateObject({
  model: openai("gpt-4o"),
  schema: z.object({
    title: z.string(),
    summary: z.string(),
    tags: z.array(z.string()),
    sentiment: z.enum(["positive", "negative", "neutral"]),
    confidence: z.number().min(0).max(1),
  }),
  prompt: "Analyze this article: ...",
});

console.log(object.title); // Fully typed
console.log(object.tags);  // string[]

Streaming Structured Output

import { streamObject } from "ai";
import { openai } from "@ai-sdk/openai";
import { z } from "zod";

const result = streamObject({
  model: openai("gpt-4o"),
  schema: z.object({
    steps: z.array(
      z.object({
        title: z.string(),
        description: z.string(),
      })
    ),
  }),
  prompt: "Create a 5-step plan to learn Rust.",
});

for await (const partialObject of result.partialObjectStream) {
  console.log(partialObject); // Partial typed object as it streams
}

Multi-Provider with Custom Configuration

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

// Custom provider instances
const groqProvider = createOpenAI({
  apiKey: process.env.GROQ_API_KEY!,
  baseURL: "https://api.groq.com/openai/v1",
});

const togetherProvider = createOpenAI({
  apiKey: process.env.TOGETHER_API_KEY!,
  baseURL: "https://api.together.xyz/v1",
});

// Use Groq for speed
const { text: fastText } = await generateText({
  model: groqProvider("llama-3.3-70b-versatile"),
  prompt: "Quick classification task...",
});

// Use Together for open-source models
const { text: openText } = await generateText({
  model: togetherProvider("meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo"),
  prompt: "Complex analysis task...",
});

Embeddings

import { embedMany, embed } from "ai";
import { openai } from "@ai-sdk/openai";

// Single embedding
const { embedding } = await embed({
  model: openai.embedding("text-embedding-3-small"),
  value: "What is machine learning?",
});

// Batch embeddings
const { embeddings } = await embedMany({
  model: openai.embedding("text-embedding-3-small"),
  values: ["First document", "Second document", "Third document"],
});

Best Practices

  • Use streamText + toDataStreamResponse() for all chat endpoints — streaming is the default expected UX.
  • Define tools with Zod schemas — the SDK validates parameters and provides full TypeScript types.
  • Set maxSteps when using tools to allow multi-turn tool execution loops. Without it, the model cannot call tools iteratively.
  • Use generateObject/streamObject for structured extraction — it is more reliable than parsing JSON from text.
  • Use @ai-sdk/react's useChat for chat UIs — it handles streaming, message management, loading states, and errors.
  • Create custom provider instances with createOpenAI() to connect to any OpenAI-compatible API (Groq, Together, local models).
  • Use onFinish callbacks in useChat for analytics, logging, or triggering side effects after a response completes.
  • Handle isLoading state in the UI to disable inputs and show indicators during generation.

Anti-Patterns

  • Using generateText for chat endpoints — always use streamText for user-facing responses. Non-streaming creates unacceptable latency.
  • Not setting maxSteps when tools are defined — without it, the model can only call tools once and cannot react to results.
  • Building custom streaming parsing instead of using useChat — the hook handles the AI SDK's streaming protocol automatically.
  • Mixing raw fetch calls with AI SDK hooksuseChat expects the specific data stream format from toDataStreamResponse().
  • Not using Zod for tool parameters — raw JSON schemas lose type safety. The SDK's Zod integration gives you validated, typed arguments.
  • Creating a new model instance per request — create the provider once at module scope and reuse it.
  • Ignoring the steps array in tool results — it contains the full execution trace including all tool calls and intermediate results.
  • Hardcoding a single provider — the SDK's main value is provider abstraction. Structure your code to make swapping easy.

Install this skill directly: skilldb add ai-llm-services-skills

Get CLI access →