boundless-author-7811
02/26/2026, 12:32 PMtall-airline-16756
02/26/2026, 12:32 PMtall-airline-16756
02/26/2026, 12:32 PMLLMMetrics event in LiveKit agents. To track tokens-per-minute, aggregate Total Tokens (or Prompt/Completion separately if you need breakdowns) across a one-minute window and optionally use Tokens/Second to spot performance bottlenecks or spikes.
If you want, I can help with next steps:
• Show an example aggregation query or script to compute tokens/minute from LLMMetrics.
• Recommend thresholds/alerts to catch over-usage.
• Help you decide whether to monitor prompt vs completion tokens separately for cost vs performance visibility.
Which of those would be most useful right now?
Sources: LLM Metrics | LiveKit Documentation | Agents (Node.js) | livekit/livekit_composite | DeepWikiboundless-author-7811
02/26/2026, 1:08 PMtall-airline-16756
02/26/2026, 1:09 PMLLMMetrics / metrics_collected events on agent sessions, so you can aggregate total_tokens over one‑minute windows to compute tokens/minute for real-time usage.
If you want, I can:
• Show a small example (code or aggregation query) to compute tokens/minute from metrics_collected events.
• Recommend alert thresholds or aggregation cadence for real-time workloads.
Which would help you most right now?
Sources: Agents (Node.js) | livekit/livekit_composite | DeepWiki | LLM Metrics | LiveKit Documentation | examples/voice_agents | livekit/livekit_composite | DeepWikiboundless-author-7811
02/26/2026, 1:09 PMtall-airline-16756
02/26/2026, 1:10 PMprompt_tokens are constant or recalculated each time. Good question.
Prompt tokens are recalculated for every request. Each time you invoke the model the agent measures the number of tokens in the actual prompt content sent for that invocation and emits that value (e.g., prompt_tokens) in the LLMMetrics / metrics_collected event. In other words:
• Prompt token counts reflect the exact input for that single request and can change between calls if the prompt text, system instructions, or conversation history sent to the model changes.
• For streaming or real-time scenarios, token counts are still reported per invocation/segment so you should aggregate them over your desired time window (for example, sum total_tokens or prompt_tokens across one minute) to compute tokens/minute.
If you’d like, I can:
• Show a short example of how to aggregate prompt_tokens over a 60-second window, or
• Recommend whether to track prompt, completion, or total tokens separately for cost vs. usage visibility.
Which would you prefer?boundless-author-7811
02/26/2026, 1:19 PMtall-airline-16756
02/26/2026, 1:20 PMllm_prompt_tokens = 243,899 is very large. Most real-time and standard LLMs have context windows well below 243k tokens, so a single prompt of that size is unlikely for a single model call.
• llm_completion_tokens = 6,633 is plausible for a long generated response.
• Combined total is consistent but depends on whether the prompt count is valid.
Likely explanations
• The prompt token count may be:
◦ Aggregated across multiple turns or multiple LLM calls, not just one prompt.
◦ Counting conversation history repeatedly (e.g., every turn re-sends full history and you logged cumulative tokens).
◦ A bug in token-counting instrumentation.
◦ From a model/endpoint that supports huge context windows (confirm if you used such a model).
Recommended next steps
1. Confirm scope of this log: does it represent a single LLM invocation, the whole call session (multiple invocations), or aggregated logs across users/segments?
2. Check which model/endpoint was used for this call and its maximum context size.
3. Inspect your token-counting code/instrumentation to see whether it:
◦ Counts tokens per request only (expected), or
◦ Sums tokens across turns before emitting the metric.
1. If this was intended to be a single invocation, treat the prompt token count as a red flag and trace the sequence of LLM calls for this session.
If you want, I can:
• Show how to aggregate LLMMetrics correctly to compute tokens/minute, or
• Walk through traces/logs if you paste the per-turn metrics_collected events for this session.
Which would you like to do next?boundless-author-7811
02/26/2026, 1:42 PMtall-airline-16756
02/26/2026, 1:42 PMsummary = usage_collector.get_summary() — reads the aggregated metrics from your collector.
• llm_prompt_tokens: summary.llm_prompt_tokens and llm_completion_tokens: summary.llm_completion_tokens — use the collector’s stored counters.
• total_tokens is computed as the sum of those two values. This is correct: total tokens = prompt + completion.
Important behavior to know
• Prompt tokens are recalculated per model invocation. The collector’s llm_prompt_tokens appears to be an aggregated counter (sum of prompt tokens across whatever period/get_summary() covers), not the single-request prompt size.
• Because you’re summing the collector’s prompt tokens, a very large llm_prompt_tokens (like 243,899) most likely means the summary covers many invocations or turns — not a single prompt sent in one request.
• For real‑time/streaming models, the same applies: the collector aggregates tokens reported by each LLMMetrics / metrics_collected event.
Next steps and recommendations
1. If you want tokens/minute:
a. Record timestamps when you call get_summary() (or collect metrics_collected events) and compute the delta of total_tokens over a 60‑second window.
b. Example high-level flow:
▪︎ t0_summary = usage_collector.get_summary()
▪︎ wait 60s
▪︎ t1_summary = usage_collector.get_summary()
▪︎ tokens_in_last_minute = (t1_summary.llm_prompt_tokens + t1_summary.llm_completion_tokens) − (t0_summary.llm_prompt_tokens + t0_summary.llm_completion_tokens)
1. If you need per-call validation: capture per-invocation metrics_collected events or log the prompt/completion tokens immediately after each LLM call instead of relying only on the aggregated summary.
2. If you expect a single-invocation prompt to be <context limit> but see huge numbers, check whether:
◦ The collector is summing across turns or calls (likely).
◦ Conversation history is being resent every turn (causing repeated prompt accumulation).
◦ Your instrumentation accidentally re-adds the same tokens multiple times.
If you’d like, I can:
• Show a concrete code example that computes tokens/minute using get_summary() deltas.
• Help you adapt the collector to emit per-call metrics (or a rolling-minute window) so large aggregated values don’t confuse single-call analysis.
Which would you like next — a tokens/minute example using your current pattern, or a per‑call instrumentation example?boundless-author-7811
02/27/2026, 5:16 AMasync def _create_agent_and_session(self):
<http://self.logger.info|self.logger.info>("[%s] 🤖 Starting customer session...", self.room_name)
# Get audio configuration for noisy phone environments
audio_config = self.audio_config_service.get_noisy_environment_config()
# Create Silero VAD for local speech detection (filters noise before OpenAI)
self.silero_vad = self.audio_config_service.create_silero_vad(audio_config)
# Get turn detection config for OpenAI semantic VAD
turn_detection = self.audio_config_service.get_turn_detection_config(audio_config)
# Create Model with semantic VAD for noise resilience
model = self.model_config.create_model(self.agent_config, turn_detection)
# Create agent - tools come from agent_config.tools (ToolSchema objects)
agent = GenericAgent(
agent_config=self.agent_config,
customer_data=self.customer_data,
on_transfer_requested=None # PHASE 2: Supervisor transfer disabled
)
# Load dynamic tools (agent handles this internally)
await agent.initialize_dynamic_tools()
<http://self.logger.info|self.logger.info>("✅ Agent ready with %d tools", len(agent.tools))
# Create Session with Silero VAD and tuned interruption settings
self.agent_session = AgentSession(
llm=model,
vad=self.silero_vad, # Silero VAD for noise filtering
min_interruption_duration=audio_config.min_interruption_duration,
min_interruption_words=audio_config.min_interruption_words,
)
# Setup Metrics Collector
self.usage_collector = metrics.UsageCollector()
@self.agent_session.on("metrics_collected")
def _on_metrics_collected(ev: MetricsCollectedEvent):
metrics.log_metrics(ev.metrics)
self.usage_collector.collect(ev.metrics)
# Store metadata for tools and shutdown
self.agent_session.metadata = {
"sip_trunk_id": self.call_context.trunk_id,
"customer_id": self.customer_data.customer_id,
"customer_name": self.customer_data.customer_name,
"campaign_id": self.customer_data.campaign_id,
"call_context": self.call_context
}
<http://self.logger.info|self.logger.info>(
"[%s] ✅ Agent session created with Silero VAD (threshold=%.2f) and Semantic VAD",
self.room_name,
audio_config.silero.activation_threshold
)
return agent
def get_usage_collector(self) -> Optional[metrics.UsageCollector]:
"""Get the usage collector for metrics reporting"""
return self.usage_collector
below are the logs:
Feb 26, 2026 @ 184556.348
{"message": "RealtimeModel metrics", "level": "INFO", "name": "livekit.agents", "model_name": "gpt-realtime", "model_provider": "fpl-voicebot-realtime.openai.azure.com", "ttft": 0.83, "input_tokens": 7814, "cached_input_tokens": 6272, "input_text_tokens": 6368, "input_cached_text_tokens": 5824, "input_image_tokens": 0, "input_cached_image_tokens": 0, "input_audio_tokens": 1446, "input_cached_audio_tokens": 448, "output_tokens": 76, "output_text_tokens": 22, "output_audio_tokens": 54, "output_image_tokens": 0, "total_tokens": 7890, "tokens_per_second": 52.39, "pid": 105, "job_id": "AJ_ieL8Kn4ZvxoB", "room_id": "RM_TK7TeUrb2NVY", "timestamp": "2026-02-26T131556.347673+00:00"}
Feb 26, 2026 @ 184554.523
{"message": "RealtimeModel metrics", "level": "INFO", "name": "livekit.agents", "model_name": "gpt-realtime", "model_provider": "fpl-voicebot-realtime.openai.azure.com", "ttft": -1.0, "input_tokens": 7754, "cached_input_tokens": 7616, "input_text_tokens": 6308, "input_cached_text_tokens": 6208, "input_image_tokens": 0, "input_cached_image_tokens": 0, "input_audio_tokens": 1446, "input_cached_audio_tokens": 1408, "output_tokens": 52, "output_text_tokens": 52, "output_audio_tokens": 0, "output_image_tokens": 0, "total_tokens": 7806, "tokens_per_second": 40.15, "pid": 105, "job_id": "AJ_ieL8Kn4ZvxoB", "room_id": "RM_TK7TeUrb2NVY", "timestamp": "2026-02-26T131554.521891+00:00"}
Feb 26, 2026 @ 184550.998
{"message": "RealtimeModel metrics", "level": "INFO", "name": "livekit.agents", "model_name": "gpt-realtime", "model_provider": "fpl-voicebot-realtime.openai.azure.com", "ttft": 1.03, "input_tokens": 7551, "cached_input_tokens": 7488, "input_text_tokens": 6192, "input_cached_text_tokens": 6144, "input_image_tokens": 0, "input_cached_image_tokens": 0, "input_audio_tokens": 1359, "input_cached_audio_tokens": 1344, "output_tokens": 179, "output_text_tokens": 92, "output_audio_tokens": 87, "output_image_tokens": 0, "total_tokens": 7730, "tokens_per_second": 50.9, "pid": 105, "job_id": "AJ_ieL8Kn4ZvxoB", "room_id": "RM_TK7TeUrb2NVY", "timestamp": "2026-02-26T131550.996810+00:00"}
Feb 26, 2026 @ 184541.351
{"message": "RealtimeModel metrics", "level": "INFO", "name": "livekit.agents", "model_name": "gpt-realtime", "model_provider": "fpl-voicebot-realtime.openai.azure.com", "ttft": 0.74, "input_tokens": 7430, "cached_input_tokens": 7296, "input_text_tokens": 6156, "input_cached_text_tokens": 6080, "input_image_tokens": 0, "input_cached_image_tokens": 0, "input_audio_tokens": 1274, "input_cached_audio_tokens": 1216, "output_tokens": 86, "output_text_tokens": 26, "output_audio_tokens": 60, "output_image_tokens": 0, "total_tokens": 7516, "tokens_per_second": 54.15, "pid": 105, "job_id": "AJ_ieL8Kn4ZvxoB", "room_id": "RM_TK7TeUrb2NVY", "timestamp": "2026-02-26T131541.350560+00:00"}
Feb 26, 2026 @ 184539.043
{"message": "RealtimeModel metrics", "level": "INFO", "name": "livekit.agents", "model_name": "gpt-realtime", "model_provider": "fpl-voicebot-realtime.openai.azure.com", "ttft": -1.0, "input_tokens": 7363, "cached_input_tokens": 7296, "input_text_tokens": 6089, "input_cached_text_tokens": 6080, "input_image_tokens": 0, "input_cached_image_tokens": 0, "input_audio_tokens": 1274, "input_cached_audio_tokens": 1216, "output_tokens": 49, "output_text_tokens": 49, "output_audio_tokens": 0, "output_image_tokens": 0, "total_tokens": 7412, "tokens_per_second": 37.43, "pid": 105, "job_id": "AJ_ieL8Kn4ZvxoB", "room_id": "RM_TK7TeUrb2NVY", "timestamp": "2026-02-26T131539.042601+00:00"}
Feb 26, 2026 @ 184530.127
{"message": "RealtimeModel metrics", "level": "INFO", "name": "livekit.agents", "model_name": "gpt-realtime", "model_provider": "fpl-voicebot-realtime.openai.azure.com", "ttft": 1.04, "input_tokens": 7176, "cached_input_tokens": 7104, "input_text_tokens": 6042, "input_cached_text_tokens": 6016, "input_image_tokens": 0, "input_cached_image_tokens": 0, "input_audio_tokens": 1134, "input_cached_audio_tokens": 1088, "output_tokens": 159, "output_text_tokens": 37, "output_audio_tokens": 122, "output_image_tokens": 0, "total_tokens": 7335, "tokens_per_second": 63.83, "pid": 105, "job_id": "AJ_ieL8Kn4ZvxoB", "room_id": "RM_TK7TeUrb2NVY", "timestamp": "2026-02-26T131530.127032+00:00"}
Feb 26, 2026 @ 184507.950
{"message": "RealtimeModel metrics", "level": "INFO", "name": "livekit.agents", "model_name": "gpt-realtime", "model_provider": "fpl-voicebot-realtime.openai.azure.com", "ttft": 0.86, "input_tokens": 6624, "cached_input_tokens": 6528, "input_text_tokens": 5921, "input_cached_text_tokens": 5888, "input_image_tokens": 0, "input_cached_image_tokens": 0, "input_audio_tokens": 703, "input_cached_audio_tokens": 640, "output_tokens": 527, "output_text_tokens": 111, "output_audio_tokens": 416, "output_image_tokens": 0, "total_tokens": 7151, "tokens_per_second": 97.31, "pid": 105, "job_id": "AJ_ieL8Kn4ZvxoB", "room_id": "RM_TK7TeUrb2NVY", "timestamp": "2026-02-26T131507.949664+00:00"}
Feb 26, 2026 @ 184502.329
{"message": "RealtimeModel metrics", "level": "INFO", "name": "livekit.agents", "model_name": "gpt-realtime", "model_provider": "fpl-voicebot-realtime.openai.azure.com", "ttft": 0.87, "input_tokens": 6522, "cached_input_tokens": 768, "input_text_tokens": 5905, "input_cached_text_tokens": 768, "input_image_tokens": 0, "input_cached_image_tokens": 0, "input_audio_tokens": 617, "input_cached_audio_tokens": 0, "output_tokens": 492, "output_text_tokens": 105, "output_audio_tokens": 387, "output_image_tokens": 0, "total_tokens": 7014, "tokens_per_second": 97.58, "pid": 105, "job_id": "AJ_ieL8Kn4ZvxoB", "room_id": "RM_TK7TeUrb2NVY", "timestamp": "2026-02-26T131502.328733+00:00"}
Feb 26, 2026 @ 184448.024
{"message": "RealtimeModel metrics", "level": "INFO", "name": "livekit.agents", "model_name": "gpt-realtime", "model_provider": "fpl-voicebot-realtime.openai.azure.com", "ttft": 0.99, "input_tokens": 6348, "cached_input_tokens": 768, "input_text_tokens": 5856, "input_cached_text_tokens": 768, "input_image_tokens": 0, "input_cached_image_tokens": 0, "input_audio_tokens": 492, "input_cached_audio_tokens": 0, "output_tokens": 136, "output_text_tokens": 39, "output_audio_tokens": 97, "output_image_tokens": 0, "total_tokens": 6484, "tokens_per_second": 65.66, "pid": 105, "job_id": "AJ_ieL8Kn4ZvxoB", "room_id": "RM_TK7TeUrb2NVY", "timestamp": "2026-02-26T131448.024238+00:00"}
Feb 26, 2026 @ 184435.940
{"message": "RealtimeModel metrics", "level": "INFO", "name": "livekit.agents", "model_name": "gpt-realtime", "model_provider": "fpl-voicebot-realtime.openai.azure.com", "ttft": 0.81, "input_tokens": 6091, "cached_input_tokens": 6016, "input_text_tokens": 5793, "input_cached_text_tokens": 5760, "input_image_tokens": 0, "input_cached_image_tokens": 0, "input_audio_tokens": 298, "input_cached_audio_tokens": 256, "output_tokens": 230, "output_text_tokens": 53, "output_audio_tokens": 177, "output_image_tokens": 0, "total_tokens": 6321, "tokens_per_second": 66.1, "pid": 105, "job_id": "AJ_ieL8Kn4ZvxoB", "room_id": "RM_TK7TeUrb2NVY", "timestamp": "2026-02-26T131435.939832+00:00"}
Feb 26, 2026 @ 184425.097
{"message": "RealtimeModel metrics", "level": "INFO", "name": "livekit.agents", "model_name": "gpt-realtime", "model_provider": "fpl-voicebot-realtime.openai.azure.com", "ttft": 1.08, "input_tokens": 5893, "cached_input_tokens": 5824, "input_text_tokens": 5738, "input_cached_text_tokens": 5696, "input_image_tokens": 0, "input_cached_image_tokens": 0, "input_audio_tokens": 155, "input_cached_audio_tokens": 128, "output_tokens": 176, "output_text_tokens": 45, "output_audio_tokens": 131, "output_image_tokens": 0, "total_tokens": 6069, "tokens_per_second": 71.67, "pid": 105, "job_id": "AJ_ieL8Kn4ZvxoB", "room_id": "RM_TK7TeUrb2NVY", "timestamp": "2026-02-26T131425.097267+00:00"}
Feb 26, 2026 @ 184414.650
{"message": "RealtimeModel metrics", "level": "INFO", "name": "livekit.agents", "model_name": "gpt-realtime", "model_provider": "fpl-voicebot-realtime.openai.azure.com", "ttft": 0.61, "input_tokens": 5679, "cached_input_tokens": 768, "input_text_tokens": 5679, "input_cached_text_tokens": 768, "input_image_tokens": 0, "input_cached_image_tokens": 0, "input_audio_tokens": 0, "input_cached_audio_tokens": 0, "output_tokens": 194, "output_text_tokens": 49, "output_audio_tokens": 145, "output_image_tokens": 0, "total_tokens": 5873, "tokens_per_second": 83.29, "pid": 105, "job_id": "AJ_ieL8Kn4ZvxoB", "room_id": "RM_TK7TeUrb2NVY", "timestamp": "2026-02-26T131414.649565+00:00"}tall-airline-16756
02/27/2026, 5:16 AMmetrics_collected / LLMMetrics). Fields you see:
◦ input_tokens / input_text_tokens / `input_audio_tokens`: tokens attributable to the request input (split by modality).
◦ cached_input_tokens / `input_cached_*`: input tokens that were served from cache.
◦ output_tokens / output_text_tokens / `output_audio_tokens`: tokens produced by the model for that invocation.
◦ `total_tokens`: sum of input + output tokens for that invocation.
◦ `tokens_per_second`: throughput measured for that invocation.
◦ timestamp, job_id, `room_id`: identify the invocation/session.
Why your usage_collector pattern is correct (and what it does)
• Your @agent_session.on("metrics_collected") handler calls self.usage_collector.collect(ev.metrics) for each invocation — that correctly aggregates the per-invocation metrics into the collector.
• usage_collector.get_summary() therefore returns aggregated counters (sum of prompt/input tokens and sum of completion/output tokens) for the time period since the collector was created or last reset — not the size of a single prompt.
• Your computed total_tokens = summary.llm_prompt_tokens + summary.llm_completion_tokens is mathematically correct for the summary.
Why you may see large prompt counts
• The summary sums tokens across many invocations. Your logs show many invocations with ~6–8k input tokens each; summing those over dozens of invocations in a call/session will produce large aggregated numbers.
• Realtime runs with audio + text tokens will show substantial input token counts per invocation (audio tokens add up quickly).
• Cached tokens reduce billable work but often still appear in metrics (see cached_input_tokens) — confirm whether your collector counts cached tokens and whether you want to exclude them for cost calculations.
How to compute tokens/minute (recommended)
1. Use delta of summary counters across a fixed 60-second window rather than the raw summary value.
2. Steps:
a. t0 = now; s0 = usage_collector.get_summary()
b. wait 60 seconds
c. t1 = now; s1 = usage_collector.get_summary()
d. tokens_in_last_minute = (s1.llm_prompt_tokens + s1.llm_completion_tokens) − (s0.llm_prompt_tokens + s0.llm_completion_tokens)
e. If you want prompt vs completion breakdown, compute deltas per field separately.
Example (pseudo-async):
s0 = usage_collector.get_summary()
await asyncio.sleep(60)
s1 = usage_collector.get_summary()
prompt_delta = s1.llm_prompt_tokens - s0.llm_prompt_tokens
completion_delta = s1.llm_completion_tokens - s0.llm_completion_tokens
tokens_per_minute = prompt_delta + completion_delta
Practical checks to validate your data
• Confirm whether usage_collector.collect() includes cached tokens. If you want billable tokens only, exclude cached counts.
• Correlate per-invocation logs (the JSON lines you pasted) with collector deltas over the same time window to verify aggregation is correct.
• If you expect much smaller per-invocation input_tokens, inspect what is being sent as prompt each turn (conversation history + system instructions + audio transcription). Re-sending long histories each turn inflates input tokens.
Suggested next steps I can help with
• Provide a concrete code snippet to compute rolling tokens/minute (e.g., 60s sliding window).
• Show how to exclude cached tokens from your summary if needed.
• Help you trace a specific session: take a 1-minute range of the per-invocation logs you pasted and I’ll compute the expected summary delta to confirm the collector’s behavior.
Which would you like next — a rolling-window code sample, or a verification of a specific time window from your logs?boundless-author-7811
02/27/2026, 5:18 AMtall-airline-16756
02/27/2026, 5:19 AMllm_prompt_tokens is the aggregated sum of the per‑invocation input_tokens values (or whichever input field your collector maps to) over the session. That explains why the summary is much larger than any single invocation (which shows ~6–8k input tokens).
Why this happens (step‑by‑step)
1. Each JSON log line is a metrics snapshot for one model invocation, e.g.:
◦ total_tokens ~ 7k–8k
◦ input_tokens (prompt/input) ~ 6k–7k for that invocation
1. Your handler runs on every metrics_collected event and calls self.usage_collector.collect(ev.metrics). The collector increments its internal counters by the values in each event.
2. Over the entire call/session you logged many invocations (from ~131414 through ~131556 in your sample). Summing ~6–8k input tokens across those invocations produces an aggregated prompt token total on the order of tens of thousands — e.g., 82,245.
Concrete verification approach you can run quickly
1. From the logs you pasted, pick a contiguous time window (for example between 131414.649565 and 131556.347673) and sum the input_tokens values from each JSON line in that window.
2. The sum of those per‑invocation input_tokens should match (or be very close to) the llm_prompt_tokens value reported by your summary at call end.
3. If your collector includes cached tokens, also sum cached_input_tokens and compare; decide whether you want them counted in your “prompt tokens” metric.
Things to check if the number still looks off
• Are you summing the same field the logs call input_tokens? Make sure summary.llm_prompt_tokens maps to the same metric (input_tokens or input_text_tokens) you expect.
• Is your collector double‑counting events? Ensure collect() is only called once per metrics_collected event and that retries/duplicates in logs aren’t being re‑collected.
• Are cached tokens included? If you want billable (non‑cached) prompt tokens only, subtract cached counts: prompt_billable = input_tokens − cached_input_tokens (per event) before aggregating.
• Confirm the time span: the summary may cover the whole call lifetime; if you expected per‑call or per‑minute values, compute deltas over the desired window instead of using the full-session summary.
If you want, I can:
• Walk through the logs you pasted and sum the input_tokens values to show the arithmetic that leads to ~82k.
• Provide a small script you can run to compute the per‑invocation sum (and optionally subtract cached tokens) and compare to usage_collector.get_summary() output.
• Show a tokens/minute delta example (rolling 60s) using your collector.
Which would help most — a summed verification of the pasted logs, or a script you can run locally to verify and optionally exclude cached tokens?boundless-author-7811
02/27/2026, 5:26 AMtall-airline-16756
02/27/2026, 5:26 AMinput_tokens in the RealtimeModel log and how that number is computed. Short answer: it’s the sum of input tokens across modalities (text + audio + image), and cached tokens are a subset already included in those per‑modality fields.
Explanation with your example
• Fields in the log:
◦ input_text_tokens = 6368
◦ input_audio_tokens = 1446
◦ input_image_tokens = 0
◦ input_tokens = 7814
◦ input_cached_text_tokens = 5824
◦ input_cached_audio_tokens = 448
◦ cached_input_tokens = 6272
• Calculation:
◦ input_tokens = input_text_tokens + input_audio_tokens + input_image_tokens
◦ For your line: 6368 + 1446 + 0 = 7814 — this matches input_tokens.
About cached tokens
• Cached tokens (e.g., input_cached_text_tokens, input_cached_audio_tokens) are a subset of the per‑modality token counts, not additional tokens to add on top.
• cached_input_tokens is the sum of the cached portions and is already included in input_tokens. If you want billable (non‑cached) input tokens, subtract cached values per invocation:
◦ non_cached_input = input_tokens − cached_input_tokens
How this ties to your aggregated summary
• Your usage collector sums input_tokens from every metrics_collected event. Because each invocation here has thousands of input tokens, summing many invocations produces the large session total (e.g., 82,245 prompt tokens).
If you want, I can:
• Provide a small script to sum per‑invocation input_tokens from your logs and show it matches the summary, or
• Show a variant that subtracts cached tokens per event so your summaries report billable tokens only.
Which would you prefer?
Sources: LLM Metrics | LiveKit Documentation | TTS Metrics | LiveKit Documentation | Gemini Realtime Agent with Live Vision | LiveKit Documentation | OpenAI Realtime API plugin guide | LiveKit Documentationboundless-author-7811
02/27/2026, 5:45 AMtall-airline-16756
02/27/2026, 5:45 AMoutput_tokens, show the math with your example, and explain how this feeds into your session summary.
What makes up output_tokens
• output_tokens = output_text_tokens + output_audio_tokens + output_image_tokens
• There’s no separate “cached output” field in these logs — generated tokens are reported as-is by the model for each invocation.
Example from your log line
• output_text_tokens = 22
• output_audio_tokens = 54
• output_image_tokens = 0
• Calculation: 22 + 54 + 0 = 76 → this matches output_tokens: 76 in the log.
How output_tokens relates to total_tokens and your summary
• total_tokens in each invocation = input_tokens + output_tokens (e.g., 7814 + 76 = 7890 in that entry).
• Your usage collector aggregates per-invocation fields. It sums output_tokens (or the mapped llm_completion_tokens) across all metrics_collected events, just like it sums input_tokens for prompts.
• The llm_completion_tokens you see in the final call summary is the sum of these per-invocation output_tokens values over the session.
Practical notes
• If you want billable output only, there’s nothing to subtract (no cached output), so summing output_tokens is the correct approach.
• To validate your summary, you can sum the output_tokens values from the per-invocation logs across the session and compare to summary.llm_completion_tokens.
• If you’d like, I can provide a short script to:
◦ Sum per-invocation input_tokens and output_tokens from your logs and compare them to the usage collector summary, or
◦ Produce a tokens/minute delta that separates prompt vs completion usage.
Which would you prefer?
Sources: LLM Metrics | LiveKit Documentation | examples/voice_agents | livekit/livekit_composite | DeepWikiboundless-author-7811
02/27/2026, 5:54 AMtall-airline-16756
02/27/2026, 5:55 AMllm_prompt_tokens (82,245) is correct and show exactly how you can verify it from your per‑invocation logs.
Quick answer
• Yes — 82,245 can be correct if your usage collector summed every invocation’s input_tokens across the session. Each RealtimeModel log line contains an input_tokens value of several thousand; summing those per-invocation values across the whole call produces a large aggregate like 82,245.
Why that happens (concise)
• Each metrics_collected event reports input_tokens for that single invocation, where
◦ input_tokens = input_text_tokens + input_audio_tokens + input_image_tokens.
• Your handler calls usage_collector.collect(ev.metrics) on every event, so usage_collector.get_summary().llm_prompt_tokens is the sum of all those per‑invocation input_tokens values for the session (or since the collector was created/reset).
• If your session has many invocations (as your logs show across ~1.5 minutes) and each invocation has ~6–8k input tokens, the summed total will be tens of thousands.
How to verify exactly (step‑by‑step)
1. Extract all log lines for the session time range (call start → call end).
2. Sum the per‑line input_tokens values:
◦ sum_input = Σ line.input_tokens
1. Compare sum_input to summary.llm_prompt_tokens (your final summary value).
◦ They should match (or be off only by rounding/ordering) if the collector is summing the same field.
Optional: exclude cached tokens for billable prompt tokens
• If you want billable (non‑cached) prompt tokens instead of raw input tokens, compute per invocation:
◦ non_cached_input = input_tokens − cached_input_tokens
• Then sum those non_cached_input values across the session:
◦ sum_billable = Σ (input_tokens − cached_input_tokens)
• Compare sum_billable to the metric you want to report for billing.
Concrete verification script (pseudo-Python)
# assume logs is a list of parsed JSON log dicts for the session
sum_input = sum(line["input_tokens"] for line in logs)
sum_cached = sum(line.get("cached_input_tokens", 0) for line in logs)
sum_non_cached = sum_input - sum_cached
print("sum_input:", sum_input)
print("sum_cached:", sum_cached)
print("sum_non_cached:", sum_non_cached)
What to check if the numbers don’t match
• Ensure the collector is summing input_tokens (not another field such as input_text_tokens) and that you’re summing the same field from logs.
• Confirm collect() is called exactly once per event and that log duplication or retries aren’t causing double counting.
• Verify the time window: the final summary covers the full call/session — make sure your log slice matches that window.
If you’d like, I can:
• Sum the input_tokens values from the exact log entries you pasted and show the arithmetic that leads to ~82k, or
• Provide a small runnable script you can drop into your environment to compute both raw and billable prompt totals for any session automatically.
Which would you prefer — I can run the sum for your pasted lines, or give you the runnable script?boundless-author-7811
02/27/2026, 6:00 AMtall-airline-16756
02/27/2026, 6:00 AMtotal_tokens field across the invocations and divide by the number of log lines in the session.
• Average prompt (input) tokens per invocation
◦ Shows how much context you send each request.
◦ Formula: avg_input_per_invocation = (Σ input_tokens_i) / N_invocations
◦ Use input_tokens from each metrics line (which equals input_text_tokens + input_audio_tokens + input_image_tokens).
• Average completion (output) tokens per invocation
◦ Shows how many tokens the model generates on average.
◦ Formula: avg_output_per_invocation = (Σ output_tokens_i) / N_invocations
◦ Use output_tokens from each metrics line (output_text_tokens + output_audio_tokens + output_image_tokens).
• Tokens per second (throughput)
◦ Your logs include tokens_per_second per invocation; averaging that gives an idea of processing speed but may vary with request size.
◦ Formula: avg_tokens_per_second = (Σ tokens_per_second_i) / N_invocations
◦ Or compute overall throughput: total_tokens_over_period / total_seconds
• Tokens per minute (session or rolling)
◦ Useful for rate limits and billing spikes.
◦ Compute token delta over a 60‑second window:
▪︎ tokens_per_minute = (summary_at_t1.total_tokens − summary_at_t0.total_tokens)
◦ Or use rolling windows on per-invocation totals.
Practical formulas using your data
• Session totals (from usage_collector summary):
◦ session_prompt = summary.llm_prompt_tokens
◦ session_completion = summary.llm_completion_tokens
◦ session_total = session_prompt + session_completion
• Average tokens per second for session:
◦ session_duration_seconds = call_end_time − call_start_time
◦ avg_tokens_per_second = session_total / session_duration_seconds
• Average tokens per minute for session:
◦ avg_tokens_per_minute = avg_tokens_per_second × 60
Concrete example using your final call summary
• call_start_time = 1772111649.6190197
• call_end_time = 1772111754.533457
◦ duration ≈ 104.9144373 seconds
• llm_prompt_tokens = 82,245
• llm_completion_tokens = 2,356
• session_total = 84,601
Compute:
• avg_tokens_per_second ≈ 84,601 / 104.914 ≈ 806.3 tokens/sec
• avg_tokens_per_minute ≈ 806.3 × 60 ≈ 48,376 tokens/min
Notes about interpretation
• These are session averages — they smooth over bursts and idle times. Real‑time calls often have bursts when audio accumulates or when the model emits longer responses.
• Per‑invocation averages (from the individual metrics lines you pasted) will usually be smaller and show the per‑turn behavior (e.g., ~7–8k input_tokens per invocation in your logs).
• tokens_per_second in each log line measures instantaneous throughput for that invocation and is helpful to find slow or fast invocations.
Billable vs non‑billable (cached) tokens
• If you want billable averages, subtract cached tokens:
◦ billable_prompt = input_tokens − cached_input_tokens (per invocation)
◦ billable_total = billable_prompt + output_tokens
◦ Sum these billable values across the session and use the same formulas above.
Suggested checks and next steps
• To validate your summary vs logs: sum per-invocation input_tokens and output_tokens from the metrics lines and compare to the usage_collector summary.
• If you want rolling metrics: take successive snapshots of usage_collector.get_summary() and compute deltas over 60s windows.
• If you want, I can:
◦ Provide a short script that parses your log lines and computes per‑invocation and session averages (including billable versus cached), or
◦ Compute the averages for the exact log lines you provided and show the step‑by‑step math.
Which would you prefer — a runnable script you can drop into your environment, or that step‑by‑step computation on your pasted logs?boundless-author-7811
02/27/2026, 6:06 AMtall-airline-16756
02/27/2026, 6:07 AMmetrics_collected event and sums those per‑invocation values. That produces an aggregated session total (summary.llm_prompt_tokens and summary.llm_completion_tokens).
• In your pasted logs, each invocation had ~6–8k input_tokens. Summing dozens of those invocations across the session naturally yields tens of thousands of prompt tokens (e.g., 82,245). So the arithmetic and the aggregation model are consistent and plausible for real‑time audio + text streams.
What to check if you want to confirm the reported totals
1. Verify you’re summing the same field:
◦ Confirm summary.llm_prompt_tokens corresponds to per‑invocation input_tokens.
1. Check cached token handling:
◦ cached_input_tokens (and input_cached_*) are subsets of input_tokens. If you want billable tokens only, compute per‑invocation non_cached_input = input_tokens − cached_input_tokens, then sum those.
1. Ensure no double‑collection:
◦ Confirm collect() is called exactly once per metrics_collected event and no duplicate events are being re‑ingested.
1. Match time windows:
◦ Make sure the logs you sum cover the same call_start_time → call_end_time used in the final summary.
Quick verification you can run
• Sum per‑invocation inputs from your logs and compare to the summary:
sum_input = sum(line["input_tokens"] for line in logs_for_session)
sum_output = sum(line["output_tokens"] for line in logs_for_session)
sum_total = sum_input + sum_output
# Compare sum_input to summary.llm_prompt_tokens, sum_output to summary.llm_completion_tokens
• Or compute billable prompt tokens:
sum_billable_prompt = sum(line["input_tokens"] - line.get("cached_input_tokens", 0) for line in logs_for_session)
When totals would be incorrect
• If the collector is double‑counting events, if your log slice doesn’t match the summary window, or if you accidentally summed different fields (e.g., summing input_text_tokens + input_cached_audio_tokens incorrectly), the final number could be wrong. Otherwise, the cumulative pattern you see is expected.
If you want, I can:
• Sum the exact per‑invocation input_tokens and output_tokens from the log lines you pasted and show the arithmetic that leads to ~82k prompt tokens, or
• Provide a small script you can run against your full logs to compute raw vs billable (non‑cached) tokens and confirm the usage_collector summary.
Which would help you most — verification of your pasted lines, or a runnable script for your environment?
Sources: LLM Metrics | LiveKit Documentation | DeepSeek LLM | LiveKit Documentationboundless-author-7811
03/04/2026, 9:00 AMasync def _create_agent_and_session(self):
<http://self.logger.info|self.logger.info>("[%s] 🤖 Starting customer session...", self.room_name)
# Get audio configuration for noisy phone environments
audio_config = self.audio_config_service.get_noisy_environment_config()
# Create Silero VAD for local speech detection (filters noise before OpenAI)
self.silero_vad = self.audio_config_service.create_silero_vad(audio_config)
# Get turn detection config for OpenAI semantic VAD
turn_detection = self.audio_config_service.get_turn_detection_config(audio_config)
# Create Model with semantic VAD for noise resilience
model = self.model_config.create_model(self.agent_config, turn_detection)
# Create agent - tools come from agent_config.tools (ToolSchema objects)
agent = GenericAgent(
agent_config=self.agent_config,
customer_data=self.customer_data,
on_transfer_requested=None # PHASE 2: Supervisor transfer disabled
)
# Load dynamic tools (agent handles this internally)
await agent.initialize_dynamic_tools()
<http://self.logger.info|self.logger.info>("✅ Agent ready with %d tools", len(agent.tools))
# Create Session with Silero VAD and tuned interruption settings
self.agent_session = AgentSession(
llm=model,
vad=self.silero_vad, # Silero VAD for noise filtering
min_interruption_duration=audio_config.min_interruption_duration,
min_interruption_words=audio_config.min_interruption_words,
)
# Setup Metrics Collector
self.usage_collector = metrics.UsageCollector()
@self.agent_session.on("metrics_collected")
def _on_metrics_collected(ev: MetricsCollectedEvent):
metrics.log_metrics(ev.metrics)
self.usage_collector.collect(ev.metrics)
# Store metadata for tools and shutdown
self.agent_session.metadata = {
"sip_trunk_id": self.call_context.trunk_id,
"customer_id": self.customer_data.customer_id,
"customer_name": self.customer_data.customer_name,
"campaign_id": self.customer_data.campaign_id,
"call_context": self.call_context
}
<http://self.logger.info|self.logger.info>(
"[%s] ✅ Agent session created with Silero VAD (threshold=%.2f) and Semantic VAD",
self.room_name,
audio_config.silero.activation_threshold
)
return agent
def get_usage_collector(self) -> Optional[metrics.UsageCollector]:
"""Get the usage collector for metrics reporting"""
return self.usage_collector
below are the logs:
Feb 26, 2026 @ 184556.348
{"message": "RealtimeModel metrics", "level": "INFO", "name": "livekit.agents", "model_name": "gpt-realtime", "model_provider": "fpl-voicebot-realtime.openai.azure.com", "ttft": 0.83, "input_tokens": 7814, "cached_input_tokens": 6272, "input_text_tokens": 6368, "input_cached_text_tokens": 5824, "input_image_tokens": 0, "input_cached_image_tokens": 0, "input_audio_tokens": 1446, "input_cached_audio_tokens": 448, "output_tokens": 76, "output_text_tokens": 22, "output_audio_tokens": 54, "output_image_tokens": 0, "total_tokens": 7890, "tokens_per_second": 52.39, "pid": 105, "job_id": "AJ_ieL8Kn4ZvxoB", "room_id": "RM_TK7TeUrb2NVY", "timestamp": "2026-02-26T131556.347673+00:00"}
Feb 26, 2026 @ 184554.523
{"message": "RealtimeModel metrics", "level": "INFO", "name": "livekit.agents", "model_name": "gpt-realtime", "model_provider": "fpl-voicebot-realtime.openai.azure.com", "ttft": -1.0, "input_tokens": 7754, "cached_input_tokens": 7616, "input_text_tokens": 6308, "input_cached_text_tokens": 6208, "input_image_tokens": 0, "input_cached_image_tokens": 0, "input_audio_tokens": 1446, "input_cached_audio_tokens": 1408, "output_tokens": 52, "output_text_tokens": 52, "output_audio_tokens": 0, "output_image_tokens": 0, "total_tokens": 7806, "tokens_per_second": 40.15, "pid": 105, "job_id": "AJ_ieL8Kn4ZvxoB", "room_id": "RM_TK7TeUrb2NVY", "timestamp": "2026-02-26T131554.521891+00:00"}
Feb 26, 2026 @ 184550.998
{"message": "RealtimeModel metrics", "level": "INFO", "name": "livekit.agents", "model_name": "gpt-realtime", "model_provider": "fpl-voicebot-realtime.openai.azure.com", "ttft": 1.03, "input_tokens": 7551, "cached_input_tokens": 7488, "input_text_tokens": 6192, "input_cached_text_tokens": 6144, "input_image_tokens": 0, "input_cached_image_tokens": 0, "input_audio_tokens": 1359, "input_cached_audio_tokens": 1344, "output_tokens": 179, "output_text_tokens": 92, "output_audio_tokens": 87, "output_image_tokens": 0, "total_tokens": 7730, "tokens_per_second": 50.9, "pid": 105, "job_id": "AJ_ieL8Kn4ZvxoB", "room_id": "RM_TK7TeUrb2NVY", "timestamp": "2026-02-26T131550.996810+00:00"}
Feb 26, 2026 @ 184541.351
{"message": "RealtimeModel metrics", "level": "INFO", "name": "livekit.agents", "model_name": "gpt-realtime", "model_provider": "fpl-voicebot-realtime.openai.azure.com", "ttft": 0.74, "input_tokens": 7430, "cached_input_tokens": 7296, "input_text_tokens": 6156, "input_cached_text_tokens": 6080, "input_image_tokens": 0, "input_cached_image_tokens": 0, "input_audio_tokens": 1274, "input_cached_audio_tokens": 1216, "output_tokens": 86, "output_text_tokens": 26, "output_audio_tokens": 60, "output_image_tokens": 0, "total_tokens": 7516, "tokens_per_second": 54.15, "pid": 105, "job_id": "AJ_ieL8Kn4ZvxoB", "room_id": "RM_TK7TeUrb2NVY", "timestamp": "2026-02-26T131541.350560+00:00"}
Feb 26, 2026 @ 184539.043
{"message": "RealtimeModel metrics", "level": "INFO", "name": "livekit.agents", "model_name": "gpt-realtime", "model_provider": "fpl-voicebot-realtime.openai.azure.com", "ttft": -1.0, "input_tokens": 7363, "cached_input_tokens": 7296, "input_text_tokens": 6089, "input_cached_text_tokens": 6080, "input_image_tokens": 0, "input_cached_image_tokens": 0, "input_audio_tokens": 1274, "input_cached_audio_tokens": 1216, "output_tokens": 49, "output_text_tokens": 49, "output_audio_tokens": 0, "output_image_tokens": 0, "total_tokens": 7412, "tokens_per_second": 37.43, "pid": 105, "job_id": "AJ_ieL8Kn4ZvxoB", "room_id": "RM_TK7TeUrb2NVY", "timestamp": "2026-02-26T131539.042601+00:00"}
Feb 26, 2026 @ 184530.127
{"message": "RealtimeModel metrics", "level": "INFO", "name": "livekit.agents", "model_name": "gpt-realtime", "model_provider": "fpl-voicebot-realtime.openai.azure.com", "ttft": 1.04, "input_tokens": 7176, "cached_input_tokens": 7104, "input_text_tokens": 6042, "input_cached_text_tokens": 6016, "input_image_tokens": 0, "input_cached_image_tokens": 0, "input_audio_tokens": 1134, "input_cached_audio_tokens": 1088, "output_tokens": 159, "output_text_tokens": 37, "output_audio_tokens": 122, "output_image_tokens": 0, "total_tokens": 7335, "tokens_per_second": 63.83, "pid": 105, "job_id": "AJ_ieL8Kn4ZvxoB", "room_id": "RM_TK7TeUrb2NVY", "timestamp": "2026-02-26T131530.127032+00:00"}
Feb 26, 2026 @ 184507.950
{"message": "RealtimeModel metrics", "level": "INFO", "name": "livekit.agents", "model_name": "gpt-realtime", "model_provider": "fpl-voicebot-realtime.openai.azure.com", "ttft": 0.86, "input_tokens": 6624, "cached_input_tokens": 6528, "input_text_tokens": 5921, "input_cached_text_tokens": 5888, "input_image_tokens": 0, "input_cached_image_tokens": 0, "input_audio_tokens": 703, "input_cached_audio_tokens": 640, "output_tokens": 527, "output_text_tokens": 111, "output_audio_tokens": 416, "output_image_tokens": 0, "total_tokens": 7151, "tokens_per_second": 97.31, "pid": 105, "job_id": "AJ_ieL8Kn4ZvxoB", "room_id": "RM_TK7TeUrb2NVY", "timestamp": "2026-02-26T131507.949664+00:00"}
Feb 26, 2026 @ 184502.329
{"message": "RealtimeModel metrics", "level": "INFO", "name": "livekit.agents", "model_name": "gpt-realtime", "model_provider": "fpl-voicebot-realtime.openai.azure.com", "ttft": 0.87, "input_tokens": 6522, "cached_input_tokens": 768, "input_text_tokens": 5905, "input_cached_text_tokens": 768, "input_image_tokens": 0, "input_cached_image_tokens": 0, "input_audio_tokens": 617, "input_cached_audio_tokens": 0, "output_tokens": 492, "output_text_tokens": 105, "output_audio_tokens": 387, "output_image_tokens": 0, "total_tokens": 7014, "tokens_per_second": 97.58, "pid": 105, "job_id": "AJ_ieL8Kn4ZvxoB", "room_id": "RM_TK7TeUrb2NVY", "timestamp": "2026-02-26T131502.328733+00:00"}
Feb 26, 2026 @ 184448.024
{"message": "RealtimeModel metrics", "level": "INFO", "name": "livekit.agents", "model_name": "gpt-realtime", "model_provider": "fpl-voicebot-realtime.openai.azure.com", "ttft": 0.99, "input_tokens": 6348, "cached_input_tokens": 768, "input_text_tokens": 5856, "input_cached_text_tokens": 768, "input_image_tokens": 0, "input_cached_image_tokens": 0, "input_audio_tokens": 492, "input_cached_audio_tokens": 0, "output_tokens": 136, "output_text_tokens": 39, "output_audio_tokens": 97, "output_image_tokens": 0, "total_tokens": 6484, "tokens_per_second": 65.66, "pid": 105, "job_id": "AJ_ieL8Kn4ZvxoB", "room_id": "RM_TK7TeUrb2NVY", "timestamp": "2026-02-26T131448.024238+00:00"}
Feb 26, 2026 @ 184435.940
{"message": "RealtimeModel metrics", "level": "INFO", "name": "livekit.agents", "model_name": "gpt-realtime", "model_provider": "fpl-voicebot-realtime.openai.azure.com", "ttft": 0.81, "input_tokens": 6091, "cached_input_tokens": 6016, "input_text_tokens": 5793, "input_cached_text_tokens": 5760, "input_image_tokens": 0, "input_cached_image_tokens": 0, "input_audio_tokens": 298, "input_cached_audio_tokens": 256, "output_tokens": 230, "output_text_tokens": 53, "output_audio_tokens": 177, "output_image_tokens": 0, "total_tokens": 6321, "tokens_per_second": 66.1, "pid": 105, "job_id": "AJ_ieL8Kn4ZvxoB", "room_id": "RM_TK7TeUrb2NVY", "timestamp": "2026-02-26T131435.939832+00:00"}
Feb 26, 2026 @ 184425.097
{"message": "RealtimeModel metrics", "level": "INFO", "name": "livekit.agents", "model_name": "gpt-realtime", "model_provider": "fpl-voicebot-realtime.openai.azure.com", "ttft": 1.08, "input_tokens": 5893, "cached_input_tokens": 5824, "input_text_tokens": 5738, "input_cached_text_tokens": 5696, "input_image_tokens": 0, "input_cached_image_tokens": 0, "input_audio_tokens": 155, "input_cached_audio_tokens": 128, "output_tokens": 176, "output_text_tokens": 45, "output_audio_tokens": 131, "output_image_tokens": 0, "total_tokens": 6069, "tokens_per_second": 71.67, "pid": 105, "job_id": "AJ_ieL8Kn4ZvxoB", "room_id": "RM_TK7TeUrb2NVY", "timestamp": "2026-02-26T131425.097267+00:00"}
Feb 26, 2026 @ 184414.650
{"message": "RealtimeModel metrics", "level": "INFO", "name": "livekit.agents", "model_name": "gpt-realtime", "model_provider": "fpl-voicebot-realtime.openai.azure.com", "ttft": 0.61, "input_tokens": 5679, "cached_input_tokens": 768, "input_text_tokens": 5679, "input_cached_text_tokens": 768, "input_image_tokens": 0, "input_cached_image_tokens": 0, "input_audio_tokens": 0, "input_cached_audio_tokens": 0, "output_tokens": 194, "output_text_tokens": 49, "output_audio_tokens": 145, "output_image_tokens": 0, "total_tokens": 5873, "tokens_per_second": 83.29, "pid": 105, "job_id": "AJ_ieL8Kn4ZvxoB", "room_id": "RM_TK7TeUrb2NVY", "timestamp": "2026-02-26T131414.649565+00:00"}
is there a way to reduce the total token utilized, as you can see the summmation of input_tokens is hightall-airline-16756
03/04/2026, 9:00 AMinput_tokens across the session and want ways to reduce total token utilization. I’ll keep this brief and practical: why it’s high, what you can change immediately, and small code examples to measure and reduce usage.
Why the total is high (short)
• Each metrics line is a per‑invocation snapshot. Your usage collector sums those input_tokens values across the session, so many invocations with ~6–8k input tokens each will naturally sum to tens of thousands.
• Much of that input_tokens can be cached context (your logs show large cached_input_tokens) — cached tokens inflate the reported input count but are usually not billable.
• Audio + text modality also increases input tokens per invocation (audio is tokenized internally).
Ways to reduce token usage (prioritize these)
1. Trim or summarize conversation history
◦ Remove or summarize older messages so less context is sent each turn.
◦ Options: delete oldest conversation items, replace long history with a brief summary, or keep only N last turns.
1. Shorten tool and system prompts
◦ Strip long tool descriptions or examples; keep only compact, essential instructions.
◦ Move large static docs to an external retrieval step instead of embedding them in every prompt.
1. Restart/rotate sessions
◦ Limit session lifetime (e.g., restart after X minutes or Y turns) so context doesn’t grow unbounded.
1. Exclude cached tokens for billing-sensitive metrics
◦ For billable counts, subtract cached_input_tokens per invocation:
non_cached_input = input_tokens − cached_input_tokens
1. Use text-only output + external TTS (if acceptable)
◦ Generate text responses in the realtime model and synthesize audio via an external, cheaper TTS. This reduces output/audio tokens.
1. Tune VAD / turn detection
◦ Ensure Silero VAD and semantic turn detection only call the model when a real utterance is complete to avoid extra short invocations.
How to measure per-turn and billable usage (code)
• Track deltas per metrics_collected so you can see actual tokens added each turn and exclude cached tokens:
class TokenTracker:
def __init__(self):
self.prev_input = 0
self.prev_output = 0
def on_metrics(self, m):
delta_input = m.input_tokens - self.prev_input
delta_output = m.output_tokens - self.prev_output
# billable input for this turn
cached = getattr(m, "cached_input_tokens", 0)
billable_input = delta_input - cached if delta_input > cached else 0
self.prev_input = m.input_tokens
self.prev_output = m.output_tokens
return delta_input, delta_output, billable_input
• Or compute tokens/minute via summary deltas:
s0 = usage_collector.get_summary()
await asyncio.sleep(60)
s1 = usage_collector.get_summary()
tokens_last_min = (s1.llm_prompt_tokens + s1.llm_completion_tokens) - (s0.llm_prompt_tokens + s0.llm_completion_tokens)
Practical small steps to try now
• Run the per‑invocation delta script above for a session and compare:
◦ raw per‑turn input vs cached_input_tokens
◦ summed deltas vs your final summary.llm_prompt_tokens — they should match
• Shorten tool descriptions and test token impact
• If acceptable, switch outputs to text-only and use external TTS for a test session to quantify savings
If you want I can:
• Sum the input_tokens in the exact log lines you gave and show the arithmetic matching your 82,245 total.
• Provide a runnable script you can drop into your service to compute per-turn billable tokens and a rolling tokens/minute meter.
Which would you like next — the exact sum verification, or the runnable per‑turn/billing script?
Sources: examples/voice_agents | livekit/livekit_composite | DeepWiki | Realtime models overview | LiveKit Documentationboundless-author-7811
03/04/2026, 9:06 AMtall-airline-16756
03/04/2026, 9:06 AMdef create_agent_session(agent_config, short_system_prompt):
model = create_model_for_agent(agent_config)
session = AgentSession(llm=model, vad=self.silero_vad, ...)
session.metadata = {...}
# minimal on-metrics wiring
session.on("metrics_collected", lambda ev: usage_collector.collect(ev.metrics, agent=agent_config.name))
return session
1. Hand-off flow:
• Current agent determines next_agent_name (policy or rule).
• Compute handoff_payload:
◦ required_facts = extract_structured_facts(conversation_history)
◦ summary = summarize_recent_turns(conversation_history) # optional
• Close or prune current session state:
◦ session.close() or session.clear_conversation() if available
• Create/activate next agent session and seed it:
next_session = create_agent_session(next_agent_config, next_system_prompt)
# Seed minimal context:
seed_messages = [
{"role":"system","content": next_system_prompt},
{"role":"system","content": f"handoff: {json.dumps(required_facts)}"},
{"role":"user","content": summary},
]
await next_session.send_seed_messages(seed_messages)
1. Summarize at handoff
• Use a brief LLM call (or local summarizer) to convert the last N turns into a 1–3 sentence summary and include only that in the next agent’s context.
Practical tips to reduce tokens further
• Subtract cached tokens when reporting billable usage so optimizations focus on new token use.
• Prefer structured metadata over natural language for facts (JSON facts are shorter and less ambiguous).
• Limit how many prior turns you re-send; keep N small (e.g., last 2 turns) plus a 1-sentence summary of earlier history.
• Make tool invocation explicit rather than embedding tool instructions into the prompt each time.
When multi‑agent is not ideal
• If your dialog requires rich, tightly-coupled context across all phases (lots of back-and-forth referencing earlier specifics), frequent switching and pruning may lose necessary context and require more re-sends, offsetting savings.
• If you only have one short conversation flow, added complexity may not be worth it.
If you’d like, I can:
• Draft a concrete handoff example using your current AgentSession pattern (including code to seed the next agent and summarize history).
• Provide a small script to simulate tokens saved by moving from single-agent to multi-agent for one of your call traces.
Which would help — a concrete code handoff example tailored to your existing _create_agent_and_session flow, or a token-savings simulation for one of your sessions?boundless-author-7811
03/04/2026, 9:10 AMtall-airline-16756
03/04/2026, 9:11 AMboundless-author-7811
03/04/2026, 9:18 AM