Skip to content
Release notesAI & LLMs

AI SDK 7: what changes for Vue apps

ai 7.0.0 and @ai-sdk/vue 4.0.0 were published on 25 June. For a Vue app the headline is small and welcome: useChat is back as a composable. The rest is renames, a codemod and two changes the codemod cannot make for you.

vuejs.school team30 Jun 20266 min readai 7.0 · @ai-sdk/vue 4.0 · Nuxt 4.4

Vercel published AI SDK 7.0.0 on 25 June, together with @ai-sdk/vue 4.0.0 and version 4 of the provider packages such as @ai-sdk/openai. The announcement is mostly about building agents; the migration guide lists the breaking changes. If your Vue or Nuxt app streams a chat with AI SDK 6, the upgrade touches both its page and its server route.

The worked example is the chat page and route from our May post on streaming responses, upgraded to version 7 and then given a typed tool. It uses Nuxt 4.4, with pages in app/ and API routes in server/ at the project root.

What shipped

The announcement groups the release into five areas: developing agents, running them, plugging in other agent harnesses, observing them, and going beyond text. The parts most Vue apps will notice:

  • A reasoning option on generateText and streamText that maps to each provider's own reasoning settings.
  • Typed tool context. A tool declares a contextSchema, and values such as API keys reach only that tool through toolsContext.
  • Tool approvals set on the call with toolApproval, from automatic rules to asking the user.
  • Timeouts for the whole call, each step, the gap between stream chunks and each tool.
  • Telemetry registered once at startup, with OpenTelemetry moved to a separate @ai-sdk/otel package, plus per-step performance figures such as time to first output.

There is more for agent builders: a durable WorkflowAgent, an experimental HarnessAgent, a terminal UI, file and skill uploads, and experimental realtime voice and video generation. Two of the new UI pieces, the MCP Apps renderer and the realtime hook, ship for React. Vue gets something more useful day to day.

useChat is back as a composable

AI SDK 5 replaced Vue's useChat with a Chat class. Version 4 of @ai-sdk/vue adds useChat again as a composable and deprecates the class, which keeps working so you can move one page at a time.

useChat returns messages, status and error as shallow refs, alongside sendMessage, regenerate, stop, clearError, resumeStream, addToolOutput and addToolApprovalResponse. The status values are unchanged: submitted, streaming, ready and error.

It also accepts a getter or a ref instead of a plain object, and creates a new chat whenever a value inside it changes. That is what you want when the chat belongs to a route. This is the <script setup> of a page at app/pages/chats/[id].vue:

app/pages/chats/[id].vue
import { useChat } from '@ai-sdk/vue'
import { DefaultChatTransport } from 'ai'

const route = useRoute()
const { messages, sendMessage } = useChat(() => ({
  id: String(route.params.id),
  transport: new DefaultChatTransport({ api: `/api/chats/${route.params.id}` }),
}))

The composable's source shows the catch: recreating the chat resets messages to the initial list and status to ready. Put only values that really identify a different conversation inside the getter. A model picker or a text input in there would wipe the conversation every time it changes.

Upgrade the packages and run the codemod

All AI SDK packages are now ESM only, and they need Node 22 or newer; the SDK is tested on 22, 24 and 26. A Nuxt app already uses ESM. Check your Node version, though: Nuxt 4.4.6 (18 May) raised its own floor to Node 22.12, or 24.11 on Node 24, but earlier 4.4 patches still allowed Node 20.

Upgrade the packages together, then run the codemod:

npm install ai@7 @ai-sdk/vue@4 @ai-sdk/openai@4 zod
npx @ai-sdk/codemod v7

The guide's codemod table lists more than thirty transformations, among them:

  • system becomes instructions.
  • onFinish becomes onEnd.
  • stepCountIs becomes isStepCount.
  • fullStream becomes stream.
  • experimental_telemetry becomes telemetry.
  • experimental_context in tool callbacks becomes context.
  • Long-deprecated experimental_ options, such as experimental_activeTools, get their stable names.

Many old names still work in version 7 as deprecated aliases, so code the codemod misses often keeps running, and most of those names show as deprecated in your editor. Removed options are different. TypeScript flags them, but the SDK no longer reads them, so code that runs anyway silently loses the setting: a leftover experimental_activeTools no longer limits which tools the model sees.

Update the chat route and page

The route from May changes in three places:

  1. instructions replaces system.
  2. toUIMessageStream and createUIMessageStreamResponse join the ai import.
  3. Those two functions build the response. They replace result.toUIMessageStreamResponse(), which still works but is deprecated. toUIMessageStream reads result.stream, the new name for fullStream.
server/api/chat.post.ts
import { createOpenAI } from '@ai-sdk/openai'
import { convertToModelMessages, createUIMessageStreamResponse, streamText, toUIMessageStream, type UIMessage } from 'ai'

export default defineEventHandler(async (event) => {
  const { messages } = await readBody<{ messages: UIMessage[] }>(event)
  const openai = createOpenAI({ apiKey: useRuntimeConfig(event).openaiApiKey })

  const abort = new AbortController()
  event.node.res.on('close', () => abort.abort())

  const result = streamText({
    model: openai('gpt-5.1'),
    instructions: 'You are a patient Vue tutor. Keep answers short.',
    messages: await convertToModelMessages(messages),
    abortSignal: abort.signal,
  })

  return createUIMessageStreamResponse({
    stream: toUIMessageStream({
      stream: result.stream,
      onError: () => 'The tutor is unavailable. Try again in a minute.',
    }),
  })
})

