
reactor-sdk 1.5.0 (Python)
New
- Wheels for Alpine Linux.
pip install reactor-sdknow finds a wheel on a musl 1.2+ distribution, x86_64 and aarch64. Before, there was none, and pip resolved to an older release with a different API. Pinreactor-sdk>=1.0to get an error instead.
Fixed
- A
FileRefnested insidesend_command()’sdatanow reaches the model. Put one in a list or a dict, and the SDK sends it as the upload reference the model reads. Before, the call raisedTypeError, so a parameter that takes several files needed a hand-written{"upload_id": ...}dict, which you can now delete. A top-levelFileRefstill travels beside the arguments.
js-sdk 3.0.2
Fixed
- A
FileRefnested insidesendCommand()’sdatanow reaches the model. Pass one inside an array or object, and the SDK sends it as the upload reference the model reads. Before, it crossed the wire with the class’s own field names (uploadId,mimeType), and the model refused it. A parameter that takes several files needed a hand-written{ upload_id }object, which you can now delete. A top-levelFileRefstill travels beside the arguments.
C++ SDK 2.0.1
Rolls up the C++ SDK’s 2.0.0 and 2.0.1 releases.
Breaking
- Audio tracks use
on_frame()andpush_frame(), as video already did.on_audio()andpush_audio()are gone. Take anAudioFramein the handler you pass toon_frame(), and push interleaved 16-bit PCM withpush_frame(Samples{...}). The overload has to match the track’skind(), so a video handler on an audio track throwsInvalidStateError. Rename the two calls. Nothing else about them changed. pause()andresume()throw on a sendonly track. Both control what the client receives, and a sendonly audio track now refuses them exactly as a video one already did. Code that paused a track it publishes did nothing before, and throws now.Options::localskips the API-key exchange and selects localhost. Withapi_urlleft at its default, a local connection goes toreactor::LOCAL_API_URL(http://localhost:8080) rather than to production. A key you passed is no longer exchanged for a token. A customapi_urlis kept as it is, and so is a JWT you supplied. This matches the Python SDK.
New
fetch_jwt(). Exchanges an API key for a token without building a client or opening a session, for a backend that mints tokens somebody else will use.FetchJwtOptionsscopes the token to a set of models, caps how many sessions it may open and how long each one runs, and sets its lifetime. It returnsstd::future<std::string>, and a failure arrives as the same typedReactorErrora call throws.- A constructor that takes no credential.
reactor::Reactor{model, options}withoptions.local = trueconnects to a local runtime, which needs none. on_capabilities_received()andon_session_id_changed(). The model’s capabilities as they arrive, and the session id whenever it is set or cleared (std::nullopt). Register both beforeconnect()to catch the first of each. The declared tracks are refreshed before capabilities are delivered, sotracks()is already current inside that handler.- A
FileRefcan go inside a command’s arguments. A parameter that takes several files has no named slot in theuploadsmap. Pass those references inargsinstead: aFileRefconverts toJsonas the upload reference the model reads, andJson::array({first, second})is that list. A parameter that takes one file is still named inuploads.
C++ SDK 1.0.0 · reactor-sdk 1.3.0 → 1.4.0 (Python)
Rolls up the C++ SDK’s first release and reactor-sdk 1.3.0 and 1.4.0.
New
- The Reactor C++ SDK’s first release. A C++17 client for native applications (engines,
capture pipelines, desktop clients), built on the same Rust core the Python and JavaScript
SDKs bind, and shipped as one prebuilt archive per platform rather than through a package
manager. The base
Reactorclass andTrackcover connecting, commands, uploads, recordings, events, connection-wide and per-track bitrate control, and connection stats — calls returnstd::future<T>, frames arrive as BGRA on the library’s delivery thread.SpeakerandMicrophone(reactor::sdk_audio) play arecvonlyaudio track through real speakers or capture a real microphone into asendonlyone. See Installation and Using the SDK. get_stats(). A snapshot of the live connection, as aConnectionStats: RTT, jitter, packet loss, bitrates, and the WebRTC engine’s own per-stream counters. An unmeasured field readsNonerather than zero. The SDK derives the two measured bitrates against the previous call, so they stayNoneuntil a second call at least 200 ms later. Only callable once the session isready.Reactor.set_bitrate()andTrack.set_bitrate(). Bound the connection’s whole budget, and one sender’s share of it, in bits per second. The two apply together and the lower wins. For video, the track-level ceiling is the one to raise: with nothing set, WebRTC caps anything above 960x540 at 2.5 Mbps, 4K included.max_session_duration_secondson theReactor()constructor. Terminates every session the client’s own minted token creates after that many seconds, whatever the token’s expiry says. The same constraintfetch_jwt()takes, now available without minting the token yourself. Ignored when you passjwt.RecorderDisabledError.request_clip()/request_recording()raise this instead of a bareReactorErrorwhen the model’s recorder is disabled or has crashed. Retrying the call will not help. The recorder has to be restarted on the model side. See Error codes.get_stats()reports the transport the session uses.candidate_typeis"host","srflx","prflx", or"relay", and"relay"means the media goes through a TURN server. That is the first thing to check when latency is bad.relay_protocolnames that server’s own transport ("udp","tcp", or"tls").- Three fields the JavaScript SDK already reported are here too.
available_incoming_bitrate_bpsandavailable_outgoing_bitrate_bpscarry the congestion controller’s estimate of what the path can carry, not what flows over it.frames_per_secondis the rate on the received video stream. - The per-stream report carries more.
InboundStreamandOutboundStreamnow name theirkind("audio"or"video") and count video frames. A send stream also carries what the far end reports back about it:total_round_trip_time_s,fraction_lost, andpackets_lost.CandidatePairgainsnominated,writable, its own byte and packet totals, and the two bitrate estimates.
Changed
close()settles operations still in flight. Asend_command()whose reply had not arrived whenclose()ran now raisesAbortedErrorin the coroutine that awaits it. Before, thatawaitstayed pending for the life of the process.disconnect()was never affected.push_frame()refuses raw bytes that do not match their dimensions. A BGRA buffer that is not exactlywidth * height * 4bytes raisesValueError. Only the dimensions cross to the native side, never the length, so a mismatch used to send the reader past the end of the buffer. Frames pushed as a numpy array are unaffected.- Prebuilt wheels.
pip install reactor-sdknow installs a wheel with the native library already in it, one per platform: Linux on glibc 2.34 or newer (x86_64, aarch64), macOS 11+ on arm64 and 13+ on x86_64, and Windows x86_64. One wheel covers every supported interpreter, 3.10 and up. There is no source distribution, so anywhere else pip quietly resolves to an older release. Pinreactor-sdk>=1.0to get an error instead. jitter_sandpacket_loss_rationow come from the received video stream. That is the stream the JavaScript SDK reads, so the two SDKs report the same number for the same connection. They used to aggregate across every receive stream. An audio-only session still gets both: they fall back to the receive streams there are, where the JavaScript SDK reports nothing.- The two measured bitrates now cover the candidate pair that carries the media.
incoming_bitrate_bpsandoutgoing_bitrate_bpscount everything that pair carried, RTCP and the data channel included. Before, they summed RTP payload and read a little low. One consequence: both readNoneuntil ICE selects a pair, on top of the cases that already readNone. rtt_mscomes from that same pair, rather than from the highest-priority succeeded one.
Fixed
- An outbound stream’s
round_trip_time_swas always0.0. It now carries the measured value. - A send stream’s
packets_sentandretransmitted_packets_sentwrapped after about 4.3 billion packets, which is roughly seven weeks at a thousand packets a second. After the wrap, a cumulative counter appeared to go backwards. Both are 64-bit now, as are the candidate pair’s.
js-sdk 3.0.1
New
setTrackBitrate()raises the ceiling on a track you publish. With no bound set, a Chromium-based browser caps a sender above 960x540 at 2500 kbps, so a 720p or 1080p publish never exceeds 2.5 Mbps even on a fast connection. Call it withmaxBpsafterpublishTrack()to give the encoder more room.minBpssets a floor, which only Chromium honors. There is no connection-wide equivalent, because browsers do not expose one.
js-sdk 3.0.0
A major version bump for
@reactor-team/js-sdk. This release moves session and protocol
handling into the same underlying implementation the Python and C++ SDKs share — the SDK’s own
JavaScript/TypeScript no longer drives the browser’s WebRTC APIs by hand; that layer does, over
the same wire protocol.Breaking
sendCommand()now waits for its reply, and resolves with it. The returned promise used to resolve as soon as the command was sent, before the model replied. It now genuinely awaits the model’s correlated reply, bounded by a timeout, and resolves with the reply payload (orundefinedif the handler acknowledged the command without sending one back). A call site that awaits or otherwise consumes the returned promise now waits for the real round trip instead of resolving almost immediately; a caller that never awaited it in the first place is unaffected.ReactorErroris a typed class hierarchy, not a flat object. It’s now the base of 16 concrete subclasses (UnauthorizedError,ConflictError,RateLimitedError, and more — see Error codes), each carrying a stablecode,recoverable,status,operation,retry_after_ms, andtimestamp_ms. The oldcomponentfield ("api"/"gpu") is gone.retryAfter/timestampare kept alongside the new field names for compatibility.
New
- Track introspection on the base class.
tracks(),trackMapping(),pausedTracks(), andgetTrackByName()/getStreamByName()/getTrackByMid()/getStreamByMid()resolve a named or negotiated track to itsMediaStreamTrack/MediaStreamdirectly off aReactorinstance. pauseTrack()/resumeTrack()are asynchronous. Both now return aPromise<void>, queued behind the same control round-trip as other track operations, instead of applying synchronously.getSchema()/schemaReceived. The model’s command schema, fetched automatically on the first"ready"transition and cached for synchronous reads.once(). Registers an event listener that fires at most once, then removes itself.isFileRef(). A structural check for anything shaped like aFileRef, for callers who’d rather duck-type than rely oninstanceof.useReactor()exposes more store state.lastMessage,jwtToken, andconnectOptionsare now readable off the store, andpauseTrack/resumeTrackare store actions alongsidepublish/unpublish.
Removed
getState(). ReadgetStatus(),getLastError(), andgetSessionId()individually instead.- The
sessionExpirationChangedevent. It never carried a real value in any deployment — dropped rather than carried forward.
reactor-sdk 1.0.0 (Python)
Breaking
send_command()now waits for its reply. Previously fire-and-forget — the call resolved as soon as the command was queued, and any reply from the model arrived later as a separatemessageevent. It now returns the model’s correlated reply directly:{"type": ..., "data": ...}, orNoneif the handler acknowledged the command without returning a message (an auto-generatedset_<field>setter, for instance). To fire a command without waiting on the reply, schedule the call instead of awaiting it directly —asyncio.create_task(reactor.send_command(...))— keeping a reference to the task so it isn’t garbage-collected before it completes.- Errors are now typed exceptions, and
ReactorErroris the one class for both. Every failure used to arrive as one bare exception carrying only a sentence, and separately,on_errorhanded you a same-shaped-but-different dataclass.ReactorErroris now both: the base of 16 concrete subclasses (UnauthorizedError,ConflictError,RateLimitedError, and more — see Error codes) thaton_errorand a raised call hand you identically, each with a stablecode,recoverable,status,operation,retry_after_ms, and (event-only)timestamp_ms. The oldcomponentfield (api/gpu) is gone — it split codes by platform tier, which wasn’t actionable and could report the same failure under two different codes.except ReactorErrorstill catches everything; code relying onerror.componentneeds updating. - Audio devices are synthetic-only. The constructor’s
adm_modeargument is gone; noReactorinstance ever opens a real microphone or speaker on its own. Asendonlyaudio track only ever carries PCM you push yourself withTrack.push_frame(), and a model’s audio must be played back explicitly viaTrack.on_frame()/on_raw_frame()— or withSpeaker/Microphone, below. reactor.on_frame(client-wide) is removed. It only ever worked for video, and a single handler fed everyrecvonlyvideo track couldn’t tell them apart. Register on theTrackinstead, found by name (reactor.track(name)) or by filteringreactor.tracks(reactor.tracks.with_direction("recvonly").with_kind("video").one()).- The client-wide
on("frame", ...)/on("audio", ...)events are removed too, for the same reason.on()raisesValueErrorat registration for either name, naming theTrackmethod to use instead —on_frame()for decoded frames,on_raw_frame()for the same bytes with no NumPy conversion. publish_track()/unpublish_track()are the only name-based track methods still public onReactor.push_video_frame(),push_audio_frame(),pause_track(), andresume_track()are now internal — reach the same operations through theTrackobject instead:track.push_frame(),track.pause(),track.resume().publish_track()already hands back that sameTrack.Track.push_frame()now raisesInvalidStateErroron an unpublishedsendonlytrack, rather than silently dropping the frame. Callpublish()first —Track.publishedsays whether that’s already happened.reconnect()resumesrecvonlytracks automatically, but does not restore asendonlytrack’s publish. Callpublish()again after reconnecting for anything you were sending before it.@reactor.on_trackhands over theTrackitself, not a bare(name, mid). TheTrackis already resolved —reactor.track(name)done for you — so a handler that wants to push frames or registeron_frameno longer has to look it up first;track.midstill carries the WebRTC media stream ID.unpublish_track()/Track.unpublish()returnNone, not anint. A failure is now logged (reactor_sdkatWARNING) rather than left for the caller to notice by checking a return value nobody checked — unpublish is commonly the last call in afinallyblock, so it still does not raise.Reactor()’s positional arguments aremodel_name,api_key—api_urlmoved to keyword-only. Matches the oldpy-sdk’s own constructor order exactly, soReactor(model, key)ported from there needs no rewrite to keyword arguments at all. Previouslyapi_url/model_namewere positional (in either order, sniffed by which one looked like a URL); any call relying on that specifically needs updating, but a plainReactor("your-model-name")is unaffected.upload_file()now requiresready. Previously allowed as soon as the session was created, before the WebRTC handshake finished — raisesInvalidStateErrornow if called earlier, the same guardrequest_clip()andTrack.pause()already used. See File Uploads.
New
reactor.tracksis now aTrackList. Still a plainlist[Track]for iterating and indexing, withwith_kind()/with_direction()filters that chain, and.one()for the common case of “the track I mean, and an error if that’s ambiguous.” SeeTrackList.- The
Trackobject.reactor.track(name)andreactor.tracksreturn aTrackscoped to one named slot, withpublish(),push_frame(),on_frame()/on_raw_frame(), andpause()/resume()that raise on a direction mismatch instead of a silent no-op. Track.published. Whether asendonlytrack is currently activated — readable state for something the session itself doesn’t track, cleared automatically whenever the connection leavesready.download_clip(). Fetches every segment aClip’splaylist_urlnames. Given apath, streams straight to it (returnsNone, never holds more than one segment in memory — the one to use forrequest_recording()); without one, returns the assembled bytes instead. Takes an optionalon_progresscallback either way. Closes a real gap left by the rewrite off the oldpy-sdk, which had this asdownload_clip_as_file().reactor.download_clip(seconds)/reactor.download_recording().request_clip()/request_recording()plus the download above, in one call, for when the file is all you want.request_clip()/request_recording()still return just theClipfor anyone who also wants itssession_id, markers, orpredicted_ready_at_ms.connect(connection_id=...). Adopts a WebRTC connection slot a backend already registered for a session, the connection-level analogue ofsession_idadoption — see Multiple connections per session. Also closes a gap left by the rewrite: the oldpy-sdkhad this, and the JavaScript SDK still does.reconnect()now works fromready, not only after a drop. It tears the live connection down itself first — without ending the session, unlikedisconnect()— so there was never a need to calldisconnect()immediately before it; that combination is also no longer required.disconnect()’s own behavior is unchanged — it has always ended the session server-side, with no recoverable option — a few places in these docs previously described it backwards, now corrected. See Sessions.Speaker/Microphone(reactor_sdk.audio_devices). Play arecvonlyaudio track through real speakers, or capture a real microphone into asendonlytrack — context managers wrapping asounddevicestream around aTrack:with Speaker(output_track), Microphone(mic_track): .... Not re-exported fromreactor_sdk— the only part of this SDK with a runtime dependency, sopip install "reactor-sdk[audio]"and import fromreactor_sdk.audio_devicesdirectly.Speakerpromotes whatexamples/pygame_apphad hand-rolled;Microphoneis new.fetch_jwt(api_key, api_url=...)’sapi_urlis optional now, defaulting to the same production APIReactor()itself defaults to. Pass it explicitly only when minting a token against a different one.
helios · Rewind
New
- Rewind. Helios saves snapshots of the scene and returns to them on demand.
save_snapshotcaptures the world state,rewindrestores it, andlist_snapshotsreports what the buffer holds. The buffer keeps up to 50 snapshots per session and evicts the oldest once full; aresetempties it, and it is released when the session ends. Each snapshot records the one it descends from, so a save after a rewind branches off the restored snapshot while a save without one extends the branch in progress, andcurrent_snapshot_idon thestatemessage says which branch is live. Rewinding restores the scene but not the prompt, so send the prompt you want to continue with after the rewind. Available in@reactor-models/heliosassaveSnapshot,rewind, andlistSnapshots, with the matchinguseHeliosSnapshotSaved,useHeliosRewindComplete,useHeliosRewindFailed, anduseHeliosSnapshotListhooks.
js-sdk · Multi-client connections
Breaking
- Publishing an input track is now explicit. Output tracks still stream automatically on connect, but
sendonly(input) tracks send nothing until your app callspublishTrack()(or renders<WebcamStream>). Apps that relied on input media flowing at connect must now publish explicitly once the connection isready. See Tracks.
New
- Output track pause/resume. New
pauseTrack()andresumeTrack()methods, plus anautoResumeTracksconnect option (defaulttrue), control when each output track streams. - Multi-client sessions. A single session can now back several connections at once.
connect()acceptssessionIdandconnectionIdoptions to attach to a session or connection your backend created.
Pricing
Changed
- Pricing shown per second, in dollars. Model pricing on the Pricing & Billing page now leads with a per-second US-dollar rate, matching how sessions are billed, with a per-hour figure shown for reference.
Docs · Pricing & billing
New
- Pricing & billing docs. Published Pricing & Billing, covering session pricing, idle vs. active billing, and recoverable-state behavior.
- Beta status callouts. Added a banner across docs and the Overview noting that Reactor is in beta. APIs and pricing may evolve.
Authentication · API
New
- Token TTL with
expires_after. Auth tokens now carry anexpires_afterfield so clients can refresh proactively instead of waiting for a 401. See Authentication.
js-sdk · reactor-sdk · Concepts
New
- File uploads in the Python SDK.
reactor-sdknow supportsupload_file()returning aFileRefthat can be passed intosend_command(). - File uploads in the JavaScript SDK.
uploadFile()andFileRefare available in@reactor-team/js-sdkfor passing files (images, audio, blobs) as command arguments. - New concept page: File Uploads documents the upload-then-reference pattern in both SDKs.
API
Breaking
- Token endpoint is now POST. The auth-token endpoint moved from
GETtoPOSTto allow request-body parameters. Update any direct API callers; the SDKs handle this automatically once upgraded.
js-sdk 2.7 · reactor-sdk 0.4
Breaking
- New SDK protocol. Wire-level protocol overhaul shipped in
@reactor-team/js-sdk2.7 andreactor-sdk0.4. Older SDK versions will no longer connect. Upgrade to the latest release. - Removed
fetchInsecureToken()and JWT helpers. The development-only insecure token helpers were removed; all auth now flows through the standard token endpoint.
Improved
- Maple theme refresh. Docs site updated to Mintlify’s Maple theme with Lucide icons, new codeblock styling, and refreshed colors.
- Navigation split into separate “SDK” and “API Reference” tabs.
Helios
js-sdk 2.6.0
Breaking
- API renames in
@reactor-team/js-sdk2.6.0. Several public methods and types were renamed for consistency with the Python SDK. Update import paths and call sites.
Helios
New
- Helios launch. Reactor’s first interactive real-time video generation model, built on a 14B-parameter Diffusion Transformer. Produces a continuous infinite stream you can steer with prompts and reference images.
- Helios prompt guide. A dedicated Prompt Guide covering scene composition, transitions, and steering techniques.
Docs
Improved
- Renamed “Realtime AI” → “Realtime Video AI” across the docs to better reflect what Reactor does. No API or SDK changes.
reactor-sdk (Python)
New
- Python SDK launched.
reactor-sdkis now available on PyPI, async-first, with parity for sessions, commands, and message handling. See Using the SDK.
js-sdk
Breaking
sendCommand()replacessendMessage(). The command API was renamed to clarify the distinction between commands (client → model) and messages (model → client). Update allsendMessagecall sites. See Commands and messages.- Removed message queueing. Commands now have explicit
waitingstate instead of implicit queueing. Applications can decide whether to drop, replace, or buffer in-flight commands.
Authentication · API
Breaking
- JWT auth replaces
insecureApikey. All client connections now require a short-lived JWT minted server-side from your API key. TheinsecureApikeydevelopment flow is removed. - New API Reference section. Published a complete API Reference covering the
Reactorclass, React components, hooks, types, and events.
Docs
New
- Reactor docs site launched. Initial release with Overview and Quickstart. Ported from the legacy readme.io site.