attach() (observability)
attach() is VoiceGateway’s observe seam: a single, passive meter for cost
and latency. You call it once, hand it your agent session or pipeline task, and
every STT, LLM, and TTS call flowing through that session is priced and recorded.
It never reroutes, throttles, or blocks a call; measuring and controlling are a
hard line, and control lives in guard().
Because attach() is the single meter, pairing it with guard() never
double-counts. guard() writes no metrics of its own.
Signature
attach() detects the target’s framework by type and installs the matching
observer. The return value is the session id that ties every captured row
together, so you can echo it into your own logs.
LiveKit: attach(session)
Construct yourAgentSession with native livekit.plugins providers, then
attach before you start it:
attach() subscribes to the per-component
metrics_collected events, so it works with any plugin without wrapping it. The
session’s close event finalizes the meter (drains in-flight writes and flushes
the sink), so a graceful shutdown loses nothing.
Pipecat: attach(task) or Observer
Pipecat has no cumulative usage aggregate, soattach() sums the metrics it
observes. Enable Pipecat’s metrics on the task, then either call attach(task)
or pass an exported Observer to the task constructor. Both do the same thing.
Enable Pipecat metrics. The observer meters MetricsFrames, so the pipeline
must emit them:
enable_metrics / enable_usage_metrics, Pipecat emits no usage frames
and there is nothing for attach() to record.
Option A: attach(task).
voicegateway.Observer takes the same keyword arguments as attach()
(project, agent_id, tenant_id, channel, collector_url, api_key,
sink). The observer finalizes itself on the pipeline end (EndFrame): it drains
any pending STT audio into a final record and flushes the sink.
What it records
attach() writes one row per request through a Sink (local SQLite by default,
or a remote collector when collector_url / VOICEGW_COLLECTOR_URL is set).
Each row carries:
| Field group | What it captures |
|---|---|
| modality + provider + model | stt / llm / tts, the provider name, and the model id |
| usage units | STT audio minutes, LLM prompt / completion / cached tokens, TTS characters |
| cost | priced through voice-prices from the usage units |
| latency | time to first byte (ttfb_ms) and total latency |
| correlation | the session id, project, agent_id, tenant_id, and channel |
| routing | fallback_from and status when a guard() fell back to this provider |
How each modality is measured
- LLM: from the usage metric (
prompt_tokens,completion_tokens,cache_read_input_tokenson LiveKit or Pipecat’sLLMTokenUsage). - TTS: from the character count in the usage metric.
- STT: derived from audio duration as the baseline. On Pipecat the observer
accumulates the
AudioRawFramebytes routed to each STT service and converts bytes to seconds (16-bit mono PCM). A service’s direct usage is used when it exposes one.
Channel and session
Channel is"telephony" or "web". When you omit channel=, attach()
auto-detects it from the transport: a LiveKit SIP remote participant means a
phone call (else web); a Pipecat Twilio / Telnyx / Plivo (etc.) serializer means
telephony, while a Daily / WebRTC / websocket transport means web. Pass
channel= explicitly to override the guess.
Session correlation is automatic. Every row from one attached session (or
pipeline) shares the returned session id, which the dashboard uses to group a
conversation and its per-turn timeline. On LiveKit the id is created when the
session context opens; on Pipecat it is created when you attach. Multi-tenant
operators pass tenant_id= to slice costs per customer.
See also
- guard(): the active control seam that composes with
attach(). - Frameworks and extras: install
voicegateway[livekit]vsvoicegateway[pipecat]. - Migration guide: moving off the deprecated
voicegateway.LLM/STT/TTSfactories.