Skip to content
Adapters

Reactor

The Reactor adapter opens a live world or live video session. The server mints a session token. The browser plays and steers the stream. It has no chat.

  • reactorWorld() with generateWorld(): a world the user can look at and move through.
  • reactorVideo() with generateLiveVideo(): a live video session.

1. Install

The server needs @tanstack/ai-reactor. The browser needs @reactor-team/js-sdk to connect and play the stream.

shell
npm i @tanstack/ai @tanstack/ai-reactor @reactor-team/js-sdk

2. Set the API key

Create a key in the Reactor dashboard. Keys start with rk_. Put the key in the environment of your server:

shell
REACTOR_API_KEY=rk_...

reactorWorld and reactorVideo read REACTOR_API_KEY. To pass the key in code, use createReactorWorld or createReactorVideo:

ts
import { createReactorWorld } from '@tanstack/ai-reactor'

const adapter = createReactorWorld('visko-orbis-stable', process.env.REACTOR_API_KEY!)

3. Mint a world session on the server

Add a POST route at /api/world:

ts
import { generateWorld } from '@tanstack/ai'
import { reactorWorld } from '@tanstack/ai-reactor'

export async function POST() {
  const world = await generateWorld({
    adapter: reactorWorld('visko-orbis-stable'),
    prompt: 'A neon cyberpunk city at night, slow aerial drift',
    threadId: 'thread-1',
  })

  return Response.json({
    token: world.token,
    model: world.model,
    prompt: world.prompt,
    expiresAt: world.expiresAt,
  })
}

The token works for this one model only. The API key stays on the server. world.model is the Reactor connect slug, here reactor/visko-orbis-stable.

4. Play the world in the browser

Connect with the token and play the stream in a <video> element:

ts
import { Reactor } from '@reactor-team/js-sdk'

const video = document.querySelector('video')
if (!video) {
  throw new Error('Missing video element')
}

const world = await fetch('/api/world', { method: 'POST' }).then(async (response) => {
  if (!response.ok) {
    throw new Error('World session failed')
  }
  return response.json()
})

const token = typeof world.token === 'string' ? world.token : ''
const model = typeof world.model === 'string' ? world.model : ''
const prompt = typeof world.prompt === 'string' ? world.prompt : ''
if (token.length === 0 || model.length === 0 || prompt.length === 0) {
  throw new Error('World payload is incomplete')
}

const reactor = new Reactor({ modelName: model })

reactor.on('trackReceived', (name, _track, stream) => {
  if (name !== 'main_video') return
  video.muted = true
  video.srcObject = stream
  void video.play()
})

await reactor.connect(token)
await reactor.sendCommand('set_resolution', { resolution: '1080p' })
await reactor.sendCommand('set_prompt', { prompt })
await reactor.sendCommand('start', {})

The video element now plays a live world. To steer it, send a new set_prompt. For a version that attaches the stream again when a track unmutes, see World Generation.

A full working app is in examples/ts-react-media. Open the World or Live tab.

Live video

Reactor video is a live stream, not a finished file. generateLiveVideo() returns a token, the same as generateWorld():

ts
import { generateLiveVideo } from '@tanstack/ai'
import { reactorVideo } from '@tanstack/ai-reactor'

const live = await generateLiveVideo({
  adapter: reactorVideo('helios'),
  prompt: 'A neon cyberpunk city at night, slow aerial drift',
  threadId: 'thread-1',
})

Send live.token, live.model, and live.prompt to the browser. Then connect as in step 4, or see Live Generation.

helios works with both adapters:

  • Use generateWorld() for a world that the user moves through.
  • Use generateLiveVideo() for a video session.

Models and options

Pass the id as a string literal, so TypeScript can narrow the options. The result model is the connect slug. Pass it to new Reactor({ modelName }).

World models

IdConnect slug
visko-orbis-stablereactor/visko-orbis-stable
visko-orbis-dynamicreactor/visko-orbis-dynamic
happy-oyster-adventurereactor/happy-oyster-adventure
happy-oyster-directorreactor/happy-oyster-director
lingbot-world-2reactor/lingbot-world-2
lingbotreactor/lingbot
heliosreactor/helios

Happy Oyster (happy-oyster-adventure, happy-oyster-director) uses createWorld and startTravel after connect.

