Skip to content

Commit 6350e2b

Browse files
feat: show latest token usage in visualizer
Co-authored-by: openhands <[email protected]>
1 parent 4ffaa97 commit 6350e2b

File tree

2 files changed

+55
-19
lines changed

2 files changed

+55
-19
lines changed

openhands/sdk/conversation/visualizer.py

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -234,14 +234,13 @@ def _format_metrics_subtitle(self) -> str | None:
234234
return None
235235

236236
combined_metrics = self._conversation_stats.get_combined_metrics()
237-
if not combined_metrics or not combined_metrics.accumulated_token_usage:
237+
if not combined_metrics or not combined_metrics.token_usages:
238238
return None
239239

240-
usage = combined_metrics.accumulated_token_usage
240+
latest_usage = combined_metrics.token_usages[-1]
241241
cost = combined_metrics.accumulated_cost or 0.0
242242

243-
# helper: 1234 -> "1.2K", 1200000 -> "1.2M"
244-
def abbr(n: int | float) -> str:
243+
def abbr(n: int | float) -> str: # helper: 1234 -> "1.2K", 1200000 -> "1.2M"
245244
n = int(n or 0)
246245
if n >= 1_000_000_000:
247246
s = f"{n / 1_000_000_000:.2f}B"
@@ -253,26 +252,23 @@ def abbr(n: int | float) -> str:
253252
return str(n)
254253
return s.replace(".0", "")
255254

256-
input_tokens = abbr(usage.prompt_tokens or 0)
257-
output_tokens = abbr(usage.completion_tokens or 0)
255+
input_tokens = latest_usage.prompt_tokens or 0
256+
output_tokens = latest_usage.completion_tokens or 0
257+
cache_read = latest_usage.cache_read_tokens or 0
258+
cache_rate = (
259+
f"{(cache_read / input_tokens * 100):.2f}%" if input_tokens > 0 else "N/A"
260+
)
261+
reasoning_tokens = latest_usage.reasoning_tokens or 0
258262

259-
# Cache hit rate (prompt + cache)
260-
prompt = usage.prompt_tokens or 0
261-
cache_read = usage.cache_read_tokens or 0
262-
cache_rate = f"{(cache_read / prompt * 100):.2f}%" if prompt > 0 else "N/A"
263-
reasoning_tokens = usage.reasoning_tokens or 0
263+
cost_str = f"{cost:.4f}"
264264

265-
# Cost
266-
cost_str = f"{cost:.4f}" if cost > 0 else "$0.00"
267-
268-
# Build with fixed color scheme
269265
parts: list[str] = []
270-
parts.append(f"[cyan]↑ input {input_tokens}[/cyan]")
266+
parts.append(f"[cyan]↑ input {abbr(input_tokens)}[/cyan]")
271267
parts.append(f"[magenta]cache hit {cache_rate}[/magenta]")
272268
if reasoning_tokens > 0:
273269
parts.append(f"[yellow] reasoning {abbr(reasoning_tokens)}[/yellow]")
274-
parts.append(f"[blue]↓ output {output_tokens}[/blue]")
275-
parts.append(f"[green]$ {cost_str}[/green]")
270+
parts.append(f"[blue]↓ output {abbr(output_tokens)}[/blue]")
271+
parts.append(f"[green]$ {cost_str} (total)[/green]")
276272

277273
return "Tokens: " + " • ".join(parts)
278274

tests/sdk/conversation/test_visualizer.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -337,7 +337,47 @@ def test_metrics_formatting():
337337
assert "500" in subtitle # Output tokens
338338
assert "20.00%" in subtitle # Cache hit rate
339339
assert "200" in subtitle # Reasoning tokens
340-
assert "0.0234" in subtitle # Cost
340+
assert "$ 0.0234 (total)" in subtitle
341+
342+
343+
def test_metrics_formatting_uses_latest_request():
344+
"""Tokens should reflect the latest request while cost stays cumulative."""
345+
from openhands.sdk.conversation.conversation_stats import ConversationStats
346+
from openhands.sdk.llm.utils.metrics import Metrics
347+
348+
conversation_stats = ConversationStats()
349+
metrics = Metrics(model_name="test-model")
350+
metrics.add_cost(0.1)
351+
metrics.add_token_usage(
352+
prompt_tokens=120,
353+
completion_tokens=40,
354+
cache_read_tokens=12,
355+
cache_write_tokens=0,
356+
reasoning_tokens=5,
357+
context_window=8000,
358+
response_id="first",
359+
)
360+
metrics.add_cost(0.05)
361+
metrics.add_token_usage(
362+
prompt_tokens=200,
363+
completion_tokens=75,
364+
cache_read_tokens=25,
365+
cache_write_tokens=0,
366+
reasoning_tokens=0,
367+
context_window=8000,
368+
response_id="second",
369+
)
370+
conversation_stats.service_to_metrics["test_service"] = metrics
371+
372+
visualizer = ConversationVisualizer(conversation_stats=conversation_stats)
373+
374+
subtitle = visualizer._format_metrics_subtitle()
375+
assert subtitle is not None
376+
assert "input 200" in subtitle
377+
assert "output 75" in subtitle
378+
assert "cache hit 10.00%" not in subtitle # ensure using latest cache values
379+
assert "cache hit 12.50%" in subtitle
380+
assert "$ 0.1500 (total)" in subtitle
341381

342382

343383
def test_event_base_fallback_visualize():

0 commit comments

Comments
 (0)