adorable-bear-14321
04/28/2026, 3:04 AMtall-airline-16756
04/28/2026, 3:04 AMtall-airline-16756
04/28/2026, 3:04 AMmetrics_collected is deprecated and recommending session_usage_updated for session-level usage and ChatMessage.metrics for per-turn latency. That’s correct.
Would you like help:
• migrating existing metrics_collected handlers to session_usage_updated, or
• reading the latency fields on ChatMessage.metrics (examples of e2e_latency, llm_node_ttft, tts_node_ttfb)?
Tell me which one and share a snippet of your current implementation (or the language/framework you’re using) and I’ll provide focused migration code or examples.
Sources: Agent Console | LiveKit Documentation | Build Your First AI Voice Agent in Python: Complete Tutorial | LiveKit | Build Your First AI Voice Agent in Python: Complete Tutorial | LiveKit | Understand and Improve Agent Latency | LiveKit, with interactive elementsadorable-bear-14321
04/28/2026, 3:06 AMtall-airline-16756
04/28/2026, 3:06 AMmetrics_collected may still work for now, but it is deprecated and could be removed or stop receiving updates at any time. I recommend switching to:
• session_usage_updated for session-level usage (tokens, audio, cumulative counts).
• ChatMessage.metrics for per-turn latency (e2e_latency, llm_node_ttft, tts_node_ttfb, etc.).
Benefits of migrating: continued compatibility, more accurate/structured fields, and alignment with the Agent Console usage tracking.
Would you like a short migration example for your current language/framework or help converting your existing handler? If so, share the code snippet or tell me which language you’re using.
, with interactive elementsadorable-bear-14321
04/28/2026, 3:06 AMtall-airline-16756
04/28/2026, 3:07 AMadorable-bear-14321
04/28/2026, 5:34 AMtall-airline-16756
04/28/2026, 5:35 AMmetrics field. Below are short examples in Python and TypeScript.
Python
# listen for new conversation items (assistant messages)
@session.on("conversation_item_added")
def on_conversation_item_added(ev):
item = ev.item
if item.role != "assistant" or not item.metrics:
return
m = item.metrics
llm_ttft = m.get("llm_node_ttft") # LLM time-to-first-token
tts_ttfb = m.get("tts_node_ttfb") # TTS time-to-first-byte
stt_latency = m.get("stt_latency") # STT processing time (if present)
e2e = m.get("e2e_latency") # end-to-end latency (EOU-style)
print(f"LLM TTFT: {llm_ttft}s, TTS TTFB: {tts_ttfb}s, STT: {stt_latency}s, E2E: {e2e}s")
TypeScript
session.on("conversation_item_added", (ev) => {
const item = ev.item;
if (item.role !== "assistant" || !item.metrics) return;
const m = item.metrics;
const llm_ttft = m.llm_node_ttft; // LLM time-to-first-token
const tts_ttfb = m.tts_node_ttfb; // TTS time-to-first-byte
const stt_latency = m.stt_latency; // STT processing time (if present)
const e2e = m.e2e_latency; // end-to-end latency
console.log(`LLM TTFT: ${llm_ttft}s, TTS TTFB: ${tts_ttfb}s, STT: ${stt_latency}s, E2E: ${e2e}s`);
});
Notes
• Field names you’ll commonly see: e2e_latency, llm_node_ttft, tts_node_ttfb. STT fields may appear as stt_latency or similar when speech input is used.
• These per-message metrics replace per-turn values previously surfaced by metrics_collected. Use them on each assistant message to track per-turn LLM/STT/TTS/EOU delays.
• If you’d like, paste a sample metrics_collected payload and I’ll map each field to the new metrics fields for a direct migration., with interactive elementstall-airline-16756
04/28/2026, 5:39 AMllm_node_ttft, stt_latency, and tts_node_ttfb from event.item.metrics alongside e2e_latency. It keeps your existing E2E averaging logic and adds per-metric first/avg tracking. Paste this into your code replacing the metric section.
@session.on("conversation_item_added")
def on_conversation_item_added(event: ConversationItemAddedEvent):
global agent_avg_e2e_latency, agent_first_e2e_latency, agent_latency_count_e2e
global agent_first_llm_ttft, agent_avg_llm_ttft, agent_latency_count_llm
global agent_first_tts_ttfb, agent_avg_tts_ttfb, agent_latency_count_tts
global agent_first_stt, agent_avg_stt, agent_latency_count_stt
if getattr(event.item, "type", None) != "message":
logger.info("Non-message conversation item")
return
role = event.item.role
if role == "assistant":
role = "agent"
transcription = TranscriptionLog(
role=role, transcription=event.item.text_content,
time=datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
)
transcriptions.append(transcription)
if event.item.role is not None and event.item.role == 'assistant':
try:
item_metrics = getattr(event.item, "metrics", None)
# Helper to safely read metric from dict or object
def get_metric(mobj, key):
if mobj is None:
return None
if isinstance(mobj, dict):
return mobj.get(key)
return getattr(mobj, key, None)
e2e_value = get_metric(item_metrics, "e2e_latency")
llm_ttft = get_metric(item_metrics, "llm_node_ttft")
tts_ttfb = get_metric(item_metrics, "tts_node_ttfb")
stt_latency = get_metric(item_metrics, "stt_latency")
# E2E handling (existing logic)
if e2e_value is not None and e2e_value >= 0:
logger.info("E2E latency: %.3fs", e2e_value, extra={"e2e_latency_val": round(e2e_value, 3)})
if agent_first_e2e_latency == -1:
agent_first_e2e_latency = round(e2e_value, 3)
if agent_avg_e2e_latency == -1:
agent_avg_e2e_latency = round(e2e_value, 3)
agent_latency_count_e2e = 1
else:
agent_latency_count_e2e += 1
agent_avg_e2e_latency = round(
agent_avg_e2e_latency
+ (e2e_value - agent_avg_e2e_latency)
/ agent_latency_count_e2e,
3,
)
# LLM TTFT averaging
if llm_ttft is not None and llm_ttft >= 0:
logger.info("LLM TTFT: %.3fs", llm_ttft)
if 'agent_first_llm_ttft' not in globals():
agent_first_llm_ttft = -1
agent_avg_llm_ttft = -1
agent_latency_count_llm = 0
if agent_first_llm_ttft == -1:
agent_first_llm_ttft = round(llm_ttft, 3)
if agent_avg_llm_ttft == -1:
agent_avg_llm_ttft = round(llm_ttft, 3)
agent_latency_count_llm = 1
else:
agent_latency_count_llm += 1
agent_avg_llm_ttft = round(
agent_avg_llm_ttft + (llm_ttft - agent_avg_llm_ttft) / agent_latency_count_llm,
3,
)
# TTS TTFB averaging
if tts_ttfb is not None and tts_ttfb >= 0:
logger.info("TTS TTFB: %.3fs", tts_ttfb)
if 'agent_first_tts_ttfb' not in globals():
agent_first_tts_ttfb = -1
agent_avg_tts_ttfb = -1
agent_latency_count_tts = 0
if agent_first_tts_ttfb == -1:
agent_first_tts_ttfb = round(tts_ttfb, 3)
if agent_avg_tts_ttfb == -1:
agent_avg_tts_ttfb = round(tts_ttfb, 3)
agent_latency_count_tts = 1
else:
agent_latency_count_tts += 1
agent_avg_tts_ttfb = round(
agent_avg_tts_ttfb + (tts_ttfb - agent_avg_tts_ttfb) / agent_latency_count_tts,
3,
)
# STT averaging (may be None for text-only inputs)
if stt_latency is not None and stt_latency >= 0:
logger.info("STT latency: %.3fs", stt_latency)
if 'agent_first_stt' not in globals():
agent_first_stt = -1
agent_avg_stt = -1
agent_latency_count_stt = 0
if agent_first_stt == -1:
agent_first_stt = round(stt_latency, 3)
if agent_avg_stt == -1:
agent_avg_stt = round(stt_latency, 3)
agent_latency_count_stt = 1
else:
agent_latency_count_stt += 1
agent_avg_stt = round(
agent_avg_stt + (stt_latency - agent_avg_stt) / agent_latency_count_stt,
3,
)
except Exception as e:
logger.error(f"Failed to update latency metrics: {e}")
is_hangup = check_hangup(event.item.text_content)
if is_hangup:
asyncio.create_task(hangup_with_reason(ctx, participant, "Agent Speech Hangup"))
else:
is_voicemail = check_voicemail(event.item.text_content)
is_customer_hold = check_on_hold(event.item.text_content)
if is_voicemail:
asyncio.create_task(hangup_with_reason(ctx, participant, "Voicemail detected"))
elif is_customer_hold:
asyncio.create_task(hangup_with_reason(ctx, participant, "Customer Hold detected"))
Notes:
• Metric keys used: e2e_latency, llm_node_ttft, tts_node_ttfb, stt_latency. Adjust keys if your payload uses slightly different names.
• If you prefer storing per-turn values instead of averages, push each metric to a list (e.g., llm_values.append(llm_ttft)) and compute stats later.
• Tell me if you want the snippet adapted to store per-turn timestamps or to emit these metrics to your monitoring system (Prometheus, logs, etc.)., with interactive elementsadorable-bear-14321
04/28/2026, 1:15 PMtall-airline-16756
04/28/2026, 1:15 PMmetrics_collected log (0.1207s) versus the new per-message metrics on conversation_item_added (tts_node_ttfb = 0.1841s) and asking which to trust.
Short answer: rely on the per-message metrics on conversation_item_added (tts_node_ttfb) for per-turn TTS latency.
Why they can differ:
• Measurement point: metrics_collected may capture TTS timing at a different stage (e.g., the TTS provider callback or a pipeline step) whereas conversation_item_added.metrics reports the end‑to‑end timing attached to the assistant message. Those different capture points produce different TTFB numbers.
• Aggregation/rounding: legacy metrics may be sampled, aggregated, or pre-processed before emission; per-message metrics are tied to the specific assistant turn and reflect the full pipeline visible to the session.
• Context differences: metrics_collected entries can be emitted from separate subsystems (provider-specific telemetry) while conversation_item_added.metrics is the canonical, session-scoped breakdown (LLM / TTS / STT / e2e) for that turn.
• Timing skew: clocks, processing delays, or when the event is emitted can cause small discrepancies.
Recommendation:
• For per-turn latency and consistent tracking across LLM/STT/TTS/EOU, use conversation_item_added → item.metrics (e.g., tts_node_ttfb, llm_node_ttft, stt_latency, e2e_latency).
• If you still want provider-level telemetry (provider SDK logs), keep metrics_collected alongside conversation_item_added for correlation, but treat conversation_item_added.metrics as the single source of truth for per-turn metrics.
If you want, I can:
• show a short snippet to correlate the two events by job_id/room_id and timestamp, or
• add logging that records both values side-by-side for a period so you can compare them in your environment. Which would you prefer?, with interactive elementsadorable-bear-14321
04/30/2026, 11:31 AMtall-airline-16756
04/30/2026, 11:31 AMmetrics_collected differ from the per-turn tts_node_ttfb on conversation_item_added (and which to trust). Short answer: use the conversation_item_added per-message metrics for per-turn TTFB. Here’s why they can differ.
Why the numbers differ
• Different measurement points: metrics_collected can capture timing at a provider or pipeline hook (provider SDK, network callback, or an earlier pipeline stage). conversation_item_added.metrics reports the timing attached to the assistant message after the system’s end-to-end processing. Those capture points produce different values.
• Pipeline coverage: `conversation_item_added`’s tts_node_ttfb reflects the pipeline stage the platform considers the canonical TTS first-byte time for that assistant turn; provider logs in metrics_collected may omit or include extra network/setup time or run earlier in the flow.
• Aggregation vs. per-turn: metrics_collected averages may be pre-aggregated, sampled, or rolled up across many turns or jobs (smoothing or downsampling), while conversation_item_added is a direct per-turn measurement.
• Timing/emit delays and clock skew: events may be emitted at different times, or measured with slightly different clocks, causing small-to-moderate deltas.
• Instrumentation differences: provider-specific telemetry may compute TTFB differently (e.g., time to receive first audio byte from Azure vs. time the system attaches to the message record).
• Presence of retries/latency spikes: metrics_collected averages can be affected by spikes or retries at the provider level that aren’t reflected identically in per-message metrics.
Which to rely on
• For per-turn latency, monitoring of LLM/STT/TTS per message, and alerting on user-visible latency: rely on conversation_item_added → item.metrics (e.g., tts_node_ttfb, llm_node_ttft, stt_latency, e2e_latency).
• Keep metrics_collected or provider logs only for provider-level debugging or correlation (e.g., to investigate provider-side anomalies).
If you want, I can:
• provide a short snippet to log both values side‑by‑side for correlation, or
• give a small script to join/compare metrics_collected and conversation_item_added by timestamps/job_id so you can quantify the offset. Which would help you most?, with interactive elementsadorable-bear-14321
04/30/2026, 11:32 AMtall-airline-16756
04/30/2026, 11:33 AMconversation_item_added (CA) and metrics_collected. Good observation. Quick, focused guidance:
• If the per‑turn averages are identical, that means both telemetry sources are ultimately measuring the same TTS first‑byte event for those turns (either the provider telemetry and the session‑scoped metric are aligned, or your instrumentation is correlating them by job/turn). That’s expected when provider timing and session attaching happen at the same logical point in the pipeline.
• Why they can still differ in general (brief recap):
◦ They can be measured at different pipeline points (provider callback vs session message attachment).
◦ One source can be aggregated/sampled while the other is per‑turn.
◦ Emission timing or clock skew can create discrepancies.
• Which to use:
◦ Use conversation_item_added → item.metrics.tts_node_ttfb as your canonical per‑turn TTFB for monitoring, alerts, and per‑turn dashboards.
◦ Keep metrics_collected or provider logs for correlation or provider‑level troubleshooting if you need to investigate root cause or provider-side anomalies.
• Next step offer:
◦ If you want, I can provide a tiny snippet to log both values side‑by‑side (CA vs metrics_collected) matched by job_id/turn so you can verify alignment across many turns. Do you want that snippet (Python or TypeScript)?, with interactive elementsadorable-bear-14321
04/30/2026, 11:34 AMconversation_item_added in comparison to the ttfb coming from the metrics_collected evetntall-airline-16756
04/30/2026, 11:35 AMconversation_item_added (CA) versus metrics_collected. Here’s the concise explanation and recommended action.
Why CA TTFB can be higher than provider metrics_collected TTFB
• Measurement boundary: metrics_collected often reports provider-level TTFB (time from request to provider first audio byte). CA’s tts_node_ttfb is attached after the platform’s internal pipeline, so it can include provider TTFB plus extra platform work.
• Pipeline coverage: CA can incorporate queuing, internal processing (formatting, buffering, encoding/decoding), and time to attach the audio to the assistant message. Provider telemetry usually stops at the provider boundary.
• Emission timing / event placement: metrics_collected may be emitted earlier (immediately on provider callback). conversation_item_added is emitted when the assistant message is finalized for the session, adding post‑provider overhead.
• Instrumentation differences: The two sources may use different start/end timestamps or clock references, producing systematic offsets even with identical averaging math.
• Retries/spikes & sampling: If provider logs exclude retries or are sampled/deduplicated differently, their average can be lower than the end‑to‑end CA average that reflects retries or queuing delays.
Which to rely on
• For per‑turn, user‑visible latency and alerting: rely on conversation_item_added.item.metrics.tts_node_ttfb (it reflects the end‑to‑end time seen by the session).
• For provider debugging or correlating provider-side failures: keep metrics_collected as a complementary source.
If you want, I can give a short snippet to:
• Log CA vs provider TTFB side‑by‑side per turn, or
• Join/compare both sources by job_id/timestamp to quantify the systematic offset across many turns.
Which snippet/language would you like?
, with interactive elementsadorable-bear-14321
04/30/2026, 12:17 PMtall-airline-16756
04/30/2026, 12:18 PMtts_node_ttfb, but its behaviour is documented in the Agent latency/metrics guidance and code examples. Key points you can rely on:
• What tts_node_ttfb means: time from the TTS node starting to process the text until the first audio frame is produced — i.e., node‑level first‑byte time that includes local processing (synthesis, buffering/encoding) in addition to the provider response.
• Why it can be higher than provider TTFB in `metrics_collected`: `conversation_item_added`’s tts_node_ttfb is measured after the platform’s internal pipeline and can include provider time plus platform processing, buffering, event emission timing, and any retries — while metrics_collected often reports provider‑side timing alone.
• Which to use: treat conversation_item_added.item.metrics.tts_node_ttfb as the canonical per‑turn TTFB for user‑visible latency. Keep metrics_collected for provider‑level correlation.
Reference links:
• Build Your First AI Voice Agent (includes conversation_item_added examples and metrics): https://livekit.com/blog/build-your-first-ai-voice-agent-python
If you want, I can:
• give a tiny snippet to log/compare tts_node_ttfb vs provider tts_ttfb side‑by‑side per turn, or
• produce a short join script to quantify the systematic offset across many turns. Which would help? Sources: Build Your First AI Voice Agent in Python: Complete Tutorial | LiveKit | Build Your First AI Voice Agent in Python: Complete Tutorial | LiveKit, with interactive elementsadorable-bear-14321
05/04/2026, 5:59 PMtall-airline-16756
05/04/2026, 6:00 PMadorable-bear-14321
05/04/2026, 6:09 PMadorable-bear-14321
05/06/2026, 7:36 PM