fix(dashboard): align token savings headline denominator (#1653)
Some checks failed
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime name:]) (push) Waiting to run
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code name:code]) (push) Waiting to run
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code-nonroot name:code-nonroot]) (push) Waiting to run
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code-slim name:code-slim]) (push) Waiting to run
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code-slim-nonroot name:code-slim-nonroot]) (push) Waiting to run
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-nonroot name:nonroot]) (push) Waiting to run
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-slim name:slim]) (push) Waiting to run
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-slim-nonroot name:slim-nonroot]) (push) Waiting to run
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime name:]) (push) Waiting to run
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code name:code]) (push) Waiting to run
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code-nonroot name:code-nonroot]) (push) Waiting to run
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code-slim name:code-slim]) (push) Waiting to run
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code-slim-nonroot name:code-slim-nonroot]) (push) Waiting to run
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-nonroot name:nonroot]) (push) Waiting to run
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-slim name:slim]) (push) Waiting to run
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-slim-nonroot name:slim-nonroot]) (push) Waiting to run
Docker / docker-manifest (map[bake_target:runtime name:]) (push) Blocked by required conditions
Docker / docker-manifest (map[bake_target:runtime-code name:code]) (push) Blocked by required conditions
Docker / docker-manifest (map[bake_target:runtime-code-nonroot name:code-nonroot]) (push) Blocked by required conditions
Docker / docker-manifest (map[bake_target:runtime-code-slim name:code-slim]) (push) Blocked by required conditions
Docker / docker-manifest (map[bake_target:runtime-code-slim-nonroot name:code-slim-nonroot]) (push) Blocked by required conditions
Docker / docker-manifest (map[bake_target:runtime-nonroot name:nonroot]) (push) Blocked by required conditions
Docker / docker-manifest (map[bake_target:runtime-slim name:slim]) (push) Blocked by required conditions
Docker / docker-manifest (map[bake_target:runtime-slim-nonroot name:slim-nonroot]) (push) Blocked by required conditions
Docker / promote-latest (push) Blocked by required conditions
Deploy Documentation / validate (push) Failing after 4s
Install Native E2E / install-native (ubuntu-latest) (push) Failing after 55s
Deploy Documentation / deploy (push) Failing after 1m16s
rust / test (ubuntu) (push) Failing after 42s
rust / parity (nightly, allowed to fail during Phase 0) (push) Failing after 5s
rust / wheels (x86_64-unknown-linux-gnu) (push) Failing after 10s
rust / audit (push) Failing after 8s
Security / CodeQL (javascript-typescript) (push) Failing after 33s
Security / Dependency audit (pip-audit) (push) Failing after 1m17s
Security / CodeQL (python) (push) Failing after 1m8s
Release Please / release-please (push) Failing after 10m19s
Merge Conflicts / merge-conflicts (push) Failing after 10m25s
Init Native E2E / init-native (ubuntu-latest, copilot) (push) Failing after 10m35s
Init Native E2E / init-native (ubuntu-latest, codex) (push) Failing after 10m38s
Init Native E2E / init-native (ubuntu-latest, claude) (push) Failing after 10m43s
Init E2E / docker-init-e2e (push) Failing after 10m48s
Dev Containers / validate-worktree (push) Failing after 13m16s
Dev Containers / validate (.devcontainer/memory-stack/devcontainer.json, memory-stack) (push) Failing after 13m18s
Dev Containers / validate (.devcontainer/devcontainer.json, default) (push) Failing after 13m25s
CI / commitlint (push) Failing after 13m53s
CI / changes (push) Failing after 14m10s
Wrap Native E2E / wrap-native (ubuntu-latest) (push) Failing after 12m56s
Wrap E2E / docker-wrap-e2e (push) Failing after 13m6s
Security / Secret scan (gitleaks) (push) Failing after 13m13s
Init Native E2E / init-native (macos-latest, claude) (push) Has been cancelled
Init Native E2E / init-native (macos-latest, codex) (push) Has been cancelled
Init Native E2E / init-native (macos-latest, copilot) (push) Has been cancelled
Install Native E2E / install-native (macos-latest) (push) Has been cancelled
rust / wheels (aarch64-apple-darwin) (push) Has been cancelled
Wrap Native E2E / wrap-native (macos-latest) (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / build-wheel (push) Has been cancelled
CI / prefetch-model (push) Has been cancelled
CI / test (1) (push) Has been cancelled
CI / test (2) (push) Has been cancelled
CI / test (3) (push) Has been cancelled
CI / test (4) (push) Has been cancelled
CI / test-extras (push) Has been cancelled
CI / test-agno (push) Has been cancelled
CI / test-dashboard-ui (push) Has been cancelled
CI / build (push) Has been cancelled
CI / workflow-validation (push) Has been cancelled
CI / docker-native-e2e (push) Has been cancelled
CI / windows-native-wrapper (push) Has been cancelled
CI / macos-native-wrapper (push) Has been cancelled

## Description
Fixes a dashboard denominator mismatch in the Token Savings card.

The headline was showing the active attempted-token ratio, while the
same card's sublabel reports total-wire savings. This made sessions show
values like about 17% in the headline and about 1.2% in the total-wire
line for the same saved-token count.

This changes the headline to use `stats.tokens.savings_percent`, with
`proxy_savings_percent` as a fallback, so the headline and card copy use
the same denominator.

Closes #

## Type of Change
- [x] Bug fix (non-breaking change fixes issue)
- [ ] New feature (non-breaking change adds functionality)
- [ ] Breaking change (fix or feature cause existing functionality
change)
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)

