attach(session): a passive observer. It reads STT/LLM/TTS metrics off the session’s own event stream, prices them viavoice-prices, and writes aRequestRecord.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.
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
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 useasync/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.