[chat-headless] Add built-in chat adapters#22479
Conversation
Two built-in adapters that ship with @mui/x-chat-headless: a simple echo adapter for examples and an AI SDK adapter that bridges Vercel AI SDK UIMessage streams into ChatMessageChunk events. Wire both through the headless core barrel so they can be imported from '@mui/x-chat-headless' and '@mui/x-chat-headless/core'.
Deploy previewhttps://deploy-preview-22479--material-ui-x.netlify.app/ Bundle size
Check out the code infra dashboard for more information about this PR. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 59f5389856
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const payload = trimmed.startsWith('data:') ? trimmed.slice(5).trimStart() : trimmed; | ||
| if (payload === '[DONE]') { | ||
| return null; | ||
| } | ||
| return JSON.parse(payload); |
There was a problem hiding this comment.
Ignore non-data SSE fields before JSON parsing
parseStreamLine says it should ignore SSE field lines like event: and id:, but the implementation only special-cases data: and otherwise sends the full line to JSON.parse. When an upstream SSE stream includes standard non-data fields (or reconnect metadata), this path throws, and convertToChatStream turns it into a ChatStreamError, terminating an otherwise valid stream.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 849b199. parseStreamLine now ignores non-data SSE field lines such as event:, id:, and retry: before JSON parsing, with SSE coverage added.
There was a problem hiding this comment.
Pull request overview
Adds built-in adapter implementations to @mui/x-chat-headless and wires them into the headless/core/adapters entry points, with an additional re-export from @mui/x-chat intended to make quickstart usage easier.
Changes:
- Added
createEchoAdapter(deterministic local adapter) andcreateAiSdkAdapter(Vercel AI SDK bridge) to@mui/x-chat-headless. - Exported the new adapter APIs from
@mui/x-chat-headlessentry points (src/index.ts,src/core/index.ts,src/adapters/index.ts). - Updated
@mui/x-chatexports + export manifest to includecreateEchoAdapter.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| scripts/x-chat.exports.json | Adds createEchoAdapter + options to the generated @mui/x-chat export manifest. |
| packages/x-chat/src/index.ts | Re-exports createEchoAdapter from @mui/x-chat-headless for convenience. |
| packages/x-chat-headless/src/index.ts | Exposes new adapters/types from the main headless entry point. |
| packages/x-chat-headless/src/core/index.ts | Exposes new adapters/types from the “core” headless entry point. |
| packages/x-chat-headless/src/adapters/index.ts | Exposes adapters/types from the adapters barrel. |
| packages/x-chat-headless/src/adapters/createEchoAdapter.ts | Introduces the echo adapter implementation. |
| packages/x-chat-headless/src/adapters/createAiSdkAdapter.ts | Introduces the AI SDK adapter implementation (stream + useChat() integration). |
| packages/x-chat-headless/src/adapters/createAiSdkAdapter.test.ts | Adds unit coverage for AI SDK adapter behaviors (object stream, NDJSON/SSE decoding, errors, cancellation, etc.). |
Comments suppressed due to low confidence (1)
packages/x-chat-headless/src/adapters/createEchoAdapter.ts:55
- If
signalis already aborted beforestart()runs, the abort event listener won't fire and the stream won't close untildelayMselapses (it only checkssignal.abortedinside the timeout). Consider checkingsignal.abortedup-front instart()and closing immediately to avoid artificial delay on cancellation.
return new ReadableStream<ChatMessageChunk>({
start(controller) {
const timer = setTimeout(() => {
if (signal.aborted) {
controller.close();
return;
}
controller.enqueue({ type: 'start', messageId: replyId });
controller.enqueue({ type: 'text-start', id: partId });
controller.enqueue({ type: 'text-delta', id: partId, delta: reply });
controller.enqueue({ type: 'text-end', id: partId });
controller.enqueue({ type: 'finish', messageId: replyId, finishReason: 'stop' });
controller.close();
}, delayMs);
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // ─── Built-in adapters (re-exported for quickstart convenience) ─────────────── | ||
| export { createEchoAdapter } from '@mui/x-chat-headless'; | ||
| export type { CreateEchoAdapterOptions } from '@mui/x-chat-headless'; |
There was a problem hiding this comment.
Fixed in 849b199. createAiSdkAdapter and its public structural types are now re-exported from @mui/x-chat, so the package surface matches the PR description.
| { "name": "ChatVariant", "kind": "TypeAlias" }, | ||
| { "name": "createEchoAdapter", "kind": "Function" }, | ||
| { "name": "CreateEchoAdapterOptions", "kind": "Interface" }, |
There was a problem hiding this comment.
Fixed in 849b199. scripts/x-chat.exports.json now includes createAiSdkAdapter and its public types for the @mui/x-chat export surface.
| return new ReadableStream<ChatMessageChunk>({ | ||
| start(controller) { | ||
| const timer = setTimeout(() => { | ||
| if (signal.aborted) { | ||
| controller.close(); | ||
| return; | ||
| } | ||
| controller.enqueue({ type: 'start', messageId: replyId }); | ||
| controller.enqueue({ type: 'text-start', id: partId }); | ||
| controller.enqueue({ type: 'text-delta', id: partId, delta: reply }); | ||
| controller.enqueue({ type: 'text-end', id: partId }); | ||
| controller.enqueue({ type: 'finish', messageId: replyId, finishReason: 'stop' }); | ||
| controller.close(); | ||
| }, delayMs); | ||
|
|
||
| signal.addEventListener( | ||
| 'abort', | ||
| () => { | ||
| clearTimeout(timer); | ||
| try { | ||
| controller.close(); | ||
| } catch { | ||
| // already closed | ||
| } | ||
| }, | ||
| { once: true }, | ||
| ); | ||
| }, |
There was a problem hiding this comment.
Fixed in 849b199. createEchoAdapter now clears the pending timer on abort/cancel and implements stream cancel() cleanup so it does not enqueue after cancellation.
| /** | ||
| * A minimal in-memory `ChatAdapter` that echoes the user's last message. | ||
| * | ||
| * Intended for quickstart demos, prototyping, and tests — not for production. | ||
| * The reply is emitted as a single text chunk after `delayMs` (no simulated | ||
| * token-by-token streaming). | ||
| */ | ||
| export function createEchoAdapter(options: CreateEchoAdapterOptions = {}): ChatAdapter { | ||
| const respond = options.respond ?? defaultRespond; | ||
| const delayMs = options.delayMs ?? 400; | ||
|
|
||
| return { | ||
| async sendMessage({ message, signal }) { | ||
| const text = message.parts.map((part) => (part.type === 'text' ? part.text : '')).join(''); |
There was a problem hiding this comment.
Fixed in 849b199. Added createEchoAdapter unit tests covering delayed emission, abort-before-delay, and stream cancellation cleanup.
| // SSE framing prepends each event with `data: `. Strip it; ignore comments | ||
| // (`:` prefix) and SSE field lines we don't care about (`event:`, `id:`, …). | ||
| const trimmed = rawLine.trim(); | ||
| if (trimmed.length === 0 || trimmed.startsWith(':')) { | ||
| return null; | ||
| } | ||
| const payload = trimmed.startsWith('data:') ? trimmed.slice(5).trimStart() : trimmed; | ||
| if (payload === '[DONE]') { | ||
| return null; | ||
| } | ||
| return JSON.parse(payload); |
There was a problem hiding this comment.
Fixed in 849b199. The SSE parser now skips non-data SSE metadata fields before parsing, and the test stream includes event:, id:, retry:, and comments.
Summary
createEchoAdapterandcreateAiSdkAdapter.Details
createEchoAdapterprovides a local deterministic adapter for simple chat flows, demos, and smoke usage.createAiSdkAdapterbridges the AI SDK integration into the X Chat headless adapter contract.Validation
git diff --check upstream/master...x-chat-headless-adapterspnpm --filter "@mui/x-chat-headless" run typescriptpnpm test:unit --project "x-chat*" --runBatch merge structure
Merge top to bottom.
#22478can merge independently, but it should land before#22483because#22483assumes the new docs IA.