n4nAI

Vercel AI SDK v4 migration guide for existing chat apps

Step-by-step vercel ai sdk v4 migration guide for chat apps: update providers, streaming, tool calls, and avoid breaking changes with code.

n4n Team5 min read1,048 words

Audio narration

Coming soon — every post will get a voice note here.

If you maintain a Next.js chat app built on Vercel AI SDK v3 or earlier, this vercel ai sdk v4 migration guide strips the changelog down to the breaking changes that will actually break your build. v4 reorganizes providers into separate packages, reworks the streaming response shape, and replaces the old function-calling API with a typed tools interface. Follow the ordered path below to get to a clean upgrade without rewriting your UI.

1. Audit your current AI SDK surface

Run a quick grep across your app to find every touchpoint with the SDK. You are looking for imports from ai, ai/react, and any provider packages, plus direct usage of streamText, useChat, useCompletion, and OpenAI.

grep -rn "from 'ai'" app/ --include="*.ts" --include="*.tsx"
grep -rn "useChat\|useCompletion\|streamText" app/ --include="*.ts" --include="*.tsx"

Note the file paths. Route handlers (API routes or app router route.ts) and client components will need different fixes. If you see experimental_StreamData or AIStream, those are removed in v4—plan to delete them. Also check for maxTokens passed directly to streamText; v4 expects it inside model settings or as maxOutputTokens in some provider configs, so flag those lines now.

A thorough audit prevents the “works on my machine” syndrome where a stray v3 import survives in a rarely-hit admin route and crashes only in production.

2. Upgrade dependencies and split provider packages

As part of this vercel ai sdk v4 migration guide, v4 moves every provider out of the core ai package. Install the react bindings and the specific provider you use:

npm install ai@^4 @ai-sdk/react@^1 @ai-sdk/openai@^1 zod
npm uninstall @vercel/ai # if present from old versions

Check your package.json afterward. The core ai package now contains only server-side primitives (streamText, generateText, tool). Client hooks live in @ai-sdk/react. This separation is intentional: it shrinks client bundles by isolating server-only code.

