Skip to content
+

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.

MUI docs assistant

Assistant

Hi! Ask me anything about Material UI, MUI X Data Grid, Date Pickers, Charts, Tree View, Scheduler, or Chat.

Press Enter to start editing

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:

Mock stream

Assistant

Type anything — I stream the reply back word by word, like the AI SDK UI Message Stream.

Press Enter to start editing

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