OpenRTC is designed for the common case where you want to run several different voice agents on a small VPS without paying the memory cost of one full LiveKit worker per agent.
Table of Contents
A standard livekit-agents worker process loads shared runtime assets such as
Python, Silero VAD, and turn-detection models. If you run ten agents as ten
separate workers, you pay that base memory cost ten times.
OpenRTC keeps your agent classes completely standard and only centralizes the worker boilerplate:
- shared prewarm for VAD and turn detection
- metadata-based dispatch to the correct agent
- per-agent
AgentSessionconstruction inside one worker
Your agent code still subclasses livekit.agents.Agent directly. If you stop
using OpenRTC later, your agent classes still work as normal LiveKit agents.
OpenRTC intentionally wraps only the worker orchestration layer:
AgentServer()setup and prewarm- a universal
@server.rtc_session()entrypoint - per-call
AgentSession()creation with the right providers
OpenRTC does not replace:
livekit.agents.Agent@function_toolRunContexton_enter,on_exit,llm_node,stt_node,tts_node- standard LiveKit deployment patterns
| Deployment model | Shared runtime loads | Approximate memory shape |
|---|---|---|
| 10 separate LiveKit workers | 10x | ~500 MB × 10 |
| 1 OpenRTC pool with 10 agents | 1x shared + per-call session cost | ~500 MB shared + active-call overhead |
The exact numbers depend on your providers, concurrency, and environment, but OpenRTC is built to reduce duplicate worker overhead.
Install OpenRTC with the common runtime dependencies required for shared prewarm:
pip install openrtc[common]If you are developing locally, the repository uses uv for environment and
command management.
OpenRTC uses the same environment variables as a standard LiveKit worker:
LIVEKIT_URL=ws://localhost:7880
LIVEKIT_API_KEY=devkey
LIVEKIT_API_SECRET=secretAdd only the provider keys needed for the models you actually use:
DEEPGRAM_API_KEY=...
OPENAI_API_KEY=...
CARTESIA_API_KEY=...
GROQ_API_KEY=...
ELEVENLABS_API_KEY=...Use AgentPool.add(...) when you want the most explicit setup.
from livekit.agents import Agent
from openrtc import AgentPool
class RestaurantAgent(Agent):
def __init__(self) -> None:
super().__init__(instructions="You help callers make restaurant bookings.")
class DentalAgent(Agent):
def __init__(self) -> None:
super().__init__(instructions="You help callers manage dental appointments.")
pool = AgentPool()
pool.add(
"restaurant",
RestaurantAgent,
stt="deepgram/nova-3:multi",
llm="openai/gpt-4.1-mini",
tts="cartesia/sonic-3",
greeting="Welcome to reservations.",
)
pool.add(
"dental",
DentalAgent,
stt="deepgram/nova-3:multi",
llm="openai/gpt-4.1-mini",
tts="cartesia/sonic-3",
)
pool.run()Use discovery when you want one agent module per file. OpenRTC will import each
module, find a local Agent subclass, and optionally read overrides from the
@agent_config(...) decorator.
from pathlib import Path
from openrtc import AgentPool
pool = AgentPool(
default_stt="deepgram/nova-3:multi",
default_llm="openai/gpt-4.1-mini",
default_tts="cartesia/sonic-3",
)
pool.discover(Path("./agents"))
pool.run()Example agent file:
from livekit.agents import Agent
from openrtc import agent_config
@agent_config(name="restaurant", greeting="Welcome to reservations.")
class RestaurantAgent(Agent):
def __init__(self) -> None:
super().__init__(instructions="You help callers make restaurant bookings.")A discovered module does not need to provide any OpenRTC metadata. If the agent
class has no @agent_config(...) decorator:
- the agent name defaults to the Python filename stem
- STT/LLM/TTS/greeting fall back to
AgentPool(...)defaults
That keeps discovery straightforward while still allowing per-agent overrides when needed.
For each incoming room, AgentPool resolves the agent in this order:
ctx.job.metadata["agent"]ctx.job.metadata["demo"]ctx.room.metadata["agent"]ctx.room.metadata["demo"]- room name prefix match, such as
restaurant-call-123 - the first registered agent
This lets one worker process host several agents while staying compatible with standard LiveKit job and room metadata.
OpenRTC can play a greeting after ctx.connect() and pass extra options into
AgentSession(...).
pool.add(
"restaurant",
RestaurantAgent,
greeting="Welcome to reservations.",
session_kwargs={"allow_interruptions": False},
max_tool_steps=4,
preemptive_generation=True,
)Direct keyword arguments take precedence over the same keys inside
session_kwargs.
OpenRTC passes provider strings through to livekit-agents, so the common case
stays simple.
deepgram/nova-3deepgram/nova-3:multiassemblyai/...google/...
openai/gpt-4.1-miniopenai/gpt-4.1groq/llama-4-scoutanthropic/claude-sonnet-4-20250514
cartesia/sonic-3elevenlabs/...openai/tts-1
If you need advanced provider configuration, you can still pass provider objects instead of strings.
OpenRTC includes a small CLI for agent discovery.
openrtc list \
--agents-dir ./agents \
--default-stt deepgram/nova-3:multi \
--default-llm openai/gpt-4.1-mini \
--default-tts cartesia/sonic-3openrtc start --agents-dir ./agentsopenrtc dev --agents-dir ./agentssrc/openrtc/
├── __init__.py
├── cli.py
└── pool.py
pool.pycontains the coreAgentPoolimplementationcli.pyprovides agent discovery and worker startup commands__init__.pyexposes the public package API
Older prototypes used module-level constants such as AGENT_NAME and
AGENT_STT. OpenRTC now standardizes on @agent_config(...) plus
AgentPool(...) defaults.
Before:
AGENT_NAME = "restaurant"
AGENT_STT = "deepgram/nova-3:multi"
AGENT_LLM = "openai/gpt-4.1-mini"
AGENT_TTS = "cartesia/sonic-3"
AGENT_GREETING = "Welcome to reservations."After:
from openrtc import agent_config
@agent_config(
name="restaurant",
greeting="Welcome to reservations.",
)
class RestaurantAgent(Agent):
...Move shared provider settings into the pool:
pool = AgentPool(
default_stt="deepgram/nova-3:multi",
default_llm="openai/gpt-4.1-mini",
default_tts="cartesia/sonic-3",
)Contributions are welcome. Please read CONTRIBUTING.md before opening a pull request.
MIT. See LICENSE.