A generation hook tracks the request, the loading state, the error, and the result for one media call.
This connects a React component to an image route. It 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-openaiimport {
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, reset } = 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: {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'}
/>
))}
{result && <button onClick={reset}>Clear</button>}
</div>
)
}Type a prompt and click Generate. The button shows the loading state, then the image appears. Clear resets the hook.
Every generation hook does the same job:
You give the hook one transport:
The server half and the three transports are explained in Generations.
| Hook | Input | Result |
|---|---|---|
| useGenerateImage | ImageGenerateInput | ImageGenerationResult |
| useGenerateAudio | AudioGenerateInput | AudioGenerationResult |
| useGenerateSpeech | SpeechGenerateInput | TTSResult |
| useTranscription | TranscriptionGenerateInput | TranscriptionResult |
| useSummarize | SummarizeGenerateInput | SummarizationResult |
| useGenerateVideo | VideoGenerateInput | VideoGenerateResult |
| useGeneration | Your own TInput | Your own TResult |
Every hook returns generate, result, isLoading, error, status, stop, reset, and runId. The full tables are in Options and Return value.
Each section lists what generate() accepts. The linked guide shows the server route and the provider options.
Makes one or more images. Guide: Image Generation.
| Field | Type | Description |
|---|---|---|
| prompt | string | MediaPromptPart[] | What to draw (required). An array of text and image parts works on models that take reference images. |
| numberOfImages | number | How many images to make |
| size | string | Image size, for example "1024x1024" |
| modelOptions | Record<string, any> | Model-specific options |
Makes music or sound effects from a prompt. Guide: Audio Generation.
| Field | Type | Description |
|---|---|---|
| prompt | string | What the audio sounds like (required) |
| duration | number | Length in seconds |
| modelOptions | Record<string, any> | Model-specific options |
This example also shows a fetcher with Bring Your Own Key (BYOK). The user's key goes in x-byok-* headers. With a fetcher, spread headers from the second argument onto your request so the keys reach the server:
import { useGenerateAudio } from '@tanstack/ai-react'
import { defineByok, defaultByokStorage } from '@tanstack/ai-client/byok'
const byok = defineByok({ storage: defaultByokStorage() })
const threadId = 'audio'
export function AudioGenerator() {
const { generate, result, isLoading, error } = useGenerateAudio({
byok,
byokProvider: () => 'fal',
fetcher: (input, { threadId, signal, headers }) =>
fetch('/api/generate/audio', {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...headers },
body: JSON.stringify({ ...input, threadId }),
signal,
}),
threadId,
})
return (
<div>
<button
onClick={() => generate({ prompt: 'An upbeat electronic track' })}
disabled={isLoading}
>
{isLoading ? 'Generating...' : 'Generate'}
</button>
{error && <p role="alert">Error: {error.message}</p>}
{result?.audio.url && <audio src={result.audio.url} controls />}
</div>
)
}On the server, read the key from the header, then run generateAudio:
import { generateAudio, toServerSentEventsResponse } from '@tanstack/ai'
import { falAudio } from '@tanstack/ai-fal'
import { falByok } from '@tanstack/ai-fal/byok'
import { byokMissing, getByokKey } from '@tanstack/ai/byok/server'
export async function POST(request: Request) {
const apiKey = getByokKey(request, falByok)
if (!apiKey) return byokMissing(falByok)
const body: unknown = await request.json()
if (
typeof body !== 'object' ||
body === null ||
!('prompt' in body) ||
!('threadId' in body)
) {
return new Response('Bad request', { status: 400 })
}
const { prompt, threadId } = body
if (typeof prompt !== 'string' || typeof threadId !== 'string') {
return new Response('Bad request', { status: 400 })
}
const stream = generateAudio({
adapter: falAudio('fal-ai/minimax-music/v2.6', { apiKey }),
prompt,
threadId,
stream: true,
})
return toServerSentEventsResponse(stream)
}The fetcher posts the input and the threadId, so this route reads prompt and threadId from the top of the body. For defineByok and a UI to save keys, see Bring Your Own Key.
Turns text into spoken audio. Guide: Text-to-Speech.
| Field | Type | Description |
|---|---|---|
| text | string | The text to speak. Leave it out when you send turns. |
| turns | Array<TTSTurn> | Dialogue turns with more than one voice. Only for adapters that support more than one speaker. |
| timestamps | boolean | Ask for alignment and segments on the result. Only for adapters that support timestamps. |
| voice | string | The voice, for example "alloy" |
| format | 'mp3' | 'opus' | 'aac' | 'flac' | 'wav' | 'pcm' | Output audio format |
| speed | number | Speed, from 0.25 to 4.0 |
| modelOptions | Record<string, any> | Model-specific options |
TTSResult holds audio (base64) and format. It can also hold duration, contentType, alignment, and segments. To play it:
import { fetchServerSentEvents, useGenerateSpeech } from '@tanstack/ai-react'
export function SpeechPlayer() {
const { generate, result, isLoading } = useGenerateSpeech({
connection: fetchServerSentEvents('/api/generate/speech'),
threadId: 'speech',
})
return (
<div>
<button
onClick={() => generate({ text: 'Hello from TanStack AI!', voice: 'alloy' })}
disabled={isLoading}
>
Speak
</button>
{result && (
<audio
src={`data:audio/${result.format};base64,${result.audio}`}
controls
autoPlay
/>
)}
</div>
)
}Turns recorded audio into text. Guide: Transcription.
| Field | Type | Description |
|---|---|---|
| audio | string | File | Blob | ArrayBuffer | The audio (required). Over a connection, send a base64 string or a data URL. |
| language | string | Language in ISO-639-1 format, for example "en" |
| prompt | string | A hint that guides the transcription |
| responseFormat | 'json' | 'text' | 'srt' | 'verbose_json' | 'vtt' | Output format |
| modelOptions | Record<string, any> | Model-specific options |
TranscriptionResult holds text. It can also hold language, duration, segments, and words.
Makes a summary of long text. There is no separate guide, so the full example is here.
import { useState } from 'react'
import { fetchServerSentEvents, useSummarize } from '@tanstack/ai-react'
export function Summarizer() {
const [text, setText] = useState('')
const { generate, result, isLoading, error } = useSummarize({
connection: fetchServerSentEvents('/api/summarize'),
threadId: 'summary',
})
return (
<div>
<textarea
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="Paste text to summarize..."
rows={8}
/>
<button
onClick={() => generate({ text, style: 'bullet-points', maxLength: 200 })}
disabled={isLoading || !text.trim()}
>
{isLoading ? 'Summarizing...' : 'Summarize'}
</button>
{error && <p role="alert">Error: {error.message}</p>}
{result && <p>{result.summary}</p>}
</div>
)
}| Field | Type | Description |
|---|---|---|
| text | string | The text to summarize (required) |
| maxLength | number | Maximum length of the summary |
| style | 'bullet-points' | 'paragraph' | 'concise' | Summary style |
| focus | Array<string> | Topics to focus on |
| modelOptions | Record<string, any> | Model-specific options |
On the server, call summarize() with stream: true and return toServerSentEventsResponse(). generationParamsFromRequest has no summarize kind, so read the envelope yourself. The hook sends its input in forwardedProps, and threadId and runId at the top of the body:
import { summarize, toServerSentEventsResponse } from '@tanstack/ai'
import { openaiSummarize } from '@tanstack/ai-openai'
export async function POST(request: Request) {
const body: unknown = await request.json()
if (
typeof body !== 'object' ||
body === null ||
!('threadId' in body) ||
!('forwardedProps' in body)
) {
return new Response('Bad request', { status: 400 })
}
const { threadId, forwardedProps } = body
if (
typeof threadId !== 'string' ||
typeof forwardedProps !== 'object' ||
forwardedProps === null ||
!('text' in forwardedProps) ||
typeof forwardedProps.text !== 'string'
) {
return new Response('Bad request', { status: 400 })
}
const runId =
'runId' in body && typeof body.runId === 'string' ? body.runId : undefined
const stream = summarize({
adapter: openaiSummarize('gpt-5.5'),
text: forwardedProps.text,
style: 'bullet-points',
maxLength: 200,
threadId,
runId,
stream: true,
})
return toServerSentEventsResponse(stream)
}Makes a video. A video runs as a job: the server starts it, then polls the provider until it is done. Guide: Video Generation.
| Field | Type | Description |
|---|---|---|
| prompt | string | MediaPromptPart[] | What the video shows (required). An array of text and image parts works on models that take a start frame or references. |
| size | string | Video size. The format depends on the provider, for example "16:9" or "1280x720". |
| duration | number | Length in seconds |
| modelOptions | Record<string, any> | Model-specific options |
useGenerateVideo returns two more values, so you can show progress:
| Property | Type | Description |
|---|---|---|
| jobId | string | null | The job id. Set when the server creates the job. |
| videoStatus | VideoStatusInfo | null | The last status update: status, progress, and jobId |
interface VideoStatusInfo {
jobId: string
status: 'pending' | 'processing' | 'completed' | 'failed'
progress?: number // 0-100
url?: string // set when completed
error?: string // set when failed
}It also takes two more callbacks: onJobCreated(jobId) and onStatusUpdate(status).
Every framework has the same hooks with the same options. Only the names and the reactive values change.
| Generation | React, Vue, Solid, Octane | Svelte, Remix | Angular |
|---|---|---|---|
| Image | useGenerateImage | createGenerateImage | injectGenerateImage |
| Audio | useGenerateAudio | createGenerateAudio | injectGenerateAudio |
| Speech | useGenerateSpeech | createGenerateSpeech | injectGenerateSpeech |
| Transcription | useTranscription | createTranscription | injectTranscription |
| Summarization | useSummarize | createSummarize | injectSummarize |
| Video | useGenerateVideo | createGenerateVideo | injectGenerateVideo |
| Base (custom) | useGeneration | createGeneration | injectGeneration |
How to read the values in each framework:
Every package re-exports fetchServerSentEvents and fetchHttpStream from @tanstack/ai-client. For full samples, see the API pages for Vue, Solid, Svelte, Angular, Octane, and Remix.
Every hook takes these options:
| Option | Type | Description |
|---|---|---|
| connection | ConnectConnectionAdapter | Streaming transport (SSE, HTTP stream, or custom) |
| fetcher | (input, options) => Promise<Result | Response> | An async function, or a server function that returns an SSE Response. options holds signal, threadId, runId, and headers. |
| body | Record<string, any> | Extra fields sent with each connection request |
| threadId | string | Required. A stable name for this generation slot. |
| persistence | boolean | true restores the last run for threadId from the server when the component mounts |
| onResult | (result) => TOutput | null | void | React to the result, or change what is stored. See Result transforms. |
| onError | (error: Error) => void | Called when the run fails |
| onProgress | (progress: number, message?: string) => void | Progress updates, from 0 to 100 |
Options for special cases:
| Option | Type | Description |
|---|---|---|
| byok | ByokClient | A keyring. Keys go in x-byok-* headers, never in the body. |
| byokProvider | () => ProviderId | undefined | The provider slug. If it returns a slug, only that key is sent. Otherwise the hook uses body.provider. If no slug resolves, generate throws. |
| onChunk | (chunk: StreamChunk) => void | Called for each stream event. Only with connection. |
| hydrateGeneration | handler | Restores the last run when the connection has no restore handler, for example next to a fetcher |
| joinRun | handler | Re-attaches to a run that is still going after a reload. Without it, a restored running run shows as an error. |
| devtools | AIDevtoolsDisplayOptions | How the run shows in TanStack AI Devtools |
| Property | Type | Description |
|---|---|---|
| generate | (input) => Promise<void> | Start a generation |
| result | TOutput | null | The result, after any transform. null before the first result. |
| isLoading | boolean | true while a generation runs |
| error | Error | undefined | The current error, if any |
| status | GenerationClientState | 'idle', 'generating', 'success', or 'error' |
| stop | () => void | Abort the current generation |
| reset | () => void | Clear the result and the error, and go back to 'idle' |
| runId | string | null | The id of the run in progress, or null |
stop() only aborts the local stream. Work that already runs on the provider continues. To cancel or poll that provider job, send runId to your own endpoint.
onResult can change what the hook stores on result:
TypeScript infers the type of result from what onResult returns. You do not need a generic parameter.
import { fetchServerSentEvents, useGenerateSpeech } from '@tanstack/ai-react'
import type { TTSResult } from '@tanstack/ai'
export function SpeechPlayer() {
const { result } = useGenerateSpeech({
connection: fetchServerSentEvents('/api/generate/speech'),
threadId: 'speech',
onResult: (raw: TTSResult) => ({
audioUrl: `data:${raw.contentType};base64,${raw.audio}`,
duration: raw.duration,
}),
})
// result is { audioUrl: string; duration?: number } | null
}Every generation hook takes the same persistence option that useChat takes. With it, the status and the result of a long run come back after a page reload or a dropped connection.
The hook restores by threadId. For a generation, threadId names the slot that successive runs fill, for example product-7-hero. It does not name a conversation. See Id map. The setup is in Generation Persistence.
All the hooks above use useGeneration internally. Use it directly for a generation type that has no built-in hook:
import { fetchServerSentEvents, useGeneration } from '@tanstack/ai-react'
interface EmbeddingInput {
text: string
model?: string
}
interface EmbeddingResult {
embedding: Array<number>
model: string
usage: { totalTokens: number }
}
export function EmbeddingGenerator() {
const { generate, result, isLoading } = useGeneration<
EmbeddingInput,
EmbeddingResult
>({
connection: fetchServerSentEvents('/api/generate/embedding'),
threadId: 'generation',
})
return (
<div>
<button onClick={() => generate({ text: 'Hello world' })} disabled={isLoading}>
Generate embedding
</button>
{result && <p>Dimensions: {result.embedding.length}</p>}
</div>
)
}useGeneration takes the same options and returns the same values as the other hooks.