Discord

Live video SDKs

Python SDK

A sync client (LiveVideo) and an asyncio client (AsyncLiveVideo) with the same surface; on the async client every network method is a coroutine. Read the SDK overview first for the concepts (broker model, tokens, connect, heartbeat).

Published. Both SDKs are on PyPI and npm as videorouter-live. A live-video model can only be booked once its price is verified (see Live video & world models), and the SDKs surface that as PricingUnavailableError.
pip install videorouter-live

Requires Python 3.9+. The only dependency is httpx.

Quickstart (sync)

from videorouter_live import LiveVideo

lv = LiveVideo(api_key="llmr_sk_live_...")     # or set VIDEOROUTER_API_KEY

with lv.session(
    "fal/happy-oyster-adventure",
    max_duration_seconds=60,
    options={"world": {"prompt": "a rainy cyberpunk city", "perspective": "first_person"}},
) as s:
    # The session is already ready and heartbeating in the background.
    send_to_browser(s.connect)         # host credentials - returned once by the server
    wait_for_user_to_finish()

# Settled on exit, even if your code raised.
print(s.status, s.billed_seconds, s.cost_usd, s.end_reason)

lv.session(...) creates the session, waits until it is ready, starts the heartbeat, and settles it on exit — even if your code raised. While a world is building the session is waiting and nothing is metered.

Async client

import asyncio
from videorouter_live import AsyncLiveVideo

async def main():
    async with AsyncLiveVideo(api_key="llmr_sk_live_...") as lv:
        async with lv.session("decart/lucy-2.5", max_duration_seconds=120,
                              options={"speed": "standard"}) as s:
            s.on("error", lambda e: print("heartbeat problem:", e))
            await hand_to_browser(s.connect)
            await asyncio.sleep(30)
        print(s.cost_usd)

asyncio.run(main())

Heartbeats run as an asyncio task on the running loop; handlers may be coroutine functions.

Manual lifecycle & events

s = lv.create_session("fal/happy-oyster-adventure", max_duration_seconds=60,
                      options={"world": {"prompt": "x", "perspective": "third_person"}})
print(s.status)                       # "waiting" - a world is building, nothing is metered
s.on("status_changed", lambda st: print("->", st))
s.on("ended", lambda sess: print("settled:", sess.cost_usd))

s.wait_until_ready(timeout=180, poll_interval=4)   # polls; the poll that sees it ready returns s.connect
s.start_heartbeat()                   # daemon thread, every ~5 s
use(s.connect)

s.end(host_travel_id="...")           # optional (Happy Oyster): bill on the host's own duration and fetch the replay

Use the manual flow when the session outlives one function, or when you want to own the polling. get_session(id) rebuilds a handle from an id (it never carries connect).

Browser tokens

token = lv.create_token(models=["fal/happy-oyster-adventure"], expires_in=300,
                        max_sessions=1, max_session_duration_seconds=60)
token.token        # "llmr_sk_live_..." - give this to the browser
token.expires_at   # ISO timestamp

Pass the token to the JavaScript SDK. A token can only drive live-video sessions (never billable endpoints, never create_token), is capped by the limits you set, and expires in expires_in seconds (1–21,600, default 3,600).

Generating videos

The clients also wrap the /v1/videos generation API (AsyncLiveVideo has the same surface as coroutines). Params are the API’s own fields, passed through unchanged — see Video generation. It needs an API key; a browser token is live-video only. Creating a job is never retried, so a flaky network cannot bill you twice.

from videorouter_live import LiveVideo

lv = LiveVideo()                           # reads VIDEOROUTER_API_KEY

# Create and wait
job = lv.videos.generate({
    "model": "heygen-video-1",
    "prompt": "A presenter says: 'Welcome to VideoRouter.'",
    "duration_secs": 5,
})
print(job.url)

# Or step by step
queued = lv.videos.create({"model": "wan-3.0-prime", "prompt": "A robotic arm in a lab"})
queued.wait(timeout=600, poll_interval=5)  # raises VideoGenerationError / TimeoutError
again = lv.videos.get(queued.id)

job.status, job.done, job.failed, job.url / job.urls, job.error and job.raw are available on every job.

Handling errors