Live video models

IdConnect slug
heliosreactor/helios
fast-h3reactor/fast-h3
longlive-v2reactor/longlive-v2
ltx2reactor/ltx2

The package exports the lists as REACTOR_WORLD_MODELS, REACTOR_VIDEO_MODELS, REACTOR_WORLD_SLUGS, and REACTOR_VIDEO_SLUGS.

Browser session options

generateWorld() and generateLiveVideo() send only the prompt when they mint the token. They do not send a resolution or a seed image. Keep these options in client state, and send them with sendCommand after connect. Orbis reads them on the next start:

ts
import { Reactor } from '@reactor-team/js-sdk'

const reactor = new Reactor({ modelName: 'reactor/visko-orbis-stable' })
await reactor.sendCommand('set_resolution', { resolution: '2k' })
await reactor.sendCommand('set_seed', { seed: 42 })
await reactor.sendCommand('set_audio_enabled', { audio_enabled: true })
OptionCommandMeaning
resolutionset_resolutionThe delivery tier: 1080p, 2k, or 4k
seedset_seedThe RNG seed for the next run
audioEnabledset_audio_enabledWhen false, Reactor does not compute audio
audioPromptset_audio_promptA description of the sound, or "" for audio that follows the picture

Advanced

Start from a seed image

Send seed images from the browser after connect. Pass a File from <input type="file">. The SDK uploads it and returns a FileRef. Do not send base64.

LingBot starts from a seed image. start also needs set_prompt. Write the prompt so that it describes what the image shows:

ts
import { Reactor } from '@reactor-team/js-sdk'

const reactor = new Reactor({ modelName: 'reactor/lingbot' })
const picker = document.querySelector('input[type="file"]')
if (!(picker instanceof HTMLInputElement)) {
  throw new Error('Pick a seed image')
}
const file = picker.files?.[0]
if (file === undefined) {
  throw new Error('Pick a seed image')
}
const image = await reactor.uploadFile(file)
await reactor.sendCommand('set_image', { image })
await reactor.sendCommand('set_prompt', {
  prompt: 'A narrow stone alley at night, wet cobbles, one lit shop window.',
})
await reactor.sendCommand('start', {})

Other models:

  • Helios can take the same File with set_conditioning, so the prompt and the image arrive together.
  • Orbis takes an optional seed image. Send set_image before set_prompt and start. Use a 16:9 image, because Orbis resizes other shapes without a crop. A new image during a run has no effect until reset.

Bring your own key

The browser can save a Reactor key and send it in the x-byok-reactor header. The server reads that header first, then REACTOR_API_KEY:

ts
import { generateWorld } from '@tanstack/ai'
import { byokMissing, getByokKey } from '@tanstack/ai/byok/server'
import { reactorWorld } from '@tanstack/ai-reactor'
import { reactorByok } from '@tanstack/ai-reactor/byok'

export async function POST(request: Request) {
  const apiKey = getByokKey(request, reactorByok)
  if (!apiKey) return byokMissing(reactorByok)

  const world = await generateWorld({
    adapter: reactorWorld('visko-orbis-stable', { apiKey }),
    prompt: 'A neon cyberpunk city at night, slow aerial drift',
    threadId: 'thread-1',
  })

  return Response.json({
    token: world.token,
    model: world.model,
    prompt: world.prompt,
    expiresAt: world.expiresAt,
  })
}

Do not put the API key in the JSON body. For the browser side, see Let users bring their own Reactor key and Bring Your Own Key.

Custom endpoint

ts
import { reactorWorld } from '@tanstack/ai-reactor'

const adapter = reactorWorld('visko-orbis-stable', {
  baseUrl: 'https://api.reactor.inc',
})

API reference

FactoryCreates
reactorWorld(model, config?) / createReactorWorld(model, apiKey, config?)A world adapter for generateWorld()
reactorVideo(model, config?) / createReactorVideo(model, apiKey, config?)A live video adapter for generateLiveVideo()

The short factory reads REACTOR_API_KEY when config.apiKey is not set. The config fields:

  • apiKey: the Reactor API key.
  • baseUrl: the API base URL. The default is https://api.reactor.inc.
  • fetch: your own fetch function.