Hi! Ask me anything about Material UI, MUI X Data Grid, Date Pickers, Charts, Tree View, Scheduler, or Chat.
Chat - Vercel AI SDK adapter
Connect the Vercel AI SDK to the chat runtime by piping streamText output, or a useChat instance, into the chat UI.
The chunk types createAiSdkAdapter accepts match the AI SDK's UI Message Stream protocol structurally, so no runtime dependency on the ai package is needed.
Install ai to get full type info on the values you pass in, or skip it and rely on the local mirror.
Use this adapter when your backend speaks the AI SDK's UI Message Stream; for prototyping without a backend see the Echo adapter, and for other protocols see Building an adapter.
Demo
The demo below talks to a live MUI documentation assistant served using AI SDK. Ask it anything about Material UI or MUI X—it streams text and reasoning back through the adapter.
Pattern A: Streaming from a server route
The most common setup.
A server route uses streamText(...).toUIMessageStreamResponse(), and the client adapter pipes the response body straight into the chat:
// app/api/chat/route.ts
import { streamText, convertToModelMessages } from 'ai';
import { openai } from '@ai-sdk/openai';
export async function POST(req: Request) {
const { messages } = await req.json();
const result = streamText({
model: openai('gpt-4o-mini'),
messages: convertToModelMessages(messages),
});
return result.toUIMessageStreamResponse();
}
'use client';
import { ChatBox } from '@mui/x-chat';
import { createAiSdkAdapter } from '@mui/x-chat/headless';
const adapter = createAiSdkAdapter({
stream: async ({ messages, signal }) => {
const response = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
messages: messages.map(({ id, role, parts }) => ({ id, role, parts })),
}),
signal,
});
if (!response.ok || !response.body) {
throw new Error(`Chat API failed: ${response.status}`);
}
return response.body;
},
});
export default function ChatPage() {
return <ChatBox adapter={adapter} sx={{ height: 600 }} />;
}
Both NDJSON ({json}\n) and SSE (data: {json}\n\n with [DONE]) wire framings are decoded automatically—whichever shape your server emits, it works.
The request body shape is defined by your backend, not the adapter—the adapter only consumes the response stream, so send whatever message format your route expects.
Pattern B: Streaming in-process
Skip the server route when the model can be called in the same process (worker with proxied API keys, edge runtime, server actions returning a stream).
.toUIMessageStream() yields the same chunk shapes as a fetch response, just as JavaScript objects rather than bytes:
import { streamText } from 'ai';
import { openai } from '@ai-sdk/openai';
import { createAiSdkAdapter } from '@mui/x-chat/headless';
const adapter = createAiSdkAdapter({
stream: ({ message }) =>
streamText({
model: openai('gpt-4o-mini'),
prompt: message.parts
.map((part) => (part.type === 'text' ? part.text : ''))
.join(''),
}).toUIMessageStream(),
});
The demo below simulates toUIMessageStream() with a hand-built object stream, so you can watch the token-by-token flow without any backend:
Pattern C: Sharing state with useChat
If you already drive state through useChat and want the chat to render alongside other UI bound to the same chat object (a streaming status badge, a sidebar with message counts), hand the chat instance to the adapter directly:
'use client';
import * as React from 'react';
import { useChat } from '@ai-sdk/react';
import { DefaultChatTransport } from 'ai';
import { ChatBox } from '@mui/x-chat';
import { createAiSdkAdapter } from '@mui/x-chat/headless';
export default function ChatPage() {
const chat = useChat({
transport: new DefaultChatTransport({ api: '/api/chat' }),
});
const adapter = React.useMemo(() => createAiSdkAdapter({ chat }), [chat]);
return (
<React.Fragment>
<StreamingBadge status={chat.status} />
<ChatBox adapter={adapter} sx={{ height: 600 }} />
</React.Fragment>
);
}
Keep the memo keyed on the whole chat object, matching the adapter's own guidance—recreating the adapter when chat changes is fine, because the runtime reads it through a ref.
This integration trades token-by-token streaming for a unified state object.
useChat.sendMessage only resolves after the full reply has streamed in, so the chat shows the assistant message whole.
If you need both real-time tokens and shared state, prefer Pattern A and call chat.setMessages from a useChat onFinish callback when you need a copy.
Pattern D: Reusing a transport
When you've already configured a transport (custom headers, body transforms, auth interceptors), forward its sendMessages method to { stream }:
import { DefaultChatTransport } from 'ai';
import { createAiSdkAdapter } from '@mui/x-chat/headless';
const transport = new DefaultChatTransport({
api: '/api/chat',
headers: { Authorization: `Bearer ${token}` },
});
const adapter = createAiSdkAdapter({
stream: ({ messages, signal }) =>
transport.sendMessages({ chatId: 'main', messages, abortSignal: signal }),
});
transport.sendMessages returns a ReadableStream of UI message chunks that structurally satisfies the stream callback—no extra wiring needed.
Options
All option and request types—CreateAiSdkAdapterOptions, CreateAiSdkAdapterStreamOptions, CreateAiSdkAdapterChatOptions, CreateAiSdkAdapterRequest, AiSdkUIMessageChunk, and AiSdkChatInstance—are exported from @mui/x-chat and @mui/x-chat/headless.
| Option | Type | Notes |
|---|---|---|
stream |
(req) => ReadableStream<AiSdkUIMessageChunk | Uint8Array> |
Token streaming. Bytes are decoded for both NDJSON and SSE framings. |
chat |
AiSdkChatInstance (matches useChat()'s shape) |
Whole-reply integration with @ai-sdk/react—see Pattern C above for the streaming tradeoff. |
The stream callback receives a CreateAiSdkAdapterRequest:
| Field | Type | Notes |
|---|---|---|
message |
ChatMessage |
The message just sent. |
messages |
ChatMessage[] |
Full conversation history, including message. |
attachments |
ChatSendMessageInput['attachments'] (optional) |
Files attached in the composer—forward them in your request body if your backend accepts uploads. |
metadata |
ChatSendMessageInput['metadata'] (optional) |
Arbitrary send metadata passed by the caller. |
signal |
AbortSignal |
Aborts when the user presses stop—pass it to fetch. |
AI SDK error chunks ({ type: 'error', errorText }) are converted to ChatStreamError and surfaced through the chat's built-in error UI.
Unknown chunk types pass through unchanged so newer protocol additions don't require an adapter update.
If start, finish, or abort chunks arrive without a messageId, the adapter synthesizes one so deltas still bind to the right assistant message.
Malformed JSON mid-stream raises a ChatStreamError surfaced through the same error UI.
See also
- See Echo adapter for an in-memory adapter for prototyping.
- See Adapters for the full adapter interface reference.
- See Building an adapter for writing your own from scratch.
- See Streaming for the chunk protocol reference.
- See Vercel AI SDK: Stream protocol for the UI Message Stream spec.