v6
Type
External
Status
Published
Created
Mar 17, 2026
Updated
May 8, 2026
Updated by
Dosu Bot
Source
View

Current-version integration. Requires ai@^6 and @ai-sdk/react@^3. For older versions see the overview.

Quickstart#

Create a project#

npx create-next-app@latest my-app
cd my-app
npx create-expo-app@latest my-app
cd my-app
mkdir my-app
cd my-app
npm init -y

Install dependencies#

<InstallCommand npm={["@assistant-ui/react", "@assistant-ui/react-ai-sdk", "ai@^6", "@ai-sdk/react@^3", "@ai-sdk/openai", "zod"]} />

<InstallCommand expo={["@assistant-ui/react-native", "@assistant-ui/react-ai-sdk", "ai@^6", "@ai-sdk/react@^3", "@ai-sdk/openai", "zod"]} />

<InstallCommand npm={["@assistant-ui/react-ink", "@assistant-ui/react-ai-sdk", "ai@^6", "@ai-sdk/react@^3", "@ai-sdk/openai", "zod", "ink", "react"]} />

Set up the backend route#

For React Native and Ink, host this route in a separate backend project and
point the client runtime at its absolute URL.

import { openai } from "@ai-sdk/openai";
import {
  streamText,
  convertToModelMessages,
  tool,
  zodSchema,
} from "ai";
import type { UIMessage } from "ai";
import { z } from "zod";

export const maxDuration = 30;

export async function POST(req: Request) {
  const { messages }: { messages: UIMessage[] } = await req.json();

  const result = streamText({
    model: openai("gpt-5.4-mini"),
    messages: await convertToModelMessages(messages), // async in v6
    tools: {
      get_current_weather: tool({
        description: "Get the current weather",
        inputSchema: zodSchema(z.object({ city: z.string() })),
        execute: async ({ city }) => {
          return `The weather in ${city} is sunny`;
        },
      }),
    },
  });

  return result.toUIMessageStreamResponse();
}

Set up the frontend#

"use client";

import { Thread } from "@/components/assistant-ui/thread";
import { AssistantRuntimeProvider } from "@assistant-ui/react";
import { useChatRuntime } from "@assistant-ui/react-ai-sdk";

export default function Home() {
  const runtime = useChatRuntime();
  return (
    <AssistantRuntimeProvider runtime={runtime}>
      <div className="h-full">
        <Thread />
      </div>
    </AssistantRuntimeProvider>
  );
}
import { AssistantRuntimeProvider } from "@assistant-ui/react-native";
import {
  AssistantChatTransport,
  useChatRuntime,
} from "@assistant-ui/react-ai-sdk";
import { View } from "react-native";
import { Thread } from "@/components/assistant-ui/thread";

const API_URL = process.env.EXPO_PUBLIC_API_URL ?? "http://localhost:3000";

export default function Home() {
  const runtime = useChatRuntime({
    transport: new AssistantChatTransport({
      api: `${API_URL}/api/chat`,
    }),
  });
  return (
    <AssistantRuntimeProvider runtime={runtime}>
      <View style={{ flex: 1 }}>
        <Thread />
      </View>
    </AssistantRuntimeProvider>
  );
}
import { AssistantRuntimeProvider } from "@assistant-ui/react-ink";
import {
  AssistantChatTransport,
  useChatRuntime,
} from "@assistant-ui/react-ai-sdk";
import { Box } from "ink";
import { Thread } from "./components/thread.js";

export function App() {
  const runtime = useChatRuntime({
    transport: new AssistantChatTransport({
      api: "http://localhost:3000/api/chat",
    }),
  });
  return (
    <AssistantRuntimeProvider runtime={runtime}>
      <Box flexDirection="column">
        <Thread />
      </Box>
    </AssistantRuntimeProvider>
  );
}

Set up UI components#

Follow the UI Components guide to wire up the Thread, composer, and supporting primitives.

Follow the React Native setup to add a native Thread, composer, and supporting primitives.

