# Next.js AI

You are working in a Next.js 16 App Router project with React 19, TypeScript and the AI SDK (`ai`, `@ai-sdk/react`, and the AI Gateway or a provider package). Write code for the versions in `package.json`, not the ones in your training data: the AI SDK replaced its chat API in version 5 and renamed much of it again in version 7.

## Before you write code

- Read the `ai` version in `package.json`. AI SDK 7 is current and 6 is still common. Version 7 keeps most version 6 names as deprecated aliases, so both compile: write the installed major's names and never mix them.

| AI SDK 6 | AI SDK 7 |
| --- | --- |
| `system` on `streamText` and `generateText` | `instructions` |
| `stepCountIs(n)` | `isStepCount(n)` |
| `onFinish`, `onStepFinish` on the server | `onEnd`, `onStepEnd` |
| `result.toUIMessageStreamResponse(options)` | `createUIMessageStreamResponse({ stream: toUIMessageStream({ stream: result.stream, ...options }) })` |
| `needsApproval` on a tool | `toolApproval: { toolName: "user-approval" }` on the call |
| `experimental_throttle` on `useChat` | `throttle` |
| `totalUsage` for all steps (`usage` is the last step) | `usage` |

- The package ships its docs: read `node_modules/ai/docs/` (`02-getting-started/02-nextjs-app-router.mdx`, `04-ai-sdk-ui/`, `08-migration-guides/`) before using an API you are not sure of.
- Look for the project's chat components (`components/ui/`, `components/ai-elements/`): Message, PromptInput, Reasoning, Tool, Sources, Confirmation. Build on them instead of writing new bubbles, inputs or spinners.
- Find the project's auth helper, where model ids live, and whether it uses the AI Gateway (`AI_GATEWAY_API_KEY`, `"provider/model"` strings) or provider packages (`@ai-sdk/anthropic`, `@ai-sdk/openai`). Never invent a model id: reuse the project's or ask.

## The chat route

Stream chats from a Route Handler that converts UI messages, streams, and returns a UI message stream:

```ts
// app/api/chat/route.ts, AI SDK 7
import {
  convertToModelMessages,
  createUIMessageStreamResponse,
  isStepCount,
  streamText,
  toUIMessageStream,
  type UIMessage,
} from "ai";

export const maxDuration = 60;

export async function POST(req: Request) {
  const session = await getSession();
  if (!session) return new Response("Unauthorized", { status: 401 });
  const { messages }: { messages: UIMessage[] } = await req.json();

  const result = streamText({
    model: chatModel,
    instructions: INSTRUCTIONS,
    messages: await convertToModelMessages(messages),
    tools: createTools(session.user.id),
    stopWhen: isStepCount(5),
    abortSignal: req.signal,
  });

  return createUIMessageStreamResponse({
    stream: toUIMessageStream({ stream: result.stream, sendSources: true }),
  });
}
```

On AI SDK 6 the route uses `system`, `stepCountIs(5)` and `return result.toUIMessageStreamResponse({ sendSources: true })`.

- `convertToModelMessages` is async: `await` it.
- Check the session before reading the body, and rate-limit per user (a sliding window in Redis or the database) before calling the model. Answer 401 and 429 as plain responses.
- The body is untrusted. Drop `system` messages from the client, cap the number and size of messages, and `await validateUIMessages({ messages, tools })` when they carry tool or data parts. Instructions, model and user id come from the server, never from the request.
- Export `maxDuration` sized to the longest run you allow, and no `runtime` export: the Edge runtime is deprecated in Next.js 16.
- Bound the cost of a request: set `maxOutputTokens`, trim long histories before converting them, and record each user's usage in `streamText`'s `onEnd` (`onFinish` on 6).
- Without `stopWhen`, `streamText` stops after one step, so a tool call ends the reply with no answer. Set a step limit whenever there are tools.
- The client sees "An error occurred." by default. Pass `onError` to the UI message stream to map known errors to safe messages, and log the real error on the server.
- `sendReasoning` defaults to true and `sendSources` to false: set both to what the UI shows.
- Use a Route Handler for chat; use a Server Function for one-shot generation a form waits for. Never fetch your own `/api/chat` from a Server Component.

## Models and keys

- Provider keys and `AI_GATEWAY_API_KEY` live in server env without the `NEXT_PUBLIC_` prefix. Only Route Handlers and Server Functions call models; modules that build models or tools start with `import "server-only"`.
- Through the Gateway a model is a `"provider/model"` string; otherwise a provider instance such as `anthropic("model-id")`. Keep the choice in one module so switching providers changes one line.
- When users pick a model, the client sends a key from the list you offer and the server maps it through an allowlist record. Provider-specific settings go in `providerOptions`, keyed by provider.

## The chat UI

