Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 15 additions & 5 deletions strix/interface/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -296,16 +296,26 @@ def _build_llm_usage_stats(
input_tokens = _int_stat(usage, "input_tokens")
output_tokens = _int_stat(usage, "output_tokens")
cached_tokens = _detail_value(usage, "input_tokens_details", "cached_tokens")
uncached_tokens = max(0, input_tokens - cached_tokens)
cost = _float_stat(usage, "cost")

stats_text.append("\n")
stats_text.append("Input Tokens ", style="dim")
stats_text.append(format_token_count(input_tokens), style="white")

if live or cached_tokens > 0:
if cached_tokens > 0:
stats_text.append("Input (total) ", style="dim")
stats_text.append(format_token_count(input_tokens), style="white")
stats_text.append(" · ", style="dim white")
stats_text.append("Cached Tokens ", style="dim")
stats_text.append("Cached (in total) ", style="dim")
stats_text.append(format_token_count(cached_tokens), style="white")
stats_text.append(" · ", style="dim white")
stats_text.append("New input ", style="dim")
stats_text.append(format_token_count(uncached_tokens), style="white")
else:
stats_text.append("Input Tokens ", style="dim")
stats_text.append(format_token_count(input_tokens), style="white")
if live:
stats_text.append(" · ", style="dim white")
stats_text.append("Cached (in total) ", style="dim")
stats_text.append(format_token_count(cached_tokens), style="white")

separator = "\n" if live else " · "
stats_text.append(separator, style="dim white")
Expand Down
70 changes: 70 additions & 0 deletions tests/test_interface_stats.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
"""Tests for TUI / CLI usage stats rendering."""

from __future__ import annotations

from types import SimpleNamespace
from unittest.mock import MagicMock

from strix.interface.utils import build_final_stats_text, build_live_stats_text


def _report_state_with_usage(usage: dict) -> SimpleNamespace:
state = SimpleNamespace(vulnerability_reports=[])
state.get_total_llm_usage = MagicMock(return_value=usage)
return state


def test_usage_stats_without_cache_uses_simple_input_label() -> None:
text = build_final_stats_text(
_report_state_with_usage(
{
"requests": 1,
"input_tokens": 1_000_000,
"output_tokens": 500_000,
"cost": 0.5,
}
)
)

plain = text.plain
assert "Input Tokens 1.0M" in plain
assert "Cached" not in plain
assert "New input" not in plain


def test_live_mode_without_cache_still_shows_cached_row() -> None:
"""Live sidebar always exposes the cache metric, even when it is still zero."""
text = build_live_stats_text(
_report_state_with_usage(
{
"requests": 1,
"input_tokens": 1_000_000,
"output_tokens": 500_000,
}
)
)

plain = text.plain
assert "Input Tokens 1.0M" in plain
assert "Cached (in total) 0" in plain
assert "New input" not in plain
assert plain.index("Input Tokens") < plain.index("Cached (in total)")


def test_usage_stats_with_cache_shows_subset_breakdown() -> None:
text = build_live_stats_text(
_report_state_with_usage(
{
"requests": 1,
"input_tokens": 20_000_000,
"output_tokens": 1_000_000,
"input_tokens_details": {"cached_tokens": 19_000_000},
}
)
)

plain = text.plain
assert "Input (total) 20.0M" in plain
assert "Cached (in total) 19.0M" in plain
assert "New input 1.0M" in plain
assert plain.index("Input (total)") < plain.index("Cached (in total)") < plain.index("New input")