Follow the Ink setup to add a terminal Thread, composer, and supporting primitives.

Frontend tools and system messages#

AssistantChatTransport (used by default) forwards system messages and frontend tools to your backend on every request. Consume them with frontendTools:

import { openai } from "@ai-sdk/openai";
import { streamText, convertToModelMessages, zodSchema } from "ai";
import type { UIMessage } from "ai";
import { frontendTools } from "@assistant-ui/react-ai-sdk";

export async function POST(req: Request) {
  const {
    messages,
    system,
    tools,
  }: {
    messages: UIMessage[];
    system?: string;
    tools?: any;
  } = await req.json();

  const result = streamText({
    model: openai("gpt-5.4-mini"),
    system,
    messages: await convertToModelMessages(messages),
    tools: {
      ...frontendTools(tools),
      // your backend tools...
    },
  });

  return result.toUIMessageStreamResponse();
}

Frontend tools are registered through useAui (see the tools guide) and serialized for the backend via frontendTools.

Multi-step tool calls#

AI SDK v6 supports running multiple tool-call rounds in a single request via stopWhen. Use stepCountIs(N) to cap iterations:

import { openai } from "@ai-sdk/openai";
import {
  streamText,
  convertToModelMessages,
  stepCountIs,
} from "ai";

export async function POST(req: Request) {
  const { messages } = await req.json();

  const result = streamText({
    model: openai("gpt-5.4-mini"),
    messages: await convertToModelMessages(messages),
    tools: {
      /* ... */
    },
    stopWhen: stepCountIs(10), // cap at 10 tool-call iterations
  });

  return result.toUIMessageStreamResponse();
}

