which llm metrics reflect correct utilisation of t...
# ask-ai
b
which llm metrics reflect correct utilisation of tokens/minute?
t
Thanks for reaching out, Utkarsh. I'm an AI assistant and I'm here to help. Give me a moment while I research this topic.
Thanks — you're asking which LLM metrics show correct utilization of tokens per minute. Good question. The key metrics to monitor are: • Prompt Tokens — tokens in the input you send to the model. • Completion Tokens — tokens generated by the model in the response. • Total Tokens — sum of prompt + completion tokens for a single request; use this to measure actual token consumption. • Tokens/Second — processing throughput for requests; useful to understand how quickly tokens are being consumed. These metrics are emitted via the
LLMMetrics
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 | DeepWiki
b
is the above correct for realtime models as well?
t
Thanks — you're asking whether the same LLM token metrics (Prompt Tokens, Completion Tokens, Total Tokens, Tokens/Second) apply to real-time models. Short answer: yes. What applies to real-time models • Prompt Tokens, Completion Tokens, Total Tokens are emitted for real-time models just like for standard LLMs. • Tokens/Second (throughput) is also available and useful for spotting performance changes in real-time streams. • LiveKit Agents emit these values via the
LLMMetrics
/
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 | DeepWiki
b
prompt tokens are constant or recalculated everytime?
t
Thanks — you're asking whether
prompt_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?
b
{'call_start_time': 1772005831.0120091, 'call_end_time': 1772005985.8791935, 'llm_prompt_tokens': 243899, 'llm_completion_tokens': 6633, 'total_tokens': 250532}, is this log correct?
t
Thanks — you asked whether this log is correct. I checked the numbers and have a short analysis and next steps. Quick checks • Call duration: 1772005985.8791935 − 1772005831.0120091 = ~154.87 seconds (~2 minutes 35 seconds) — plausible. • Token arithmetic: 243,899 (prompt) + 6,633 (completion) = 250,532 (total) — arithmetic is correct. Where this looks suspicious •
llm_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?
b
summary = usage_collector.get_summary() usage_data = { 'llm_prompt_tokens': summary.llm_prompt_tokens, 'llm_completion_tokens': summary.llm_completion_tokens, 'total_tokens': summary.llm_prompt_tokens + summary.llm_completion_tokens, }, i am using this process
t
Thanks — you shared the code you’re using to collect usage and asked whether that process is correct. I’ll confirm what’s happening and point out the likely reason you saw the very large prompt token count. What your code does •
summary = 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?
b
Copy code
async 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"}
t
Thanks — I reviewed your agent/session code, the usage-collector pattern, and the RealtimeModel logs you pasted. I’ll confirm what’s happening, point out what the log fields mean, and show how to compute tokens/minute reliably from your current setup. What the logs represent • Each RealtimeModel log line is a per-invocation metrics snapshot emitted by the agent (via
metrics_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):
Copy code
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?
b
but at the end i am getting: Feb 26, 2026 @ 184558.439 {"message": "[repayment_pdd-sib-13-1772111640] 📊 Call Metrics: {'call_start_time': 1772111649.6190197, 'call_end_time': 1772111754.533457, 'llm_prompt_tokens': 82245, 'llm_completion_tokens': 2356, 'total_tokens': 84601}", "level": "INFO", "name": "orchestrator.shutdown_handler", "pid": 105, "job_id": "AJ_ieL8Kn4ZvxoB", "room_id": "RM_TK7TeUrb2NVY", "timestamp": "2026-02-26T131558.438605+00:00"}, as you can see for the above logs the: llm_prompt_tokens: 82245 is huge, how this summation is coming from the above logs?
t
Thanks — I looked at the per‑invocation RealtimeModel logs you pasted and your final call summary. I’ll explain exactly how the 82,245 prompt tokens can legitimately arise from that stream and what to check so you can validate the numbers. Short answer • The usage collector sums prompt/input tokens across every invocation it receives. The 82,245
llm_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?
b
{"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"}, how is the input_tokens calculated, it is the summation of which fields?
t
Thanks — you asked exactly which fields make up
input_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 Documentation
b
what about the output tokens?
t
Good question — output tokens are calculated the same way input tokens are, but they represent what the model generated (no cached subset). I’ll explain exactly which fields sum to
output_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 | DeepWiki
b
is the total_prompt_tokens correct?
t
Thanks — I’ll confirm whether the session’s reported
llm_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)
Copy code
# 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?
b
is the avg token used when doing real time call?
t
Thanks — you’re asking what the “average token used” is for a real‑time call and how to compute it from your logs. I’ll explain how to calculate relevant averages (per‑call, per‑minute, and per‑invocation), show formulas using fields from your RealtimeModel logs, and suggest which averages are most useful for monitoring and billing. Key averages you can compute • Average tokens per invocation (turn) ◦ Useful to understand how large each model request is. ◦ Formula: avg_tokens_per_invocation = (Σ total_tokens_i) / N_invocations ◦ From your logs: sum the
total_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?
b
no, i am asking if the total tokens are correct for real time communication?
t
Thanks — you’re asking whether the reported total tokens (and prompt tokens) are correct for real‑time communication. Short answer: yes — the totals look correct given your logs and how the collector works. Here’s why, exactly what’s being summed, and how you can verify it. Why the large totals are expected • Each RealtimeModel log line is a single invocation’s metrics snapshot. For each invocation: ◦ input_tokens = input_text_tokens + input_audio_tokens + input_image_tokens ◦ output_tokens = output_text_tokens + output_audio_tokens + output_image_tokens ◦ total_tokens = input_tokens + output_tokens • Your usage collector receives every
metrics_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:
Copy code
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:
Copy code
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 Documentation
b
Copy code
async 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 high
t
Great — you’re seeing high summed
input_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:
Copy code
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:
Copy code
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 Documentation
b
can multi agent approach solve this issue? Let say I have will have initial_greeting_agent, then name_confirmation_agent and then other agents so on based on condition, as the converstaion proceeds the agents gets loaded. Does this approach makes sense, if so, how should we do it?
t
Short answer: Yes — a multi‑agent approach can reduce token usage if designed correctly. It’s a good fit when different conversation phases need different system prompts, toolsets, or context. Below I’ll explain why it helps, trade‑offs, and a practical way to implement it with your LiveKit/AgentSession pattern. Why this helps • Smaller per‑agent system prompts and tool definitions mean less context sent each invocation. That reduces the repeated token cost when that agent is active. • You can keep each agent’s conversation history short (only relevant turns), avoiding one ever‑growing global context. • You can load heavy tools or long instructions only when needed and unload them afterwards, lowering steady-state input tokens. Trade-offs to consider • Context continuity: switching agents can lose fine-grained recent context unless you intentionally hand off a summary or the necessary state. • Complexity: more agent instances and routing logic increases implementation and testing effort. • Latency/overhead: creating/initializing agents frequently may add small delays unless you reuse warmed instances. How to design it (practical pattern) 1. Define clear responsibilities ◦ initial_greeting_agent: short greeting prompt, small toolset (e.g., IVR menu) ◦ name_confirmation_agent: focused prompt for confirming identity ◦ verification_agent / billing_agent / fallback_agent: each with minimal instructions and only required tools 1. Use lightweight system prompts and minimal tool schemas per agent ◦ Keep system messages and tool descriptions concise. ◦ Move large static references to an external retrieval service instead of embedding in the prompt. 1. Hand off context explicitly ◦ When switching agents, pass only what the next agent needs: ▪︎ Option A: send a brief structured handoff object (e.g., {customer_name, verified: true, last_user_utterance}) ▪︎ Option B: send a short summary generated at handoff time: ▪︎ Summarize recent turns to 1–2 sentences and include that as the first user/system message for the next agent. 1. Reset or prune session state per agent ◦ Start each agent session with a fresh short history or trimmed context. ◦ If you must preserve earlier facts (e.g., name, account id), store those in metadata and re-inject only the necessary fields. 1. Reuse warmed agent instances when possible ◦ Keep a pool of warm agents to avoid cold-init overhead. Initialize tool lists but keep prompts minimal until assigned. 1. Measure per-agent usage ◦ Track tokens per agent separately (tag usage_collector with agent id) so you can see which agent contributes most tokens and optimize it. Implementation sketch (LiveKit-like) 1. Create a factory for agents and sessions:
Copy code
def 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:
Copy code
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?
b
a concrete code handoff example tailored to your existing _create_agent_and_session flow
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!
b
a concrete code handoff example tailored to your existing _create_agent_and_session flow