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
reasoningoption ongenerateTextandstreamTextthat 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 throughtoolsContext. - 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/otelpackage, 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:
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:
systembecomesinstructions.onFinishbecomesonEnd.stepCountIsbecomesisStepCount.fullStreambecomesstream.experimental_telemetrybecomestelemetry.experimental_contextin tool callbacks becomescontext.- Long-deprecated
experimental_options, such asexperimental_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:
instructionsreplacessystem.toUIMessageStreamandcreateUIMessageStreamResponsejoin theaiimport.- Those two functions build the response. They replace
result.toUIMessageStreamResponse(), which still works but is deprecated.toUIMessageStreamreadsresult.stream, the new name forfullStream.
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
useChatinstead ofChat. new Chat()becomes a destructureduseChat().- In the script,
chat.sendMessagebecomessendMessageandchat.statusbecomesstatus.value. - The template drops the
chat.prefix, because top-level refs unwrap in templates.
<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. needsApprovalon a tool gives way totoolApprovalon the call.- On OpenAI's Responses API, setting a reasoning effort now turns on detailed reasoning summaries. Set
reasoningSummarytonullin 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:
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:
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:
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:
<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.