Without a stopWhen, AI SDK runs a single inference step. Set stepCountIs (or one of AI SDK's other stop conditions) when your tools chain multiple calls.

Quote context#

assistant-ui's composer can attach quote metadata to user messages (e.g. when the user selects text and clicks "Quote"). On the server, injectQuoteContext flattens that metadata into a markdown blockquote prefix so the LLM sees the quoted text:

import {
  streamText,
  convertToModelMessages,
} from "ai";
import { injectQuoteContext } from "@assistant-ui/react-ai-sdk";
import { openai } from "@ai-sdk/openai";

export async function POST(req: Request) {
  const { messages } = await req.json();
  const result = streamText({
    model: openai("gpt-5.4-mini"),
    messages: await convertToModelMessages(injectQuoteContext(messages)),
  });
  return result.toUIMessageStreamResponse();
}

injectQuoteContext is idempotent: if the blockquote prefix is already present, it is not duplicated.

Token usage#

useThreadTokenUsage exposes thread-level token usage on the client. Attach usage and modelId via messageMetadata on the server:

import { streamText, convertToModelMessages } from "ai";
import { frontendTools } from "@assistant-ui/react-ai-sdk";

export async function POST(req: Request) {
  const { messages, tools, config } = await req.json();
  const result = streamText({
    model: getModel(config?.modelName),
    messages: await convertToModelMessages(messages),
    tools: frontendTools(tools),
  });
  return result.toUIMessageStreamResponse({
    messageMetadata: ({ part }) => {
      if (part.type === "finish") {
        return { usage: part.totalUsage };
      }
      if (part.type === "finish-step") {
        return { modelId: part.response.modelId };
      }
      return undefined;
    },
  });
}
"use client";

import { useThreadTokenUsage } from "@assistant-ui/react-ai-sdk";

export function TokenCounter() {
  const usage = useThreadTokenUsage();
  if (!usage) return null;
  return <div>{usage.totalTokens} total tokens</div>;
}

getThreadMessageTokenUsage(message) returns per-message usage if you need to show it inline.

Attachments#

Attachments work through the standard attachment adapter. Pass it via useChatRuntime:

const runtime = useChatRuntime({
  adapters: { attachments: myAttachmentAdapter },
});

Your attachment adapter's send should return content parts in the AI SDK shape, e.g. for files:

return {
  ...attachment,
  status: { type: "complete" },
  content: [
    {
      type: "file",
      mimeType: attachment.contentType ?? "",
      filename: attachment.name,
      data: await getFileDataURL(attachment.file),
    },
  ],
};

For images, use { type: "image", image: <data url or remote url> }. AI SDK will pass these to providers that accept multimodal content.

Persisting chat history#

By default, messages live only in memory and reset on reload. To persist and restore history per thread, provide a ThreadHistoryAdapter via adapters.history.

The adapter **must** implement `withFormat`. `useChatRuntime` persists history through `withFormat(fmt)` so messages round-trip as AI SDK `UIMessage` objects. An adapter without `withFormat` throws at runtime; `load` and `append` on the top level are unused in the AI SDK path. For server-side cloud persistence with zero adapter code, see the [AssistantCloud integration](/docs/cloud/ai-sdk-assistant-ui).
"use client";

import { useChatRuntime } from "@assistant-ui/react-ai-sdk";
import type { ThreadHistoryAdapter } from "@assistant-ui/react";

const historyAdapter: ThreadHistoryAdapter = {
  // Required by the type — unused by useChatRuntime.
  async load() {
    return { headId: null, messages: [] };
  },
  async append() {},

  // fmt encodes UIMessage <-> storage rows (ai-sdk/v6 format).
  withFormat: (fmt) => ({
    async load() {
      const rows = await fetch("/api/history").then((r) => r.json());
      return { messages: rows.map(fmt.decode) };
    },
    async append(item) {
      await fetch("/api/history", {
        method: "POST",
        body: JSON.stringify({
          id: fmt.getId(item.message),
          parent_id: item.parentId,
          format: fmt.format,
          content: fmt.encode(item),
        }),
      });
    },
  }),
};

function Chat() {
  const runtime = useChatRuntime({ adapters: { history: historyAdapter } });
  // ...
}

Each persisted row follows { id, parent_id, format, content }. fmt.encode produces the content payload and fmt.decode reverses it, so your backend never needs to know about UIMessage internals.

Custom transport#

AssistantChatTransport is the default. To point at a non-default endpoint, instantiate it with a custom api:

import {
  useChatRuntime,
  AssistantChatTransport,
} from "@assistant-ui/react-ai-sdk";

const runtime = useChatRuntime({
  transport: new AssistantChatTransport({ api: "/my-custom-api/chat" }),
});

If you need a transport that does not inherit from AssistantChatTransport, you forfeit automatic system-message and frontend-tool forwarding; the transport is then a regular AI SDK ChatTransport and you control everything explicitly.

Advanced: useAISDKRuntime#

When you need direct access to the useChat instance (e.g. to share it with non-assistant-ui code), drop down to useAISDKRuntime:

import { useChat } from "@ai-sdk/react";
import { useAISDKRuntime } from "@assistant-ui/react-ai-sdk";

const chat = useChat();
const runtime = useAISDKRuntime(chat);

useAISDKRuntime does not provide cloud or the higher-level adapter slots; those are part of useChatRuntime. If you need both your own useChat access AND cloud thread support, you generally want useChatRuntime and to read the chat state through assistant-ui hooks instead.

Adapter support#

AdapterSupported via
Attachmentsadapters.attachments
Speechadapters.speech
Dictationadapters.dictation
Feedbackadapters.feedback
Historyadapters.history (must implement withFormat)
threadListVia cloud (managed) or RemoteThreadListRuntime (custom DB)

Key changes from v5#

Featurev5v6
ai packageai@^5ai@^6
@ai-sdk/react@ai-sdk/react@^2@ai-sdk/react@^3
convertToModelMessagesSyncAsync (await)
Tool schemaparameters: z.object({...})inputSchema: zodSchema(z.object({...}))
ResponsetoDataStreamResponse()toUIMessageStreamResponse()

Example#

examples/with-ai-sdk-v6 shows a complete reference implementation.

For responses that should survive a page reload mid-stream, see Resumable streams, which wraps the same v6 byte stream in assistant-stream/resumable.

Next#

v6 | Dosu