- 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
Generation Hooks
A generation hook tracks the request, the loading state, the error, and the result for one media call.
Quick setup#
This connects a React component to an image route. It 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- Add a POST route at /api/generate/image. It reads OPENAI_API_KEY from the environment:
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)
}- Add the hook to 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, 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.
How it works#
Every generation hook does the same job:
- generate(input) sends the input to your server.
- While the server works, isLoading is true and status is 'generating'.
- When the result arrives, the hook stores it on result. If the server fails, the hook stores the failure on error.
You give the hook one transport:
- connection: a streaming transport, such as fetchServerSentEvents('/api/...'). Use it with an API route.
- fetcher: an async function, such as a TanStack Start server function. It can return the result, or an SSE Response.
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.
Hooks by media type#
Each section lists what generate() accepts. The linked guide shows the server route and the provider options.
useGenerateImage#
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 |
useGenerateAudio#
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.
useGenerateSpeech#
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>
)
}useTranscription#
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.
useSummarize#
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)
}useGenerateVideo#
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).
Framework variants#
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:
- Vue (@tanstack/ai-vue): values are DeepReadonly<ShallowRef<>>. Read them with .value in <script> and in <template>.
- Solid (@tanstack/ai-solid): values are accessors. Call them, for example result().
- Svelte (@tanstack/ai-svelte): values are Svelte 5 $state.
- Angular (@tanstack/ai-angular): values are read-only signals. Call them, for example result().
- Remix (@tanstack/ai-remix): pass the component handle as the first argument, for example createGenerateImage(handle, options).
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.
Advanced#
Options#
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 |
Return value#
| 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.
Result transforms#
onResult can change what the hook stores on result:
- Return a value that is not null: the hook stores that value.
- Return null: the hook keeps the previous result. Use this to filter results.
- Return nothing: the hook stores the raw 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
}Keep results after a reload#
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.
Custom generations with useGeneration#
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.