from videorouter_live import (InsufficientCreditsError, ConcurrencyLimitError, HostCapacityError,
                              PricingUnavailableError, LiveVideoError)

try:
    s = lv.create_session("fal/happy-oyster-adventure", max_duration_seconds=60)
except InsufficientCreditsError:
    ...                                   # top up, or book fewer seconds
except ConcurrencyLimitError as e:
    ...                                   # end a session first; e.retry_after is set
except HostCapacityError as e:
    time.sleep(e.retry_after or 5)        # the host is full right now - sessions are never queued
except PricingUnavailableError:
    ...                                   # model.available is False
except LiveVideoError as e:
    print(e.status, e.code, e.message)

The full class table is in the overview. All classes subclass LiveVideoError and carry status, code, type, retry_after, body.

Reference

Clients

ConstructorNotes
LiveVideo(api_key=None, *, token=None, base_url=..., timeout=30.0, max_retries=2, http_client=None)api_key is your server key; token is a browser-safe token from create_token. With neither, VIDEOROUTER_API_KEY is read. base_url defaults to https://videorouter.sh/api/v1. http_client lets you inject an httpx.Client (proxies, tests). Use as a context manager or call close().
AsyncLiveVideo(...)Same arguments; http_client is an httpx.AsyncClient. async with or await aclose().
MethodReturns / behavior
models()list[Model]. available is False until the model has a verified price.
create_token(*, models=None, expires_in=None, max_sessions=None, max_session_duration_seconds=None)Token. Server-side only.
create_session(model, *, max_duration_seconds=None, options=None)LiveSession. Never retried.
get_session(session_id)LiveSession (no connect).
session(model, *, max_duration_seconds=None, options=None, wait_until_ready=True, auto_heartbeat=True, ready_timeout=300, poll_interval=4, heartbeat_interval=None)Context manager (async with on the async client): create → wait → heartbeat → settle on exit.

LiveSession / AsyncLiveSession

MemberDescription
id · session_idThe session id (lvs_...).
modelThe model slug.
status · get_status()Status: waiting, ready, ended (a str subclass, so s.status == "ready" works).
connectHost credentials, or None (waiting, or a re-fetched session).
stream_url · stream_headersComponent-edition models (vidu/vidu-s2-avatar-component) only, else None: the relayed WebSocket for PCM audio and transcripts, and the Authorization header it needs (a key never goes in the URL). Use any WebSocket client, e.g. websockets.connect(s.stream_url + "?conn_id=abc", additional_headers=s.stream_headers); the protocol is on the live-video page.
world_id, billed_seconds, cost_usd, end_reason, recording_url, expires_atServer fields; the billing fields are set once ended.
rawA copy of the last server response.
refresh()Re-read. For a waiting session this advances it; the call that sees the world built returns connect.
wait_until_ready(timeout=300, poll_interval=4)Raises TimeoutError or SessionStateError (ended while waiting).
heartbeat()One beat. Raises SessionStateError on an ended session.
start_heartbeat(interval=None) · stop_heartbeat()Background beat (daemon thread / asyncio task). Default interval: the server’s 5 s. Stops itself on 404 / 401.
end(host_travel_id=None)Settle. Idempotent. host_travel_id (Happy Oyster) bills on the host’s own duration and fetches the replay.
on(event, fn) · once · offEvents status_changed, ended, heartbeat, error. Unknown names raise ValueError.
last_error · get_last_error()The last background-heartbeat error.

Types

TypeFields
Modelid, kind (world / edit / ...), control, max_session_sec, available, price_per_second_usd, price_per_hour_usd, resolutions, description, speeds, fast_price_per_second_usd, raw
Tokentoken, expires_at, models, max_sessions, max_session_duration_seconds
StatusWAITING, READY, ENDED — strings

Testing your code

import httpx
from videorouter_live import LiveVideo

def handler(request: httpx.Request) -> httpx.Response:
    return httpx.Response(200, json={"object": "list", "data": []})

lv = LiveVideo(api_key="test", base_url="http://test/v1",
               http_client=httpx.Client(transport=httpx.MockTransport(handler)))
assert lv.models() == []

The package ships a py.typed marker, so type checkers see the annotations.