## Changes Made
- Updated `headlineSavingsPercent` to prefer the total-wire
`savings_percent` metric.
- Updated the headline tooltip to say `Of total wire input tokens`.
- Added a focused dashboard regression test that prevents the headline
getter from using `active_savings_percent` or `proxy_attempted_tokens`.

## Testing
- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [ ] Type checking passes (`mypy headroom`)
- [x] New tests added new functionality
- [x] Manual testing performed

### Test Output

```text
$ python3 - <<'PY'
from pathlib import Path
html = Path('headroom/dashboard/templates/dashboard.html').read_text(encoding='utf-8')
assert 'stats.tokens?.savings_percent' in html
assert 'Of total wire input tokens' in html
start = html.index('get headlineSavingsPercent()')
end = html.index('get headlineSavingsTitle()', start)
headline = html[start:end]
assert 'active_savings_percent' not in headline
assert 'proxy_attempted_tokens' not in headline
print('dashboard headline denominator check passed')
PY
dashboard headline denominator check passed

$ uv run --extra dev pytest tests/test_dashboard_token_savings.py
============================= test session starts ==============================
platform darwin -- Python 3.13.3, pytest-9.0.3, pluggy-1.6.0
collected 1 item
tests/test_dashboard_token_savings.py::test_token_savings_headline_uses_total_wire_denominator PASSED [100%]
============================== 1 passed in 0.10s ===============================

$ uv run --extra dev ruff check tests/test_dashboard_token_savings.py
All checks passed!
```

## Real Behavior Proof
- Environment: Local Headroom dashboard served from the installed 0.28.0
package on macOS, proxy on `127.0.0.1:8788`, checked against the same
dashboard template logic patched in this PR.
- Exact command / steps: Queried local `/stats?cached=1`, compared
`tokens.active_savings_percent` with `tokens.savings_percent`, patched
the dashboard template locally, then refreshed `/dashboard` and
confirmed the served `headlineSavingsPercent` getter reads
`tokens.savings_percent`.
- Observed result: Local stats showed `active_savings_percent` around
16.78 while `tokens.savings_percent`, `tokens.proxy_savings_percent`,
and agent total savings were around 1.24. Before the patch, the
dashboard headline used the 16.78 active value even though the card text
said total wire. After the local template patch, the served dashboard
getter uses the 1.24 total-wire value.
- Not tested: Full cross-browser visual regression; this PR only changes
the Alpine getter denominator and adds a source-level regression test.

## Review Readiness
- [x] I have performed a self-review
- [x] This PR is ready for human review

## Checklist
- [x] My code follows project's style guidelines
- [x] I performed self-review my code
- [x] I commented my code, particularly in hard-to-understand areas
- [ ] I made corresponding changes documentation
- [x] My changes generate no new warnings
- [x] I added tests prove fix is effective or feature works
- [x] New and existing unit tests pass locally my changes
- [ ] I updated CHANGELOG.md if applicable

## Screenshots (if applicable)
N/A.

## Additional Notes
- Documentation and CHANGELOG are N/A for this narrow dashboard bug fix.
- CI is green and the PR is ready for review.
This commit is contained in:
Chris Yau 2026-07-02 12:31:32 +08:00 committed by GitHub
parent 5fe4e7b195
commit 646e705514
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 31 additions and 16 deletions

View file

@ -2565,26 +2565,14 @@
// --- Headline savings percent ---
//
// Active ratio (saved / attempted) is the right metric when we have
// a working denominator. Some backend-routed paths don't yet
// populate `proxy_attempted_tokens` consistently — in that case
// the active number reads 0% even when compression is happening,
// so fall back to the whole-request ratio (`proxy_savings_percent`)
// instead of showing a misleading zero. Issue #455.
get headlineSavingsPercent() {
const attempted = this.stats.tokens?.proxy_attempted_tokens || 0;
const active = this.stats.tokens?.active_savings_percent || 0;
if (attempted > 0 && active > 0) return active;
return this.stats.tokens?.proxy_savings_percent || 0;
return this.stats.tokens?.savings_percent
?? this.stats.tokens?.proxy_savings_percent
?? 0;
},
get headlineSavingsTitle() {
const attempted = this.stats.tokens?.proxy_attempted_tokens || 0;
const active = this.stats.tokens?.active_savings_percent || 0;
if (attempted > 0 && active > 0) {
return 'Of compressible tokens attempted';
}
return 'Of whole pre-compression request (attempted-tokens metric unavailable)';
return 'Of total wire input tokens';
},
// --- Expandable Rows ---

View file

@ -0,0 +1,27 @@
"""Regression tests for dashboard token savings copy."""
from __future__ import annotations
import re
from headroom.dashboard import get_dashboard_html
def _getter_body(html: str, name: str) -> str:
match = re.search(rf"get {name}\(\) \{{(?P<body>.*?)\n\s*\}},", html, re.S)
assert match is not None
return match.group("body")
def test_token_savings_headline_uses_total_wire_denominator() -> None:
html = get_dashboard_html()
headline_body = _getter_body(html, "headlineSavingsPercent")
assert "stats.tokens?.savings_percent" in headline_body
assert "stats.tokens?.proxy_savings_percent" in headline_body
assert "stats.tokens?.active_savings_percent" not in headline_body
assert "stats.tokens?.proxy_attempted_tokens" not in headline_body
title_body = _getter_body(html, "headlineSavingsTitle")
assert "Of total wire input tokens" in title_body
assert "Of compressible tokens attempted" not in title_body