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.
"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#
| Adapter | Supported via |
|---|---|
| Attachments | adapters.attachments |
| Speech | adapters.speech |
| Dictation | adapters.dictation |
| Feedback | adapters.feedback |
| History | adapters.history (must implement withFormat) |
| threadList | Via cloud (managed) or RemoteThreadListRuntime (custom DB) |
Key changes from v5#
| Feature | v5 | v6 |
|---|---|---|
ai package | ai@^5 | ai@^6 |
@ai-sdk/react | @ai-sdk/react@^2 | @ai-sdk/react@^3 |
convertToModelMessages | Sync | Async (await) |
| Tool schema | parameters: z.object({...}) | inputSchema: zodSchema(z.object({...})) |
| Response | toDataStreamResponse() | 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.