Skip to content
Media

Generations

A generation is one request and one result: an image, a voice clip, a transcript, or a video. It does not keep a conversation.

Every generation uses the same three parts. When you know how to make one image, you know how to make all of them.

Generate your first image

You need a React app with a server route, and an OpenAI API key. This takes about 5 minutes.

  1. Install the packages:
shell
npm i @tanstack/ai @tanstack/ai-react @tanstack/ai-openai
  1. Put your key in .env:
shell
OPENAI_API_KEY=sk-...
  1. Add a POST route at /api/generate/image:
ts
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,
    stream: true,
  })
  return toServerSentEventsResponse(stream)
}
  1. Call the route from a component:
tsx
import { fetchServerSentEvents, useGenerateImage } from '@tanstack/ai-react'

export function ImageGenerator() {
  const { generate, result, isLoading, error } = useGenerateImage({
    connection: fetchServerSentEvents('/api/generate/image'),
    threadId: 'image-studio',
  })

  return (
    <div>
      <button
        onClick={() => generate({ prompt: 'A sunset over mountains' })}
        disabled={isLoading}
      >
        {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="Generated image"
        />
      ))}
    </div>
  )
}

Click the button. The image shows up below it.

For a full React and TanStack Start walkthrough with OpenRouter, open Generate Image.

How it works

The loop has three parts:

  • The server function. generateImage() calls the provider. With stream: true, it returns a stream of events instead of a plain result.
  • The response helper. toServerSentEventsResponse() sends those events to the browser as Server-Sent Events (SSE).
  • The hook. useGenerateImage() posts the input, reads the events, and gives you result, isLoading, and error.

To make a different media type, swap the server function and the hook for another pair from the table. Nothing else changes.

ActivityServer functionReact hookGuide
Image generationgenerateImage()useGenerateImage()Image Generation
Audio generationgenerateAudio()useGenerateAudio()Audio Generation
Text-to-speechgenerateSpeech()useGenerateSpeech()Text-to-Speech
TranscriptiongenerateTranscription()useTranscription()Transcription
Voice creationgenerateVoice()-Voice Creation
Summarizationsummarize()useSummarize()-
Video generationgenerateVideo()useGenerateVideo()Video Generation
Live generationgenerateLiveVideo()-Live Generation
World generationgenerateWorld()-World Generation

Three types work a little differently:

  • Video runs as a job. The server starts the job and polls it until the video is ready. The hook also gives you the job status. See Video Generation.
  • Live and world generation return a session token. The browser uses the token to connect to the provider. See Live Generation and World Generation.

The hooks for Vue, Solid, Svelte, Angular, and the other frameworks are in Generation Hooks. To keep a result after a page reload, add Generation Persistence.

Pick a transport

The quick setup uses a streaming route. You can also call a server function:

TransportUse it whenHow
Streaming routeYou have an API route. This is the default.stream: true and toServerSentEventsResponse() on the server. connection: on the hook.
DirectYou call a server function and want plain JSON back. No streaming.Return the result from the function. fetcher: on the hook.
Server function that streamsYou use TanStack Start server functions and want typed input and streaming.Return toServerSentEventsResponse(...) from the function. fetcher: on the hook.

On TanStack Start, the third transport gives you both: the input is typed and the hook still reads a stream. All three are written out in Transports in full.

Advanced

Transports in full

Streaming route (connection)

The server passes stream: true to the generation function and sends the events as SSE. The hook reads them with fetchServerSentEvents(). This is the quick setup above.

The hook sends its input inside an AG-UI request body. generationParamsFromRequest(kind, request) reads that body and returns { input, threadId, runId }. The kind is one of 'image', 'audio', 'tts', 'voice', 'video', 'transcription', 'world', or 'liveVideo'.

Direct (fetcher)

The hook calls a server function and gets the result back as JSON. There is no stream.

Server:

ts
import { createServerFn } from '@tanstack/react-start'
import { generateImage } from '@tanstack/ai'
import { openaiImage } from '@tanstack/ai-openai'

export const generateImageFn = createServerFn({ method: 'POST' })
  .inputValidator((data: { prompt: string; threadId: string }) => data)
  .handler(async ({ data }) => {
    return generateImage({
      adapter: openaiImage('gpt-image-2.5-flare'),
      prompt: data.prompt,
      threadId: data.threadId,
    })
  })

Client:

tsx
import { useGenerateImage } from '@tanstack/ai-react'
import { generateImageFn } from '../lib/server-functions'

const threadId = 'image-studio'

