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 image element (npx assistant-ui@latest add image).
Info
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-2"),
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. Its optional id keys the part when it is non-empty and no other part of the same type in the message shares it, preserving component state when parts are reordered or updated with the same id. Every part with a missing or empty id, or an id shared by another part of the same type, is keyed by its type and position. 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 element (npx assistant-ui@latest add image, installed at components/assistant-ui/elements/image.tsx) 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 "@/components/assistant-ui/elements/image";
<>
<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.