Skip to content

Migrating the Vercel AI SDK From v3 to v4

11 min read · updated August 11, 2026

The 3.4 to 4.0 jump is mostly cleanup: the compatibility layer that let v2 code keep running was removed, experimental names became stable ones, and the class-based provider constructors were replaced by factory functions. Almost all of it is caught by TypeScript. The handful that is not is what this page is for.

Three kinds of breakage

Sorting the change list by what it does to you is more useful than reading it in the documentation’s order, because the three groups need different amounts of care.

  • Removed exports. The build fails, you fix the import, you are done. Cheap and loud.
  • Renamed options. An unknown option in an object literal is a type error if your types are current and silently ignored if they are not. This is the group that bites.
  • Changed control flow. One item, and it is the important one: streamText and streamObject now return immediately rather than needing to be awaited.

Everything below is from Vercel’s 3.4 to 4.0 migration guide.

Removals that stop the build

AIStream, StreamingTextResponse, streamToResponse
  -> use streamText() and its toDataStream() / toDataStreamResponse()
toAIStream, pipeAIStreamToResponse
  -> toDataStream, pipeDataStreamToResponse
experimental_StreamData
  -> StreamData
nanoid
  -> generateId
ExperimentalMessage, ExperimentalUserMessage, ExperimentalAssistantMessage,
ExperimentalToolMessage, ExperimentalTool
  -> the stable Core* names (CoreUserMessage, CoreAssistantMessage,
     CoreToolMessage, CoreTool)

The first line is the one that matters for anyone who upgraded from v2 without finishing the job. StreamingTextResponse was the v2 way to return a stream, it survived the whole 3.x line, and 4.0 is where it stops existing. If you converted some routes to streamText in v3 and left others alone, 4.0 is the release that bills you for the remainder — which is the general argument against leaving a dual-running migration half done.

The framework packages moved too: the Svelte, Vue and SolidJS exports were removed from ai in favour of @ai-sdk/svelte, @ai-sdk/vue and @ai-sdk/solid. React’s @ai-sdk/react package is the equivalent and is where new code should import useChat from; whether the older ai/react entry point still resolves depends on which major you are on, so check your installed version rather than assuming either way.

The Core* message type names in that list were themselves renamed again in a later major, which is why the live documentation may show a third name. This is the normal condition of this SDK. Pin a major, read that major’s guide, and do not mix snippets across them.

Provider construction and the registry

The class-based provider facades were removed. Anything of the form new OpenAI(...), new Anthropic(...), new Google(...) or new Mistral(...) becomes a factory call:

- import { OpenAI } from "@ai-sdk/openai";
- const provider = new OpenAI({ apiKey: KEY, baseUrl: URL });
+ import { createOpenAI } from "@ai-sdk/openai";
+ const provider = createOpenAI({ apiKey: KEY, baseURL: URL });

Two changes in that diff, and the second is easy to miss: baseUrl became baseURL across all providers. A lowercase-u baseUrl in a configuration object is not an error in plain JavaScript; it is an unrecognised key, so the provider silently uses its default endpoint. If a self-hosted or proxied deployment starts talking to the vendor’s public API after this upgrade, that spelling is the reason.

You only need the factory if you are configuring something. The default export — openai("gpt-4o-mini") — reads its key from the environment and is unaffected.

On the registry: the experimental model-registry names, experimental_Provider, experimental_ProviderRegistry, experimental_ModelRegistry and experimental_createModelRegistry, were removed in 4.0 in favour of the provider-registry naming. If your codebase built a registry to map string ids like "fast" or "cheap" onto models, that indirection is exactly what this rename touches, and the current export names are worth reading out of the installed package rather than from a guide.

Renames that change behaviour quietly

  • maxToolRoundtrips and maxAutomaticRoundtrips became maxSteps. Both spellings are gone, and the semantics shifted with the name: a step is a model call, so the number that used to mean “extra round trips after the first” is off by one against a limit that counts every call. A tool loop that used to run twice may now stop after one call, which looks like the model refusing to use the tool rather than like a budget change.
  • streamMode became streamProtocol in the UI hooks. Same idea, different key, and the same unrecognised-key risk as baseUrl.
  • Usage types. TokenUsage and CompletionTokenUsage became LanguageModelUsage, and EmbeddingTokenUsage became EmbeddingModelUsage. Type names only, but anything writing usage into a cost table is worth checking against whatever consumes those numbers downstream.
  • Provider-specific topK moved to the standard topK setting rather than living in per-provider options.
  • The await removal. const result = await streamText(...) becomes const result = streamText(...). Leaving the await in place is harmless in JavaScript terms, which is why this one tends to survive review and then confuse the next reader.

Message parts, which arrived after 4.0

If you are upgrading to the latest 4.x rather than to 4.0 exactly, there is a second change worth planning for. The 4.2 release restructured what useChat gives you: an assistant message now carries a parts array that preserves the order of what the model produced, with part types for text, reasoning, tool-invocation, source and file, per the 4.1 to 4.2 guide. The previous fields remain for backward compatibility, so nothing breaks on upgrade.

It matters because rendering from message.content flattens away the ordering. A response that reasons, calls a tool, then writes a conclusion has a real sequence, and the old shape could not express “this text came after that tool call”. If your UI shows tool calls or reasoning at all, migrate the rendering to iterate parts and switch on part.type; if it only shows text, the old field is fine and this is not urgent.

Doing the upgrade

  1. Finish the v3 migration first. Any remaining StreamingTextResponse or OpenAIStream is a hard failure in 4.0, so convert those routes while you still have a working version that supports both.
  2. Upgrade ai and every @ai-sdk/* package in one operation, to the same release wave. Mixed versions are the source of the confusing errors, not the migration itself.
  3. Run the type check and fix the removed exports. This clears most of the list mechanically.
  4. Grep for the silent ones the type check may not catch: baseUrl, streamMode, maxToolRoundtrips, maxAutomaticRoundtrips.
  5. Re-check any tool-calling budget by hand after the maxSteps rename, and exercise a conversation that genuinely needs two tool calls.
  6. Verify streaming end to end in a browser, not only in tests. The protocol between hook and route is the seam this upgrade moves.