export function ImageGenerator() {
  const { generate, result, isLoading } = useGenerateImage({
    fetcher: (input, { threadId }) =>
      generateImageFn({ data: { ...input, threadId } }),
    threadId,
  })
  // Render the same way as in the quick setup.
}

Server function that streams (fetcher + Response)

The server function returns an SSE Response. When a fetcher returns a Response, the hook reads it as an SSE stream. You get typed input from the fetcher and streaming from the response.

Server:

ts
import { createServerFn } from '@tanstack/react-start'
import { generateImage, toServerSentEventsResponse } from '@tanstack/ai'
import { openaiImage } from '@tanstack/ai-openai'

export const generateImageStreamFn = createServerFn({ method: 'POST' })
  .inputValidator((data: { prompt: string; threadId: string }) => data)
  .handler(({ data }) => {
    return toServerSentEventsResponse(
      generateImage({
        adapter: openaiImage('gpt-image-2.5-flare'),
        prompt: data.prompt,
        threadId: data.threadId,
        stream: true,
      }),
    )
  })

Client:

tsx
import { useGenerateImage } from '@tanstack/ai-react'
import { generateImageStreamFn } from '../lib/server-functions'

const threadId = 'image-studio'

export function ImageGenerator() {
  const { generate, result, isLoading } = useGenerateImage({
    fetcher: (input, { threadId }) =>
      generateImageStreamFn({ data: { ...input, threadId } }),
    threadId,
  })
  // Render the same way as in the quick setup.
}

How the client closes the response:

  • If the client stops reading before the response ends, it cancels the response body.
  • A RUN_ERROR event also closes the unfinished response.
  • Custom cancellation hooks do not delay the error or loading state on the client.
  • The client releases the reader lock, even if cancellation fails or stays pending.

How streaming works

With stream: true, every generation function returns an async iterable of StreamChunk events instead of a plain result. A successful run sends these events:

plaintext
1. RUN_STARTED          -> the hook sets status to 'generating'
2. CUSTOM               -> the hook stores the result
   name: 'generation:result'
   value: <your result>
3. RUN_FINISHED         -> the hook sets status to 'success'

If the function throws, the run sends RUN_ERROR instead:

plaintext
1. RUN_STARTED          -> the hook sets status to 'generating'
2. RUN_ERROR            -> the hook sets error, and status to 'error'
   message: '...'

If Generation Persistence stores the generated files, a CUSTOM event named generation:artifacts comes before generation:result. It carries the stored file references.

Chat streaming uses the same event protocol. As a result, toServerSentEventsResponse() and fetchServerSentEvents() work for chat and for generations.

Show errors

The hook puts a RUN_ERROR on error and sets status to 'error'. Use onError to log it, and show error.message in your UI:

tsx
import { fetchServerSentEvents, useGenerateImage } from '@tanstack/ai-react'

export function ImageGenerator() {
  const { generate, result, error, status } = useGenerateImage({
    connection: fetchServerSentEvents('/api/generate/image'),
    threadId: 'image-studio',
    onError: (err) => console.error('Generation failed:', err.message),
  })

  return (
    <div>
      <button onClick={() => generate({ prompt: 'A sunset over mountains' })}>
        Generate
      </button>
      {status === 'error' && <p role="alert">Error: {error?.message}</p>}
      {result?.images.map((img, i) => (
        <img
          key={i}
          src={img.url || `data:image/png;base64,${img.b64Json}`}
          alt="Generated image"
        />
      ))}
    </div>
  )
}

Hook options and return values

All generation hooks share one set of options and one return shape. The full tables, result transforms, and the framework variants are in Generation Hooks.

Architecture

mermaid
flowchart TB
  subgraph Server ["Server"]
    direction TB
    activities["generateImage({ ..., stream: true })
generateSpeech({ ..., stream: true })
generateTranscription({ ..., stream: true })
summarize({ ..., stream: true })
generateVideo({ ..., stream: true })"]
    transport["toServerSentEventsResponse()"]
    activities --> transport
  end

  transport -- "StreamChunks via SSE
RUN_STARTED, generation:result, RUN_FINISHED" --> adapter

  subgraph Client ["Client"]
    direction TB
    adapter["fetchServerSentEvents('/api/...')"]
    gc["GenerationClient
(state machine)"]
    hooks["Framework hooks
useGenerateImage(), useGenerateSpeech(),
useGenerateVideo(), useSummarize(),
useTranscription()"]
    adapter --> gc
    gc -- "result, isLoading, error, status" --> hooks
  end

On the server, every generation is an async function that returns a result. With stream: true, the same function returns a StreamChunk iterable. The client already knows how to read that iterable, because chat uses it too.