Generate images in your backend and render them inline in an assistant-ui thread.
Image generation needs no dedicated primitive. Generate the image wherever you already run model calls (a route handler or a tool), store the result as an ImageMessagePart, and render it with the @assistant-ui/ui Image component.
This covers non-streaming generation, rendering, and actions. Streaming partial images and multi-image galleries are out of scope.
Generate in your backend
Call your provider from a server route. With the AI SDK that is generateImage; return the image as a data URI (or an object-store URL) plus any provider metadata you want to keep.
// app/api/image/route.ts
import { generateImage } from "ai";
import { openai } from "@ai-sdk/openai";
export async function POST(req: Request) {
const { prompt } = await req.json();
const result = await generateImage({
model: openai.image("gpt-image-1"),
prompt,
});
const revisedPrompt = (
result.providerMetadata as
| Record<string, Record<string, unknown>>
| undefined
)?.openai?.revisedPrompt;
return Response.json({
image: `data:${result.image.mediaType};base64,${result.image.base64}`,
mimeType: result.image.mediaType,
...(typeof revisedPrompt === "string" && { revisedPrompt }),
});
}The model provider is irrelevant to rendering; swap openai.image(...) for any AI SDK image model.
Store it as an ImageMessagePart
An ImageMessagePart only needs image (a data: URI, an https:// URL, or a blob: URL) plus an optional filename. Keep any provenance you want to display, the prompt, a revised prompt, a model id, in your own component state or in message metadata; the part itself stays minimal.
const part: ImageMessagePart = {
type: "image",
image: result.image, // data:, https://, or blob: URL
};Render with the Image component
The Image component in @assistant-ui/ui handles the render states for you:
- Running (
status.type === "running") renders a spinner. - Content filter (
status.type === "incomplete"withreason: "content-filter") renders an error card with no<img src>. - Complete renders a zoomable
<img>with optionalImage.Actions.
Image.Actions provides download and copy buttons, plus a regenerate button when you pass an onRegenerate callback. Wire it to the same generation flow you used above; debounce, rate limiting, and confirmation are your call.
import { Image } from "@assistant-ui/ui";
<>
<Image {...imagePart} />
<Image.Actions part={imagePart} onRegenerate={() => regenerate(prompt)} />
</>;Example
A complete Next.js example (with a mock fallback when OPENAI_API_KEY is unset) lives in examples/with-image-generation.