Assistant UI
pageai-sdk-backend-resumablesource: ok · hit
AI SDK Resumable Backend
AI SDK route handlers with resumable stream context, so a run survives a reload.
npx shadcn add "https://r.assistant-ui.com/ai-sdk-backend-resumable.json"free
No selection tags yet — empty means no evidence, not lack of support.
Dependencies
Bundle weight · mediumframeworknext
Files3 · source cached 10/8/2026, 7:40:04 PM
app/api/chat/route.ts
import { openai } from "@ai-sdk/openai";
import { frontendTools } from "@assistant-ui/ai-sdk";
import {
streamText,
convertToModelMessages,
type UIMessage,
type JSONSchema7,
UI_MESSAGE_STREAM_HEADERS,
} from "ai";
import { RESUMABLE_STREAM_ID_HEADER } from "assistant-stream/resumable";
import { resumableContext } from "@/lib/resumable-context";
export const maxDuration = 60;
export async function POST(req: Request) {
const {
messages,
system,
tools,
}: {
messages: UIMessage[];
system?: string;
tools?: Record<string, { description?: string; parameters: JSONSchema7 }>;
} = await req.json();
const streamId = crypto.randomUUID();
const result = streamText({
model: openai("gpt-6-luna"),
messages: await convertToModelMessages(messages),
tools: {
...frontendTools(tools ?? {}),
},
...(system === undefined ? {} : { system }),
});
const body = result.toUIMessageStreamResponse().body;
if (!body) {
throw new Error("UI message stream response has no body");
}
const stream = await resumableContext.run(streamId, () => body);
return new Response(stream, {
headers: {
...UI_MESSAGE_STREAM_HEADERS,
[RESUMABLE_STREAM_ID_HEADER]: streamId,
},
});
}
app/api/chat/resume/[streamId]/route.ts
import { UI_MESSAGE_STREAM_HEADERS } from "ai";
import { RESUMABLE_STREAM_ID_HEADER } from "assistant-stream/resumable";
import { resumableContext } from "@/lib/resumable-context";
export const maxDuration = 60;
// Resume access is authenticated only by stream id; scope it to the signed-in user in production (see /docs/guides/resumable-stream-deployment).
export async function GET(
_req: Request,
context: { params: Promise<{ streamId: string }> },
) {
const { streamId } = await context.params;
const stream = await resumableContext.resume(streamId);
if (!stream) {
return new Response(JSON.stringify({ error: "stream not found" }), {
status: 404,
headers: { "Content-Type": "application/json" },
});
}
return new Response(stream, {
headers: {
...UI_MESSAGE_STREAM_HEADERS,
[RESUMABLE_STREAM_ID_HEADER]: streamId,
},
});
}
lib/resumable-context.ts
import {
createInMemoryResumableStreamStore,
createResumableStreamContext,
} from "assistant-stream/resumable";
import { after } from "next/server";
const store = createInMemoryResumableStreamStore();
export const resumableContext = createResumableStreamContext({
store,
waitUntil: (promise) => after(promise),
});