- The chat is a Client Component using `useChat` from `@ai-sdk/react`. Keep the input in your own state and call `sendMessage({ text, files })`; send per-request values with `sendMessage(message, { body })`, and point at another route with `transport: new DefaultChatTransport({ api })` from `ai`.
- `status` is `"submitted"`, `"streaming"`, `"ready"` or `"error"`. While submitted or streaming, the send button becomes a stop button calling `stop()`. On error, show a generic message and a retry calling `regenerate()`; `regenerate({ messageId })` reruns one reply.
- Render `message.parts` in order, switching on `part.type`: `text`, `reasoning`, `file`, `source-url`, `source-document`, `tool-<name>`, `dynamic-tool`, `data-<name>`. There is no `message.content`.
- A tool part's `state` is `input-streaming`, `input-available`, `approval-requested`, `approval-responded`, `output-available` (`preliminary: true` while it still streams), `output-error` or `output-denied`. Handle each one.
- Type the hook: `useChat<ChatMessage>()` with `UIMessage<Metadata, DataParts, InferUITools<ReturnType<typeof createTools>>>`, imported with `import type` so server code stays out of the client bundle.
- Map parts onto the project's components: Message per turn, Reasoning, Tool, Sources, Confirmation for approvals, PromptInput as the composer with its stop state.
- Stream without jank: throttle renders (`throttle: 50` on 7, `experimental_throttle: 50` on 6), key messages by `message.id`, memoize the Markdown renderer per part, and auto-scroll only while the reader is at the bottom. For bursty providers, add `experimental_transform: smoothStream()` to `streamText`.
- Send usage or the model per message with `messageMetadata` on the UI message stream and read `message.metadata`.

## Tools and approvals

- Define tools with `tool({ description, inputSchema, execute })` and a Zod schema whose fields use `.describe()`. Return small JSON. A thrown error becomes an `output-error` part the model reads, so throw messages it can act on.
- Build tools per request in a factory that closes over the signed-in user (`createTools(userId)`) and authorize inside `execute`. Never take a user or tenant id from tool input.
- Require approval for side effects: `needsApproval: true` (or a function of the input) on the tool on 6, `toolApproval: { deleteFile: "user-approval" }` on `streamText` on 7. Answer the `approval-requested` part with `addToolApprovalResponse({ id: part.approval.id, approved })` and pass `sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithApprovalResponses` to `useChat` so the reply continues. Set `experimental_toolApprovalSecret` so an approval forged in the client's history is rejected.
- Client-side tools have no `execute`: handle them in `onToolCall` (check `toolCall.dynamic` first), call `addToolOutput` without awaiting it, and pass `sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls`.

## Structured output

- Use `generateText({ output: Output.object({ schema }) })` and read `result.output`; `streamText` with `output` gives `partialOutputStream`. `Output.array({ element })` and `Output.choice({ options })` cover lists and labels. `generateObject` and `streamObject` are deprecated.
- With tools, the output is one more step: leave room in `stopWhen`. Catch `NoObjectGeneratedError` (schema mismatch) and `NoOutputGeneratedError` (the run ended on a tool call) and offer a retry.

## Saving chats

- Create the chat on the server and route to `/chat/[id]`; the page loads its messages and passes `id` and `messages` to `useChat`. Store `UIMessage[]` with their parts, not model messages.
- Give replies server ids with `originalMessages: messages` and `generateMessageId: createIdGenerator({ prefix: "msg", size: 16 })` on the UI message stream, then save the `messages` passed to `onEnd` (`onFinish` on 6).
- Send only the last message with the transport's `prepareSendMessagesRequest`; the route loads the history, checks the chat belongs to the user, and validates it.
- Decide what a closed tab means. To finish and save anyway, do not forward `req.signal`, and call `result.consumeStream()` without awaiting it. To stop paying when the reader stops, forward `req.signal` as above, pass `consumeSseStream: consumeStream` to the response, and save the partial reply when `isAborted` is true.

## Do not

- Do not write AI SDK 4 chat code: `input`, `handleInputChange`, `handleSubmit`, `isLoading`, `append`, `reload`, `message.content`, `maxSteps`, or tool `parameters` (now `inputSchema`).
- Do not return `StreamingTextResponse`, `toDataStreamResponse()` or `OpenAIStream`, even where older docs show them.
- Do not start new features on `@ai-sdk/rsc` (`streamUI`, `createStreamableValue`): it is experimental, and AI SDK UI is the supported path.
- Do not call a provider from the browser, expose a key with `NEXT_PUBLIC_`, or let the client set the instructions, the raw model id or the user id.

## Before you finish

- Run the type checker and the linter, and fix what they report.
- In the browser: send, stop mid-reply, regenerate, approve and deny a tool, reload a saved chat, and break the key to see the safe error.
- Test tools by calling `execute` directly (mock `server-only` in the test setup), and routes with a mock model from `ai/test` (`MockLanguageModelV3` on 6, `MockLanguageModelV4` on 7), copying its response shape from `node_modules/ai/docs/03-ai-sdk-core/55-testing.mdx`.
- Say which model, `maxDuration`, step limit and rate limit you chose.
