src/voicegateway/middleware/ has more files than the request path uses. This page covers what attach() and guard() actually exercise, and says plainly where a class is constructed but never invoked.
CostTracker
File:src/voicegateway/middleware/cost_tracker_middleware.py
The one middleware class every write path uses. attach() constructs a fresh CostTracker per session; guard() constructs a no-op one (storage=None) purely to satisfy its wrapper’s constructor, since guard() writes no records. The Gateway singleton’s own gw.cost_tracker is a third, separate instance, used only by the fleet-ingest endpoint to re-rate rows submitted by remote agents.
Pricing
Costs delegate tovoice_prices.calc_price. _catalog_cost() maps the recorded units onto the modality’s Usage shape (STT: audio_input_seconds, LLM: input_tokens / output_tokens / cache_read_tokens, TTS: characters). Self-hosted local/* and ollama/* models price at $0 without a catalog lookup.
See Cost Tracking for the full per-modality flow.
Key methods
create_record(...)builds aRequestRecord: resolves cost, then rates it against the activeRateCard(see Rating).log_request(record)persists the record viastorage.log_request(), then callsnotify_spend().notify_spend(record)callsBudgetEnforcer.record_spend()(below) so the cached daily-spend figure stays current for the status badge.close_session(session_id)finalizes session-aggregate metrics and replay tables on session close.
RateLimiter
File:src/voicegateway/middleware/rate_limiter_middleware.py
A token-bucket limiter: acquire(provider) raises RateLimitExceeded if the provider’s requests-in-the-last-60-seconds count is at or over its configured requests_per_minute.
guard(provider, rate_limit="60/min")builds its ownRateLimiter, seeded from the parsed DSL string, keyed to that one provider. This is the limiter that actually runs on a call. See guard().Gateway.__init__also builds aRateLimiterfromvoicegw.yaml’s top-levelrate_limits:block, and stores it asgw._rate_limiter. Nothing calls.acquire()on it. The YAML block is parsed and held, not enforced.
BudgetEnforcer
File:src/voicegateway/middleware/budget_enforcer_middleware.py
Two of its methods are live, one is not:
record_spend(project, cost_usd): live.CostTracker.notify_spend()calls this after every logged request, updating a 30-second-TTL in-memory cache of today’s spend per project.get_budget_status(project, today_spend): live. Returns"ok"/"warning"(≥80% ofdaily_budget) /"exceeded"(≥100%). The dashboard,voicegw project <id>, and the MCP project tools call this to render the budget badge.check_budget(project): not called anywhere outside its own class and its tests. This is the method that would dispatch onbudget_action(warn/throttle/block) and raiseBudgetThrottleSignalorBudgetExceededError. It never runs, sobudget_actioncurrently has no effect on gateway behavior; see Projects: Budgets for the full caveat.
guard()’s own budget="$X/day" argument instead. That is a separate mechanism (GuardControl.check_budget() in inference/livekit/guard_livekit.py / inference/pipecat/guard_pipecat.py): it reads accumulated spend straight from storage and raises BudgetExceededError, without going through this class at all.
InstrumentedSTT / InstrumentedLLM / InstrumentedTTS
File:src/voicegateway/middleware/instrumented_provider_middleware.py
attach() does not use these. It subscribes directly to the metrics_collected events a plugin already emits (see Architecture Overview); there is nothing to wrap. These three classes have two callers instead:
guard()subclasses them withmetering=False: a control-only shell that forwards the inner plugin’s events transparently (so anattach()on the same session still sees them once) but writes noRequestRecorditself.voicegw checkis the sole caller of the module-levelwrap_provider()helper, withmetering=True, to drive one synthetic instrumented request through storage as a self-test.
livekit.agents base (InstrumentedSTT(lk_stt.STT, ...), etc.) and overrides the methods that matter (chat, recognize, synthesize, stream) to delegate to the wrapped instance. __getattr__ only fires as a fallback, for provider-specific attributes the LiveKit base class doesn’t declare (a Cartesia-only set_voice, for example): it is not the primary mechanism, just a safety net after normal attribute lookup fails.
Fallback
guard() takes a fallback=[...] list of already-constructed provider instances and tries them in order on a pre-first-token error, via GuardControl.run_with_fallback() (STT) or iterate_with_fallback() (streaming LLM/TTS). There is no separate “resolver-time fallback” mechanism in this package: the walk lives entirely inside guard(). See guard() for the exact semantics and its pre-first-token limitation.
Constructed but unused
Two more classes are wired intoGateway.__init__ and never called again: LatencyMonitor (middleware/latency_monitor_middleware.py, stored as gw._latency_monitor) and RequestLogger (middleware/logger_middleware.py, stored as gw._logger). Real TTFB/latency numbers come from LiveKit’s and Pipecat’s own metric fields, read directly by MetricCapture and InstrumentedSTT/LLM/TTS, not through LatencyMonitor.