Tradeoff: you must update import paths everywhere. A missing @ai-sdk/react import is the most common first-build failure. If you use multiple providers (Anthropic, Mistral), install each from its own @ai-sdk/* package rather than relying on a monolithic import.

3. Migrate provider instantiation

In v3 you called openai.chat('gpt-4') directly from the @ai-sdk/openai default export. v4 introduces a factory function and a unified model function call.

Old v3 code:

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

const result = streamText({
  model: openai.chat('gpt-4'),
  messages,
});

New v4 code:

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

const openai = createOpenAI({ apiKey: process.env.OPENAI_API_KEY });
const result = streamText({
  model: openai('gpt-4o'),
  messages,
});

The createOpenAI pattern lets you pass baseURL and headers without monkey-patching fetch. If you point at a gateway instead of OpenAI directly, this is where you do it. For example, n4n.ai exposes one OpenAI-compatible endpoint across 240+ models with automatic fallback when a provider is rate-limited; set baseURL: 'https://api.n4n.ai/v1' and your existing model strings keep working.

Provider-agnostic setup

Wrap the factory so you can swap later:

const getModel = (id: string) => createOpenAI({ apiKey: process.env.OPENAI_API_KEY }).chat(id);

In v4, openai('gpt-4o') returns a language model object, not a chat namespace. This unifies how you call OpenAI, Anthropic, or custom endpoints.

4. Update route handlers for the data stream

v4 standardizes the streamed protocol between server and useChat. The old toAIStreamResponse() is gone. Use toDataStreamResponse().

// app/api/chat/route.ts
export async function POST(req: Request) {
  const { messages } = await req.json();
  const result = streamText({
    model: openai('gpt-4o'),
    messages,
  });
  return result.toDataStreamResponse();
}

If you attached custom metadata or error handling, toDataStreamResponse accepts an options object:

return result.toDataStreamResponse({
  getErrorMessage: (e) => (e instanceof Error ? e.message : 'Unknown error'),
});

Pitfall: returning result.toTextStreamResponse() will compile but useChat will not parse it. The data stream wraps payloads in the typed protocol the client expects, with frames prefixed by 0: (text), 1: (tool call), etc.

Streaming with tools

When tools are present, the stream includes partial tool invocations. Do not try to JSON.stringify the whole result mid-stream; let the SDK serialize it.

5. Fix the client-side useChat import and callbacks

Client components must import from @ai-sdk/react, not ai/react. The hook API is largely stable, but onFinish now receives { text, usage, finishReason } instead of just the text string.

'use client';
import { useChat } from '@ai-sdk/react';

export function Chat() {
  const { messages, input, handleInputChange, handleSubmit } = useChat({
    onFinish: (result) => {
      // result.usage gives token counts if the provider returns them
      console.log('tokens:', result.usage);
    },
  });
  return (
    <form onSubmit={handleSubmit}>
      <input value={input} onChange={handleInputChange} />
    </form>
  );
}

If you previously used useChat({ api: '/api/chat' }) with a custom body, v4 passes headers and body more strictly. Send extra fields via body and read them in the route with await req.json().

Input handling changes

handleInputChange still works, but if you built a custom textarea with onInput, note that useChat no longer auto-clears input on error. Manage that in your submit handler.

6. Port function calls to the tools API

The biggest semantic change is replacing functions/function_call with tools built on Zod schemas. v4 validates arguments at the boundary.

v3 style:

streamText({
  model: openai.chat('gpt-4'),
  messages,
  functions: [{ name: 'getWeather', parameters: { type: 'object', properties: { city: { type: 'string' } } } }],
  function_call: 'auto',
});

v4 style:

import { tool } from 'ai';
import { z } from 'zod';

const result = streamText({
  model: openai('gpt-4o'),
  messages,
  tools: {
    getWeather: tool({
      parameters: z.object({ city: z.string() }),
      execute: async ({ city }) => ({ temp: 72, city }),
    }),
  },
});

The execute function runs server-side when the model triggers the tool. You no longer manually parse function_call arguments—the SDK handles streaming partial tool input. Tradeoff: you must define Zod schemas for every tool, but you eliminate a class of runtime JSON.parse errors.

Streaming tool output

In v4, tool results are sent back to the model automatically if you await result.toolCalls. For chat UIs, you typically just let toDataStreamResponse handle the round-trip; the client sees tool status frames without extra code.

7. Handle message history and system prompts

v4 tidies the messages array. System prompts should be passed as a separate system field rather than a leading { role: 'system' } message, although the latter still works. If you build messages from a database, split system text out to avoid double-counting tokens.

streamText({
  model: openai('gpt-4o'),
  system: 'You are a concise support agent.',
  messages, // user/assistant turns only
});

Pitfall: if you previously relied on messages[0].role === 'system' to inject instructions in the route, and also pass system, the model receives both. Pick one path. The system field is sent with higher priority in provider mappings and is the supported v4 idiom.

8. Common pitfalls and tradeoffs

  • Bundle size: Moving hooks to @ai-sdk/react helps, but if you import streamText in a client component by mistake, you pull server code into the browser. Keep ai server-only.
  • Edge vs Node: toDataStreamResponse() works on both runtimes, but tool execute functions using Node APIs (fs, prisma) must run on the Node runtime. Don’t set export const runtime = 'edge' blindly.
  • Token metering: v4 exposes usage on the final result. If you bill customers, read usage.promptTokens and usage.completionTokens. When routing through a gateway like n4n.ai, per-token usage metering is forwarded automatically, so your onFinish logging stays accurate.
  • Model strings: gpt-4 may map to different versions per provider. v4 does not resolve aliases; pass exact model IDs your provider supports.
  • Error shapes: getErrorMessage in toDataStreamResponse must return a string. Returning an object breaks the client parser.

9. Verification checklist

  1. npm run build passes with no ai/react import errors.
  2. curl -X POST localhost:3000/api/chat -H 'content-type: application/json' -d '{"messages":[{"role":"user","content":"hi"}]}' returns a stream starting with 0: or 1: frames, not raw text.
  3. Browser chat sends a message and renders assistant replies token-by-token.
  4. Tool calls fire execute and return structured data to the model without manual parsing.
  5. Server logs show usage objects on finish.
  6. System prompt appears exactly once in the rendered request (check provider debug logs).

Following this vercel ai sdk v4 migration guide in order prevents the cascade of errors that comes from mixing v3 and v4 idioms. The split packages and tools API add upfront work but yield stricter types and a smaller client footprint. Once the route handler returns a clean data stream and the client hook compiles against @ai-sdk/react, you have a maintainable base for further v4 features like structured generation and multi-modal inputs.

Tagsvercel-ai-sdkmigrationv4guide

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All vercel ai sdk deep dive posts →