Skip to main content
VoiceGateway does not sit in the inference path. You construct your own LiveKit plugin instances or Pipecat services the way you always have; two functions instrument them from the outside:
  • attach(session): a passive observer. It reads STT/LLM/TTS metrics off the session’s own event stream, prices them via voice-prices, and writes a RequestRecord.
  • guard(provider): an active wrapper around one already-constructed provider instance. It adds fallback, a rate limit, and a spend cap in front of that provider, and writes no metrics of its own.
The two compose without double-counting: guard() never meters, attach() is the only writer. See attach() and guard() for full signatures and wiring examples.

Request flow

attach() subscribes directly to the metrics_collected events LiveKit and Pipecat already emit per component; it does not wrap or replace the plugin instance. guard() does wrap: it returns a same-type drop-in that runs its checks before delegating to the real provider (or a fallback). Neither reads from nor writes through the BaseProvider classes described in Provider Abstraction.

Directory layout

There is no separate dashboard backend process. The combined server in src/voicegateway/server/ serves the built frontend SPA at / (see server/static.py); the legacy standalone dashboard API was removed in 2026-05.

Design principles

Async throughout. Database, HTTP, and provider operations use async/await. attach() and guard() are async-native from the caller’s perspective (guard()’s wrapped methods are coroutines/async streams). Framework neutral. VoiceGateway does not own the inference path or bundle provider wheels. You install the plugin wheels your agent uses (for example pip install livekit-plugins-openai); VoiceGateway meters those instances by model_id via voice-prices without ever importing the provider SDK itself. attach() reads identity off the live instance, not off a config string. attach()/guard() build provider/model from the plugin’s own .provider/.model attributes (component_identity() in inference/session/capture.py), the reverse of parsing a string to look something up. See Models and stacks for the exact provider/model format this produces. guard() wraps, attach() forwards. guard()’s wrappers subclass the matching livekit.agents base class directly and override the methods that matter (chat, recognize, synthesize, stream); a __getattr__ fallback only catches provider-specific extras the base class doesn’t declare. Every wrapper forwards the inner plugin’s metrics_collected event transparently, so an attach() bound to the same session still sees it once. Config layering. Three sources merge at startup: environment variables (highest priority), SQLite managed tables (dashboard/MCP writes), and YAML (base config). See Configuration layers. Encryption at rest. Provider API keys stored in SQLite are encrypted with Fernet (AES-128-CBC + HMAC-SHA256, via MultiFernet so a rotated key can still decrypt old rows). Keys in API responses are masked to the secr...2345 format. See Security model.

Architecture pages

Gateway Core

The Gateway class, config loading, and the provider registry.

Provider Abstraction

BaseProvider ABC and the 11 provider health-check implementations.

Middleware

Cost tracking, rate limiting, and what attach()/guard() actually wire in.

Cost Tracking

Per-modality cost calculation via voice-prices.

Rating

Turning recorded cost into a billable price with a rate card.

Configuration Layers

YAML + SQLite + env merge and the ConfigManager.

Storage

SQLite schema, views, and the ClickHouse sink.

Security Model

Key encryption, secret masking, and MCP auth.

Fleet Worker Heartbeat

The heartbeat contract between agents and the roster.

Replay Storage Costs

On-disk footprint of session replay capture.