metrics_collected is deprecated. Use session_usage...
# ask-ai
a
metrics_collected is deprecated. Use session_usage_updated for usage tracking and ChatMessage.metrics for per-turn latency.
t
Thanks for reaching out, Harshita. I'm an AI assistant and I'm here to help. Give me a moment while I research this topic.
Thanks — I see you’re noting that
metrics_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 elements
a
but still if i use metrics_collected its working
t
You're right that
metrics_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 elements
a
please give snippets of the usage of the new one
t
Thanks — I’ll show concise examples for both session-level usage (session_usage_updated) and per-turn latency (ChatMessage.metrics) in Python and TypeScript. Which language do you want first? If both, I’ll provide both short snippets. If you already have a snippet using metrics_collected, paste it and I’ll convert it for you. 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, with interactive elements
a
just how we got track of the llm, stt,tts,eou delay from metrics_collected event if this is deprecated, how do i track it per turn ? give snippet
t
Thanks — I’ll show how to get per-turn LLM, STT, TTS, and end‑of‑utterance (EOU) delays using the assistant message
metrics
field. Below are short examples in Python and TypeScript. Python
Copy code
# 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
Copy code
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 elements
Thanks — I updated your handler to extract per-turn
llm_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.
Copy code
@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 elements
a
why there is difference between the ttfb coming from on_conversation_item_added event and metrics_collected event {"message": "TTS metrics", "level": "INFO", "name": "agent", "model_name": "unknown", "model_provider": "Azure TTS", "ttfb": 0.12070749996928498, "audio_duration": 2.13, "bot_name": "Simran", "pid": 13711, "job_id": "AJ_rcBaSHJ4gzn8", "room_id": "RM_PDonwebSN3dX", "timestamp": "2026-04-28T131320.174521+00:00"} metrics in conversation item added: {'started_speaking_at': 1777381999.900089, 'stopped_speaking_at': 1777382004.936586, 'tts_node_ttfb': 0.18415445898426697} which to rely
t
Thanks — I see you’re comparing TTS TTFB from a legacy
metrics_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 elements
a
why is the ttfb so different from the ttfb avg from metrics_collected event compared to the conversation_item_added event (ca) in the below table??
t
Thanks — I understand you’re asking why the TTFB averages from
metrics_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 elements
a
this is the same per turn avg for both ca and metrics_collected
t
Thanks — I see you’re saying the per‑turn average is the same for both
conversation_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 elements
a
way of calculating avg is same per turn for both ca and metrics_collected. even tho the ttfb is diffent why is it so high in
conversation_item_added
in comparison to the ttfb coming from the metrics_collected evetn
t
Thanks — I understand you’re seeing the same per‑turn averaging method but a noticeably higher TTFB in
conversation_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 elements
a
is there any document refering this tts_node_ttfb behaviour
t
Good question — there isn’t a separate, dedicated doc page that only describes
tts_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 elements
a
min_endpointing_delay, max_endpointing_delay, false_interruption_timeout, resume_false_interruption, allow_interruptions, discard_audio_if_uninterruptible, min_interruption_duration, preemptive_generation, min_interruption_words, turn_detection are deprecated and will be removed in v2.0. Use turn_handling=TurnHandlingOptions(...) instead
t
I don't have the answer you're looking for. You could also try asking your question: • in one of the other Slack channels or • to https://deepwiki.com/livekit/livekit_composite which is trained on all LiveKit source code If you find the answer, please post it here to help others!
a
what is the correct way of implementing the turnHandlingOptions in agentsession
how will I get stt metrics from on_conversation_item_added event which I used to get from on_metrics_collected event and how will I get realtime metrics per turn ?