- MCP Server ToolsUpdated
- Managed MCP with chat()Updated
- Manual MCPUpdated
- MCP Type GenerationUpdated
- MCP Client InputUpdated
- MCP Server Tools: AdvancedNew
- MCP EventsNew
- MCP AppsUpdated
- Serve Tools over HTTPUpdated
- Serve Resources and PromptsUpdated
- Ask for InputUpdated
- Draft Text from a ToolUpdated
- MCP Server TasksUpdated
- MCP Server AuthUpdated
- MCP Server SessionsUpdated
- MCP Server on stdioUpdated
Persistence (Advanced)
- GenerationsUpdated
- Realtime Voice ChatUpdated
- Text-to-SpeechUpdated
- TranscriptionUpdated
- Transcription: AdvancedNew
- Voice CreationUpdated
- Audio RecordingUpdated
- Audio GenerationUpdated
- Image GenerationUpdated
- Image Generation: AdvancedNew
- Video GenerationUpdated
- Video Generation: AdvancedNew
- Live GenerationUpdated
- World GenerationUpdated
- Generation HooksUpdated
- OverviewUpdated
- Quick StartUpdated
- ProvidersUpdated
- HarnessesUpdated
- Harness AuthUpdated
- WorkspaceUpdated
- ToolsUpdated
- PolicyUpdated
- Lifecycle & SnapshotsUpdated
- Portable SnapshotsUpdated
- Keep Files After ReloadUpdated
- Save a Named VersionUpdated
- Branch From a VersionUpdated
- Send a Frozen FileUpdated
- Let the Agent Save and ForkUpdated
- Pick Which Files to KeepUpdated
- What a Snapshot StoresUpdated
- Instance DurabilityUpdated
- Durable RunsUpdated
- EventsUpdated
Sandboxes (Advanced)
- Runtime ContextUpdated
- Bring Your Own KeyUpdated
- Debug LoggingUpdated
- Multimodal ContentUpdated
- Files APIUpdated
- Per-Model Type SafetyUpdated
- Runtime Adapter SwitchingUpdated
- Tree-ShakingUpdated
- Extend AdapterUpdated
- Extend Adapter: AdvancedNew
- Typed Pre-Configured OptionsUpdated
- Test with a Fake ModelUpdated
- Prompt CachingUpdated
- Mid-Conversation ChangesUpdated
- Approval Flow ProcessingUpdated
Image Generation
generateImage() sends a text prompt to an image model and returns the images. useGenerateImage shows them in the browser.
Generate an image#
You need a React app with a server route, and an OpenAI API key. This takes about 5 minutes.
- Install the packages:
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-openai- Put your key in .env:
OPENAI_API_KEY=sk-...- Add a POST route at /api/generate/image:
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)
}- Call the route from a component:
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.
How it works#
- generateImage() sends the prompt to the model that the adapter names.
- The result has an images array. Each image has a url, or base64 bytes in b64Json, depending on the provider.
- With stream: true, the route sends the result to the hook as Server-Sent Events. The hook stores it on result.
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.
Options#
| 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 |
Pick a provider#
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#
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)BytePlus#
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:
- watermark is true by default. BytePlus puts "AI generated" in the corner. Set it to false to remove it.
- numberOfImages is a maximum. Seedream has no n parameter. A request for four images can return two.
- Image URLs expire after 24 hours. For inline bytes, pass response_format: 'b64_json' in modelOptions.
See the BytePlus adapter for more.
Edit a Gemini native image#
You have an image from a Gemini native model. You want to change it, without a second upload of that image.
- Generate the first image.
- Call generateImage again. Set previous_interaction_id to first.id, and write only the change in prompt.
- Read edited.images for the new picture.
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:
- Leave store unset. The API default is true. If you set store to false, a later call cannot use that id.
- A free tier keeps a stored interaction for 1 day. A paid tier keeps it for 55 days.
- If the edit needs a new reference image, add that image part on this call. Do not send the image from first again.
The Interactions API is in Beta.
Image-conditioned generation#
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:
- Part order matters. Gemini native models and OpenRouter get the parts in the same order, so text can refer to the image next to it. OpenAI, fal, and xAI take the image parts out and join the text parts into one prompt.
- Types check the model. An image part on a model with no image input is a type error.
- Unsupported adapters throw. An adapter that cannot take image inputs throws a runtime error. It does not drop the images.
Masks, roles, URL rules, and per-provider limits are in Image Generation: Advanced.
Referencing images from your prompt#
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' },
},
]Advanced#
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.