onError moves with the stream into toUIMessageStream. If you leave it out, the browser still gets the safe default, An error occurred., rather than the server's error details.

The page changes in four places:

  • The import names useChat instead of Chat.
  • new Chat() becomes a destructured useChat().
  • In the script, chat.sendMessage becomes sendMessage and chat.status becomes status.value.
  • The template drops the chat. prefix, because top-level refs unwrap in templates.
app/pages/chat.vue
<script setup lang="ts">
import { useChat } from '@ai-sdk/vue'
import { DefaultChatTransport } from 'ai'

const input = ref('')
const { messages, status, error, sendMessage, stop } = useChat({
  transport: new DefaultChatTransport({ api: '/api/chat' }),
})
const busy = computed(() => status.value === 'submitted' || status.value === 'streaming')

function send() {
  if (!input.value.trim()) return
  sendMessage({ text: input.value })
  input.value = ''
}
</script>

<template>
  <div v-for="message in messages" :key="message.id">
    <strong>{{ message.role === 'user' ? 'You' : 'Tutor' }}</strong>
    <template v-for="(part, index) in message.parts" :key="index">
      <p v-if="part.type === 'text'">{{ part.text }}</p>
    </template>
  </div>
  <p v-if="error" role="alert">{{ error.message }}</p>
  <form @submit.prevent="send">
    <label>Message <input v-model="input" type="text"></label>
    <button type="submit" :disabled="busy">Send</button>
    <button v-if="busy" type="button" @click="stop()">Stop</button>
  </form>
</template>

Changes the codemod cannot make

Two changes keep compiling but behave differently, so look for them by hand.

System messages in messages are rejected by default. If you store conversations and some contain { role: 'system' } entries, version 7 refuses them with Invalid prompt: System messages are not allowed in the prompt or messages fields. Use the instructions option instead. Move the text to instructions. The guide offers allowSystemInMessages: true for old histories, with a warning: if users can edit those messages, they can rewrite your system prompt. Only use it for messages your server wrote.

Results now cover every step. In a call with tools, usage, toolCalls, toolResults, content and similar properties on the result used to describe the last step only. Now they add up all steps, and usage means what totalUsage meant. The old values live under finalStep. If you log token counts or bill users from usage, the numbers will rise for multi-step calls after the upgrade, and that is the correct total.

Three smaller changes in the guide are also left to you:

  • Request bodies are no longer kept on results unless you opt in with include.
  • needsApproval on a tool gives way to toolApproval on the call.
  • On OpenAI's Responses API, setting a reasoning effort now turns on detailed reasoning summaries. Set reasoningSummary to null in the OpenAI provider options to keep them off.

Add a typed tool to the chat

A tool is a function the model may call, with a schema for its input. Define it once in Nuxt's shared/ folder so the server can run it and the page can infer its types:

shared/tutor-tools.ts
import { tool } from 'ai'
import { z } from 'zod'

const docsPages = {
  computed: 'https://vuejs.org/guide/essentials/computed.html',
  watchers: 'https://vuejs.org/guide/essentials/watchers.html',
  props: 'https://vuejs.org/guide/components/props.html',
} as const

export const tutorTools = {
  findDocsPage: tool({
    description: 'Find the official Vue docs page for a topic',
    inputSchema: z.object({ topic: z.enum(['computed', 'watchers', 'props']) }),
    execute: async ({ topic }) => ({ topic, url: docsPages[topic] }),
  }),
}

In the route, import it with import { tutorTools } from '#shared/tutor-tools', add isStepCount to the ai import, and pass three more options to streamText:

server/api/chat.post.ts
tools: tutorTools,
stopWhen: isStepCount(3),
timeout: { totalMs: 60_000, chunkMs: 15_000 },

stopWhen: isStepCount(3) lets the model call the tool, read the result and then answer, in up to three steps. The new timeout ends the call after a minute, or sooner if the provider goes quiet for fifteen seconds mid-stream.

On the page, give useChat the message type so each tool part knows its input and output:

app/pages/chat.vue
import type { InferUITools, UIDataTypes, UIMessage } from 'ai'
import type { tutorTools } from '#shared/tutor-tools'

type TutorMessage = UIMessage<unknown, UIDataTypes, InferUITools<typeof tutorTools>>
const { messages, status, error, sendMessage, stop } = useChat<TutorMessage>({
  transport: new DefaultChatTransport({ api: '/api/chat' }),
})

Then render the tool's part, named tool-findDocsPage after the key, next to the text part:

app/pages/chat.vue
<a v-else-if="part.type === 'tool-findDocsPage' && part.state === 'output-available'" :href="part.output.url">
  Read the docs on {{ part.output.topic }}
</a>
<p v-else-if="part.type === 'tool-findDocsPage' && part.state === 'output-error'">The docs lookup failed.</p>
<p v-else-if="part.type === 'tool-findDocsPage'">Looking up the docs…</p>

Check the type and the state in one condition, as here, so TypeScript knows part.output exists. Before that state the part is input-streaming or input-available, which the last line covers. If execute throws, the part ends in output-error; without its own branch it would fall through to "Looking up the docs…" and stay there.

Upgrade now or wait

Upgrade a branch now. The codemod does the renames, the deprecated names keep running, and useChat makes the Vue code shorter. If your app relies on an experimental feature from 6, read its section of the guide first; that is where version 7 removed the most.

Check it stuck3 questions from the Tools and structured output with the AI SDK quiz.
Practice now
Was this helpful?

Keep reading