generateImage() sends a text prompt to an image model and returns the images. useGenerateImage shows them in the browser.
You need a React app with a server route, and an OpenAI API key. This takes about 5 minutes.
npm i @tanstack/ai @tanstack/ai-react @tanstack/ai-openaipnpm add @tanstack/ai @tanstack/ai-react @tanstack/ai-openaiyarn add @tanstack/ai @tanstack/ai-react @tanstack/ai-openaibun add @tanstack/ai @tanstack/ai-react @tanstack/ai-openaiOPENAI_API_KEY=sk-...import {
generateImage,
generationParamsFromRequest,
toServerSentEventsResponse,
} from '@tanstack/ai'
import { openaiImage } from '@tanstack/ai-openai'
export async function POST(request: Request) {
const { input, threadId } = await generationParamsFromRequest('image', request)
if (typeof input.prompt !== 'string') {
return new Response('Send a text prompt.', { status: 400 })
}
const stream = generateImage({
adapter: openaiImage('gpt-image-2.5-flare'),
prompt: input.prompt,
threadId,
size: '1024x1024',
stream: true,
})
return toServerSentEventsResponse(stream)
}import { useState } from 'react'
import { fetchServerSentEvents, useGenerateImage } from '@tanstack/ai-react'
export function ImageGenerator() {
const [prompt, setPrompt] = useState('')
const { generate, result, isLoading, error } = useGenerateImage({
connection: fetchServerSentEvents('/api/generate/image'),
threadId: 'image-studio',
})
return (
<div>
<input
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
placeholder="Describe an image..."
/>
<button
onClick={() => generate({ prompt })}
disabled={isLoading || !prompt.trim()}
>
{isLoading ? 'Generating...' : 'Generate'}
</button>
{error && <p role="alert">{error.message}</p>}
{result?.images.map((img, i) => (
<img
key={i}
src={img.url || `data:image/png;base64,${img.b64Json}`}
alt={img.revisedPrompt || 'Generated image'}
/>
))}
</div>
)
}Type a prompt and click Generate. The image shows up below the button.
For a full React and TanStack Start walkthrough with OpenRouter, open Generate Image.
You can also call generateImage() without the hook, for example in a script:
import { generateImage } from '@tanstack/ai'
import { openaiImage } from '@tanstack/ai-openai'
const result = await generateImage({
adapter: openaiImage('gpt-image-2.5-flare'),
prompt: 'A beautiful sunset over mountains',
threadId: 'thread-1',
})
console.log(result.images[0]?.b64Json)The shared model for every generation (transports, events, errors) is in Generations. The hook options and return values are in Generation Hooks. To keep images after a reload, or after the provider URLs expire, add Generation Persistence.
| Option | Type | Description |
|---|---|---|
| adapter | ImageAdapter | The image adapter and model (required) |
| prompt | string | MediaPromptPart[] | What to draw (required). An array of text and image parts works on models that take reference images. See Image-conditioned generation. |
| numberOfImages | number | How many images to make |
| size | string | Image size. The format depends on the model. |
| modelOptions | object | Model-specific options, typed per model |
Change the adapter. The rest of the code stays the same.
| Provider | Adapter | Package | Newest model | Key |
|---|---|---|---|---|
| OpenAI | openaiImage | @tanstack/ai-openai | gpt-image-2.5-flare | OPENAI_API_KEY |
| Gemini | geminiImage | @tanstack/ai-gemini | gemini-nano-banana-2.1 | GOOGLE_API_KEY or GEMINI_API_KEY |
| BytePlus | byteplusImage | @tanstack/ai-byteplus | dola-seedream-5-0-pro-260628 | ARK_API_KEY or BYTEPLUS_API_KEY |
| Grok | grokImage | @tanstack/ai-grok | grok-imagine-image-2.0 | XAI_API_KEY |
| OpenRouter | openRouterImage | @tanstack/ai-openrouter | google/gemini-nano-banana-2.1 | OPENROUTER_API_KEY |
| fal.ai | falImage | @tanstack/ai-fal | Any of 600+ fal endpoints, for example fal-ai/flux/dev | FAL_KEY |
Anthropic has no image generation API.
Gemini image models (Nano Banana), such as gemini-nano-banana-2.1, use the Interactions API. size is "<aspectRatio>_<resolution>".
import { generateImage } from '@tanstack/ai'
import { geminiImage } from '@tanstack/ai-gemini'
const result = await generateImage({
adapter: geminiImage('gemini-nano-banana-2.1'),
prompt: 'A futuristic cityscape at night',
threadId: 'thread-1',
size: '16:9_4K',
})
console.log(result.images[0]?.b64Json)Seedream size takes a token (1K, 2K, 4K) or explicit pixels (2048x2048). Do not mix the two.
import { generateImage } from '@tanstack/ai'
import { byteplusImage } from '@tanstack/ai-byteplus'
const result = await generateImage({
adapter: byteplusImage('dola-seedream-5-0-pro-260628'),
prompt: 'A futuristic cityscape at night',
threadId: 'thread-1',
size: '2K',
modelOptions: { watermark: false },
})
console.log(result.images[0]?.url)Seedream works differently from the other providers in three ways:
See the BytePlus adapter for more.
You have an image from a Gemini native model. You want to change it, without a second upload of that image.
import { generateImage } from '@tanstack/ai'
import { geminiImage } from '@tanstack/ai-gemini'
const adapter = geminiImage('gemini-nano-banana-2.1')
const first = await generateImage({
adapter,
prompt: 'A red bicycle on a quiet street',
threadId: 'thread-1',
})
const edited = await generateImage({
adapter,
prompt: 'Make the bicycle blue',
threadId: 'thread-1',
modelOptions: { previous_interaction_id: first.id },
})edited.images holds the new picture. edited.id is the id for the next edit.
Rules for the edit:
The Interactions API is in Beta.
You want a new image that starts from an image you have: a restyle, an edit, or a mix of references. Pass prompt as an ordered array of text and image parts:
import { generateImage } from '@tanstack/ai'
import { openaiImage } from '@tanstack/ai-openai'
await generateImage({
adapter: openaiImage('gpt-image-2.5-flare'),
prompt: [
{ type: 'text', content: 'Turn this into a cinematic product photo' },
{
type: 'image',
source: { type: 'url', value: 'https://example.com/product.png' },
},
],
threadId: 'thread-1',
})What to know:
Masks, roles, URL rules, and per-provider limits are in Image Generation: Advanced.
The SDK sends your prompt text as you wrote it. It never adds or changes reference markers. To point at a specific input image, use the convention of the provider:
| Provider | Convention | Example |
|---|---|---|
| OpenAI (gpt-image) | Numbered prose, from the OpenAI prompting guide | "apply the style of image 2 to image 1" |
| FLUX.2 on fal / BFL | Numbered prose (BFL reads image N) | "subject from image 1, style from image 2" |
| Gemini (native models) | Describe the reference by its content or role | "using the attached fabric sample as the texture" |
| fal Kling / Seedance endpoints | @ tags, counted from 1 in input order | "Put @Image1 in the style of @Image2" |
| xAI grok-imagine | No syntax. xAI uses the request order. | "render the product in the style of the second image" |
To remember which part is "image 2", you can add a metadata.tag label. The SDK ignores it:
import type { MediaPromptPart } from '@tanstack/ai'
const productUrl = 'https://example.com/product.png'
const styleUrl = 'https://example.com/style.png'
const prompt: Array<MediaPromptPart> = [
{ type: 'text', content: 'Put @Image1 in the style of @Image2' },
{
type: 'image',
source: { type: 'url', value: productUrl },
metadata: { tag: 'product' },
},
{
type: 'image',
source: { type: 'url', value: styleUrl },
metadata: { tag: 'style' },
},
]Sizes per model, every model option, masks and role hints, URL inputs, provider limits, the result shape, cost tracking, errors, and explicit API keys are in Image Generation: Advanced.