mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-27 14:17:10 -04:00
## Description `headroom perf` read only `proxy.log` compression records, so RTK's savings — which live in RTK's own lifetime counter and never land in `proxy.log` — were **invisible**: perf reported "token savings" while silently dropping the entire CLI-filtering layer. The dashboard **Session** card likewise showed only the session-delta (≈0 right after a proxy restart), with no scope label and no lifetime figure. This surfaces RTK lifetime savings in `headroom perf` (text + JSON) and clarifies the dashboard Session card. It complements #1324 (which added RTK to the Historical tab) by covering the two surfaces #1324 didn't: `perf` and the live Session card. Closes # N/A — complements #1324; no standalone issue. ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) - [ ] New feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - `headroom/perf/analyzer.py`: `format_report` and `build_perf_summary` now attach RTK/CLI context-tool **lifetime** savings, sourced best-effort from `_get_context_tool_stats().lifetime` (the same source `/stats` and #1324 use). Lifetime — not session — is the right scope for a one-shot CLI, since the proxy-session baseline `/stats` subtracts is meaningless out of process. Omitted entirely when no tool is installed or its stats can't be read, so the report degrades to proxy-only rather than erroring. - `headroom/dashboard/templates/dashboard.html`: the Session card now labels the RTK number **"this session"**, uses the real `session_savings_pct` (via a new `cliFilteringSessionPctDisplay` getter) instead of an ad-hoc share, and shows **lifetime** alongside it (new `cliFilteringLifetime` getter + row, hidden when 0). - `tests/test_perf_cli_filtering.py` (new): perf surfaces RTK in text + JSON; omits cleanly when the tool is absent. - `tests/test_rtk_session_savings.py` (new): exercises the real `_get_context_tool_stats()` plumbing to pin that session RTK savings are the **delta from the startup baseline**, and session `savings_pct` is derived from that delta — not RTK's lifetime-diluted average. - `tests/test_proxy_dashboard_stats_cache.py`: updated the Session-card label assertion and added one for the new lifetime row. ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check`) - [x] Type checking passes (`mypy headroom`) - [x] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text $ ruff check headroom/perf/analyzer.py tests/test_perf_cli_filtering.py tests/test_rtk_session_savings.py tests/test_proxy_dashboard_stats_cache.py All checks passed! $ mypy headroom/perf/analyzer.py mypy: No issues found $ python -m pytest tests/test_perf_cli_filtering.py tests/test_rtk_session_savings.py tests/test_proxy_dashboard_stats_cache.py tests/test_owned_asset_encoding.py -q 17 passed, 1 skipped in 15.66s ``` ## Real Behavior Proof - Environment: macOS (darwin), Python 3.12, branch `fix/rtk-savings-perf-dashboard`, RTK v0.28.2. - Exact command / steps: `headroom perf` and `headroom perf --format json`. - Observed result: the text report now includes a section `RTK CLI Filtering (lifetime, all-time) — Tokens saved: 26,867,610 (68.8%), Commands: 8,023`, and the JSON output carries `"cli_filtering": {"tool":"rtk","label":"RTK","tokens_saved":26867610,"commands":8023,"savings_pct":68.8}`. Before this change, both omitted RTK entirely (perf's "Total saved" was proxy-compression only). The dashboard template renders the new "this session" / "lifetime" RTK rows (verified via `get_dashboard_html()` + substring test). - Not tested: live dashboard browser click-through (template loads and the new strings are asserted by the substring test); CSV output of `perf` (per-model table only, by design). ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Checklist - [x] My code follows the project's style guidelines - [x] I have performed a self-review of my code - [x] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] I have updated the CHANGELOG.md if applicable ## Additional Notes - CHANGELOG: left to Release Please (the conventional `fix(perf):` commit generates the entry on merge), matching how the existing "Bug Fixes" entries are produced. - Follow-up: #1403 (`fix/rtk-savings-scope-regression`) bundles unrelated kompress must-keep work (overlaps #1400/#1419) and only documents the scope `%` invariant in the abstract. The real, code-exercising session-delta regression now lives here (`test_rtk_session_savings.py`), so #1403 can be split — route the kompress bits to #1400/#1419 and drop the rest.
70 lines
2.6 KiB
Python
70 lines
2.6 KiB
Python
"""Session RTK savings must be the delta from the proxy-startup baseline.
|
|
|
|
Regression for the scope-mixing bug: the dashboard's *session* RTK number must
|
|
be computed from token deltas since the baseline pinned at proxy startup — NOT
|
|
from RTK's lifetime average (which dilutes a 62%-this-session rate down to an
|
|
18.5% all-time number). This exercises the real ``_get_context_tool_stats()``
|
|
plumbing rather than asserting the arithmetic in the abstract.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import headroom.proxy.helpers as helpers
|
|
|
|
|
|
def _reset(monkeypatch):
|
|
monkeypatch.delenv(helpers._RTK_GAIN_SCOPE_ENV, raising=False)
|
|
monkeypatch.setenv("HEADROOM_CONTEXT_TOOL", "rtk")
|
|
helpers._context_tool_stats_cache.update(
|
|
{"expires_at": 0.0, "has_value": False, "tool": None, "value": None}
|
|
)
|
|
helpers._context_tool_session_baseline.update(
|
|
{
|
|
"initialized": False,
|
|
"tool": None,
|
|
"total_commands": 0,
|
|
"input_tokens": 0,
|
|
"output_tokens": 0,
|
|
"tokens_saved": 0,
|
|
"total_time_ms": 0,
|
|
"captured_at": 0.0,
|
|
}
|
|
)
|
|
|
|
|
|
def _bust_cache():
|
|
helpers._context_tool_stats_cache.update(
|
|
{"expires_at": 0.0, "has_value": False, "tool": None, "value": None}
|
|
)
|
|
|
|
|
|
def test_session_savings_is_delta_not_lifetime_average(monkeypatch):
|
|
_reset(monkeypatch)
|
|
|
|
state: dict = {"summary": None}
|
|
|
|
def fake_lifetime(tool):
|
|
return helpers._context_tool_summary_payload(
|
|
tool="rtk", installed=True, scope="global", summary=state["summary"]
|
|
)
|
|
|
|
monkeypatch.setattr(helpers, "_read_context_tool_lifetime_stats", fake_lifetime)
|
|
|
|
# First poll pins the baseline to the current lifetime → session delta is 0,
|
|
# but the lifetime number is preserved untouched.
|
|
state["summary"] = {"total_input": 1000, "total_output": 400, "total_saved": 600}
|
|
first = helpers._get_context_tool_stats()
|
|
assert first is not None
|
|
assert first["session"]["tokens_saved"] == 0
|
|
assert first["lifetime"]["tokens_saved"] == 600
|
|
|
|
# Lifetime advances (more RTK commands run this session); the session number
|
|
# is the DELTA, not the 800 lifetime total.
|
|
_bust_cache()
|
|
state["summary"] = {"total_input": 1300, "total_output": 500, "total_saved": 800}
|
|
second = helpers._get_context_tool_stats()
|
|
assert second["session"]["tokens_saved"] == 200 # 800 - 600
|
|
assert second["lifetime"]["tokens_saved"] == 800
|
|
# Session % is derived from the delta (200 saved / 300 input delta), not the
|
|
# lifetime-diluted average.
|
|
assert second["session"]["savings_pct"] == round(200 / 300 * 100, 4)
|