LiveKit metrics
The numbers livekit-agents reports, and how they reach a call's summary.
Where livekit-agents 1.8 produces each metric, and every class and field it declares: what the
wire's metrics models (pinecall/wire/metrics.py) are written against.
13 · 14 — where the metrics are produced#
Two independent channels, and we carry both.
The typed blocks. Every component (stt, tts, llm, vad, the interruption detector,
the turn detector, the keyterm detector, a realtime session) emits metrics_collected on
itself; AgentActivity subscribes to all of them (agent_activity.py:1044-1066) and forwards
through _on_metrics_collected (:1967), which stamps speech_id on LLMMetrics and
TTSMetrics from the speech-handle contextvar (:1971), feeds
AgentSession.usage (:1981) and re-emits both metrics_collected (:1983) and
session_usage_updated (:1984). EOUMetrics is emitted straight onto the session
(:2699) and so never reaches the usage collector.
AgentSession.on("metrics_collected")is deprecated in 1.8 — subscribing logs a warning namingsession_usage_updatedandChatMessage.metricsas the replacements (agent_session.py:725-731). The event still carries every block. The session subscribes to the components we built inproviders/instead, which is the same data with no warning, and takes the session'ssession_usage_updatedfor the rows. Two consequences, insession/session.py(_measured) andsession/_livekit.py(end_of_utterance): an emitter's listeners are a set (rtc/event_emitter.py:15), so ours may run before livekit's own stampsspeech_idon the block (:1971) — the write waits one loop tick; andEOUMetricshas no component to hear it on (:2699), so it is rebuilt from the user turn's ownMetricsReport, the way livekit builds it (:2683-2697).
The per-turn latencies. New in 1.8: ChatMessage.metrics is a MetricsReport
(agents/llm/chat_context.py:232,318), a total-false TypedDict — a field it could not
measure is simply absent. The agent's report is assembled before the message exists
(agent_activity.py:3568-3597) and handed to add_message(metrics=…) (:3620); the user's is
built by _init_metrics_from_end_of_turn (:4768), attached at :2554, and
on_user_turn_completed_delay is written into that same dict at :2615.
The timing, which is the whole reason the session's reading can be simple: a message reaches
conversation_item_added (agent_session.py:2065) with its metrics already complete.
The typed blocks arrive earlier — LLMMetrics when the stream closes, TTSMetrics per
segment — so in the log metrics.llm precedes the turn.agent it belongs to, joined by
speech_id.
The metrics, class by class#
Every class and every field the library declares, in declaration order. This table is the
contract the wire's metrics models and the session's reading of them (session/_livekit.py) are
built against. RealtimeModelMetrics.InputTokenDetails and
.OutputTokenDetails are nested classes; the wire flattens their names to
RealtimeInputTokenDetails / RealtimeOutputTokenDetails.
| class | where | fields |
|---|---|---|
Metadata |
agents/metrics/base.py:8 |
model_name model_provider |
STTMetrics |
agents/metrics/base.py:49 |
type label request_id timestamp duration audio_duration input_tokens output_tokens streamed acquire_time connection_reused metadata |
LLMMetrics |
agents/metrics/base.py:20 |
type label request_id timestamp duration ttft cancelled completion_tokens prompt_tokens prompt_cached_tokens cache_creation_tokens reasoning_tokens total_tokens tokens_per_second speech_id metadata |
TTSMetrics |
agents/metrics/base.py:71 |
type label request_id timestamp ttfb duration audio_duration cancelled characters_count input_tokens output_tokens streamed acquire_time connection_reused segment_id speech_id metadata |
VADMetrics |
agents/metrics/base.py:96 |
type label timestamp idle_time inference_duration_total inference_count metadata |
EOUMetrics |
agents/metrics/base.py:106 |
type timestamp end_of_utterance_delay transcription_delay on_user_turn_completed_delay speech_id metadata |
EOTInferenceMetrics |
agents/metrics/base.py:127 |
type timestamp total_duration detection_delay prediction_duration num_requests metadata |
RealtimeModelMetrics |
agents/metrics/base.py:143 |
type label request_id timestamp duration session_duration ttft cancelled input_tokens output_tokens total_tokens tokens_per_second input_token_details output_token_details acquire_time connection_reused metadata |
RealtimeModelMetrics.InputTokenDetails |
agents/metrics/base.py:143 |
audio_tokens text_tokens image_tokens cached_tokens cached_tokens_details |
RealtimeModelMetrics.OutputTokenDetails |
agents/metrics/base.py:143 |
text_tokens audio_tokens image_tokens |
InterruptionMetrics |
agents/metrics/base.py:194 |
type timestamp total_duration prediction_duration detection_delay num_interruptions num_backchannels num_requests metadata |
AvatarMetrics |
agents/metrics/base.py:212 |
type timestamp playback_latency session_started_time avatar_joined_time metadata |
LLMModelUsage |
agents/metrics/usage.py:27 |
type provider model input_tokens input_cached_tokens input_cache_creation_tokens input_audio_tokens input_cached_audio_tokens input_text_tokens input_cached_text_tokens input_image_tokens input_cached_image_tokens output_tokens output_audio_tokens output_text_tokens output_reasoning_tokens session_duration |
TTSModelUsage |
agents/metrics/usage.py:68 |
type provider model input_tokens output_tokens characters_count audio_duration |
STTModelUsage |
agents/metrics/usage.py:87 |
type provider model input_tokens output_tokens audio_duration |
InterruptionModelUsage |
agents/metrics/usage.py:104 |
type provider model total_requests |
EOTModelUsage |
agents/metrics/usage.py:116 |
type provider model total_requests |
MetricsReport |
agents/llm/chat_context.py:232 |
started_speaking_at stopped_speaking_at transcription_delay end_of_turn_delay on_user_turn_completed_delay llm_node_ttft llm_node_tps llm_node_ttfs tts_node_ttfb playback_latency e2e_latency provider_request_ids llm_metadata tts_metadata stt_metadata |
MetricsMetadata |
agents/llm/chat_context.py:227 |
model_name model_provider |
AgentSessionUsage (agents/metrics/usage.py:133) is one field, model_usage, a list of the
five ModelUsage rows above; ModelUsageCollector (:137) sums them. call.summary carries
that list whole.
18 — a streaming STT's STTMetrics is a usage meter, not a turn's block#
Seen on the first real voice call (room smoke-voice-1): 73 stored metrics.stt entries for a
42-second call, one every ~120 ms of speech, every field identical except timestamp and
audio_duration. Read from the library, none of that is an anomaly.
A streamed STT emits STTMetrics from exactly two places, and neither is a turn. The first is
STTStream._report_connection_acquired (agents/stt/stt.py:445-459), once per websocket, with
zero usage and the only two numbers a stream ever measures about itself — acquire_time and
connection_reused. The second is _metrics_monitor_task (agents/stt/stt.py:519-541), which
turns every RECOGNITION_USAGE speech event into a block whose audio_duration is the
incremental seconds of audio processed since the last report (stt.py:110-111) and whose
duration is hard-coded 0.0 — as the field's own docstring says, "the request duration in
seconds, 0.0 if the STT is streaming" (agents/metrics/base.py:54-55). The zero is the
contract, not a plugin bug.
The cadence is the plugin's choice, and the two we ship differ by a factor of forty. Deepgram
collects usage through a PeriodicCollector(callback=self._on_audio_duration_report, duration=5.0)
(plugins/deepgram/stt.py:484-486, emitted at :801-809): one block per five seconds of audio.
Soniox calls _report_processed_audio_duration(total_audio_proc_ms) for every frame it receives
off the websocket (plugins/soniox/stt.py:604, the report itself at :312-326) — hence one block
per partial, ~120 for a spoken sentence.
No STTMetrics can be joined to a turn. AgentActivity._on_metrics_collected stamps
speech_id from the speech-handle contextvar onto LLMMetrics and TTSMetrics only
(agents/voice/agent_activity.py:1969-1972), and STTMetrics declares no such field
(agents/metrics/base.py:49-68). There is therefore no "the stt of this turn" to keep,
and no last-one-of-the-turn to distinguish from the rest: the blocks are indistinguishable by
construction.
The turn's STT numbers live elsewhere, and we already store them whole. transcription_delay
and end_of_turn_delay ride the ChatMessage.metrics report (verdict 14) — the log writes them as
turn.user.metrics and, rebuilt, as metrics.eou. And the call's STT total is not lost by
dropping the ticks: livekit sums exactly these blocks into STTModelUsage.audio_duration
(agents/metrics/usage.py:87-101, summed by ModelUsageCollector, :137), which call.summary
carries whole and providers/prices.py bills from. On the smoke call the 73 ticks summed to the
42.48 s the summary row already carried.
What the session does with them is session/session.py _write_block: a streaming STT's tick
with no acquire_time is written ephemeral, so the durable log keeps the total and not the ticks.