mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-27 14:17:10 -04:00
## Description
`rates.saved_pct` and `rates.yield_pct` in the beacon payload divide
`tokens_saved` by `original` / `attempted`. Tool-schema deferral never
lands in either denominator — `outcome.py` says so explicitly, and
`tokens.tool_saved` exists precisely because of it — so every beacon
rate silently reports context compression only.
On a tool-heavy fleet that is not a rounding difference. Across the
first 516 sessions in the corpus the beacon reads **2.80%** where the
dashboard headline for the same traffic reads **12.82%**: 157.6M context
tokens vs 803.9M all-layers, with 646.3M of tool-schema deferral missing
from the ratio.
`headroom/proxy/server.py` already resolved this for the dashboard in
#2737 — `savings_percent` is `all_layers_saved / (input +
all_layers_saved)` and `active_savings_percent` puts tool savings on
both sides of the ratio. The beacon was never brought along. This does
that.
Closes #
## 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/telemetry/session.py`: add `rates.all_layers_saved_pct` and
`rates.all_layers_yield_pct`, computed the way `server.py` builds
`savings_percent` / `active_savings_percent` — tool savings added to
**both** sides, since deferred schemas were attempted work that
succeeded whole.
- `headroom/telemetry/session.py`: extend the `demo()` self-test to
assert both new rates against the existing tool-heavy fixture.
- `deploy/beacon/query.sh`: add an `all_layers_pct` column to the fleet
summary, so the reader stops showing the understated number too.
**Kept alongside `saved_pct` rather than folded into it.** Every row
already in the corpus means context-only under that name; redefining it
would make old and new rows non-comparable with no field to tell them
apart.
**`SCHEMA_VERSION` deliberately stays at 1.** The change is purely
additive, nothing reads the field, and the query uses `union_by_name =
true`, so old and new rows mix cleanly. Happy to bump it if maintainers
want the marker.
## 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
$ python -m headroom.telemetry.session
ok
$ python -m pytest tests/test_savings_tool_search_aggregation.py tests/test_outcome_dual_ruler_funnel.py -q
FAILED tests/test_outcome_dual_ruler_funnel.py::test_ledger_delta_stays_on_the_local_ruler
FAILED tests/test_outcome_dual_ruler_funnel.py::test_ledger_falls_back_to_billed_when_local_omitted
2 failed, 5 passed, 3 warnings in 4.42s
$ ruff check headroom/
All checks passed!
$ mypy --python-version 3.12 headroom/telemetry/session.py
Success: no issues found in 1 source file
```
The two `test_outcome_dual_ruler_funnel.py` failures are **pre-existing
on `main`, not caused by this PR** — verified by `git stash`-ing the
change and re-running:
```text
$ git stash && python -m pytest tests/test_outcome_dual_ruler_funnel.py -q
FAILED tests/test_outcome_dual_ruler_funnel.py::test_ledger_delta_stays_on_the_local_ruler
FAILED tests/test_outcome_dual_ruler_funnel.py::test_ledger_falls_back_to_billed_when_local_omitted
2 failed, 3 passed, 3 warnings in 3.71s
```
## Real Behavior Proof
- **Environment:** macOS 25.4.0 arm64, Python 3.12.6, repo `.venv`,
branch rebased on `upstream/main` @ `d0a86d40`.
- **Exact command / steps:** drive a real `SessionAggregator` with a
tool-heavy outcome (`original=1000`, `attempted=400`,
`tokens_saved=300`, plus `tool_search_deferred_tokens=800` and
`turn_hook_tools_saved_tokens=200`) and print the emitted payload's
`rates` block:
```text
tokens: {"original": 1000, "attempted": 400, "saved": 300, "tool_saved": 1000}
rates: {
"saved_pct": 30.0,
"eligible_pct": 40.0,
"yield_pct": 75.0,
"all_layers_saved_pct": 65.0,
"all_layers_yield_pct": 92.86,
"cache_read_pct": 50.0,
"overhead_pct": 5.0
}
```
- **Observed result:** the pre-existing rates are byte-identical (30.0 /
40.0 / 75.0 / 50.0 / 5.0 — no regression), and the two new fields report
the all-layers view: 1300 saved of 2000 sent = **65.0%**, 1300 of 1400
attempted = **92.86%**. Both denominators grow with the numerator,
matching `server.py`.
Cross-checked against the live corpus with DuckDB over the R2 bucket —
restating all 516 sessions both ways reproduces the gap this PR closes:
```text
┌───────────┬────────────┬──────────────────┬──────────────────┬─────────────────────┐
│ ctx_saved │ tool_saved │ all_layers_saved │ beacon_saved_pct │ dashboard_saved_pct │
├───────────┼────────────┼──────────────────┼──────────────────┼─────────────────────┤
│ 157561051 │ 646293457 │ 803854508 │ 2.8 │ 12.82 │
└───────────┴────────────┴──────────────────┴──────────────────┴─────────────────────┘
```
- **Not tested:** no live proxy run was made against a real provider —
the payload above comes from `SessionAggregator` driven directly, which
is the same code path the proxy feeds. The 516 sessions already in R2
are **not backfilled**: they carry `tool_saved`, so the corrected rate
is computable from them today, but their own `rates` block stays
context-only. Only sessions emitted from the next release carry the new
fields.
## 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
- [x] I did **not** edit `CHANGELOG.md`
## Additional Notes
- **Documentation:** N/A — no doc references the beacon `rates` field
names (`grep -rn "saved_pct\|yield_pct" docs/` returns nothing). The
field semantics are documented inline in `session.py`, which this PR
extends.
- **Worth a maintainer opinion:** the dashboard formula adds
`tool_saved` to the *denominator* as well. That is defensible — deferred
schemas were attempted work that succeeded 100% — but it does mean a
tool-heavy session's ratio is partly measuring a layer that is
near-always at full yield. This PR matches the dashboard rather than
inventing a third convention; if the convention should change, it should
change in both places at once.
- **Follow-up:** nothing here changes what the fleet actually saved,
only what the beacon admits to. The 4.6x gap was reporting, not
performance.
103 lines
4.3 KiB
Bash
Executable file
103 lines
4.3 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# Query the telemetry corpus in R2 with DuckDB.
|
|
#
|
|
# ./query.sh # fleet summary
|
|
# ./query.sh sessions # one row per session (deduped)
|
|
# ./query.sh "SELECT ..." # your own SQL against the corpus
|
|
#
|
|
# Setup, once:
|
|
# brew install duckdb
|
|
# Cloudflare > R2 > API > Create Account API Token (Object Read only,
|
|
# scoped to headroom-telemetry), then put the values in ~/env.txt
|
|
# (or any file named by HEADROOM_ENV_FILE):
|
|
#
|
|
# R2_ACCOUNT_ID=...
|
|
# R2_ACCESS_KEY_ID=...
|
|
# R2_SECRET_ACCESS_KEY=...
|
|
#
|
|
# R2_ACCOUNT_TOKEN is Cloudflare's REST-API token and is NOT used here — the
|
|
# S3 protocol wants the access-key pair.
|
|
set -euo pipefail
|
|
|
|
BUCKET="${R2_BUCKET:-headroom-telemetry}"
|
|
_repo_env="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)/.env"
|
|
ENV_FILE="${HEADROOM_ENV_FILE:-$HOME/env.txt}"
|
|
[ -f "$ENV_FILE" ] || ENV_FILE="$_repo_env"
|
|
|
|
[ -f "$ENV_FILE" ] || { echo "no env file (~/env.txt or $_repo_env) — see this script's header" >&2; exit 1; }
|
|
# shellcheck disable=SC1090
|
|
set -a; source "$ENV_FILE"; set +a
|
|
|
|
for v in R2_ACCOUNT_ID R2_ACCESS_KEY_ID R2_SECRET_ACCESS_KEY; do
|
|
[ -n "${!v:-}" ] || { echo "$v not set in $ENV_FILE" >&2; exit 1; }
|
|
done
|
|
command -v duckdb >/dev/null || { echo "duckdb not installed: brew install duckdb" >&2; exit 1; }
|
|
|
|
# Credentials go in via a heredoc on stdin, never on the command line, so they
|
|
# stay out of `ps` and shell history.
|
|
SECRET="
|
|
INSTALL httpfs; LOAD httpfs;
|
|
CREATE OR REPLACE SECRET r2corpus (
|
|
TYPE r2,
|
|
KEY_ID '${R2_ACCESS_KEY_ID}',
|
|
SECRET '${R2_SECRET_ACCESS_KEY}',
|
|
ACCOUNT_ID '${R2_ACCOUNT_ID}'
|
|
);
|
|
"
|
|
|
|
# The corpus is heartbeats: a session reports every 5 minutes with CUMULATIVE
|
|
# totals under one id. So the row with the highest seq per (install, session) is
|
|
# the whole session — never SUM across heartbeats, you would count each session
|
|
# once per report.
|
|
DEDUPE="
|
|
CREATE OR REPLACE TEMP VIEW sessions AS
|
|
SELECT * FROM read_ndjson('r2://${BUCKET}/sessions/**/*.json', union_by_name = true)
|
|
QUALIFY row_number() OVER (
|
|
PARTITION BY resource['headroom.install_id'], session.id
|
|
ORDER BY session.seq DESC
|
|
) = 1;
|
|
"
|
|
|
|
case "${1:-summary}" in
|
|
summary)
|
|
# Fleet rates come from summing raw counts. Averaging the per-session
|
|
# rates.*_pct fields would weight a 10-token session equal to a 1M one.
|
|
QUERY="
|
|
SELECT count(*) AS sessions,
|
|
count(DISTINCT resource['headroom.install_id']) AS installs,
|
|
sum(session.turns) AS turns,
|
|
sum(tokens.saved) AS tokens_saved,
|
|
sum(tokens.tool_saved) AS tool_tokens_saved,
|
|
round(sum(tokens.attempted) * 100.0
|
|
/ nullif(sum(tokens.original), 0), 2) AS eligible_pct,
|
|
round(sum(tokens.saved) * 100.0
|
|
/ nullif(sum(tokens.attempted), 0), 2) AS yield_pct,
|
|
round(sum(tokens.saved) * 100.0
|
|
/ nullif(sum(tokens.original), 0), 2) AS saved_pct,
|
|
-- saved_pct/yield_pct above are context-compression only, because
|
|
-- tool_saved never lands in original/attempted. This is the
|
|
-- dashboard headline (server.py `savings_percent`): tool-schema
|
|
-- savings on BOTH sides, since deferred schemas were attempted work
|
|
-- that succeeded whole. On a tool-heavy fleet the two differ several-
|
|
-- fold, so say which one you are quoting.
|
|
round(sum(tokens.saved + tokens.tool_saved) * 100.0
|
|
/ nullif(sum(tokens.original + tokens.tool_saved), 0), 2)
|
|
AS all_layers_pct,
|
|
sum(failures) AS failures
|
|
FROM sessions;"
|
|
;;
|
|
sessions)
|
|
QUERY="
|
|
SELECT resource['headroom.install_id'][1:8] AS install,
|
|
session.id, session.seq, session.turns, session.duration_s,
|
|
tokens.original, tokens.attempted, tokens.saved,
|
|
rates.saved_pct, rates.eligible_pct, rates.yield_pct,
|
|
providers, models, skips
|
|
FROM sessions
|
|
ORDER BY session.duration_s DESC
|
|
LIMIT 50;"
|
|
;;
|
|
*) QUERY="$1" ;;
|
|
esac
|
|
|
|
printf '%s\n%s\n%s\n' "$SECRET" "$DEDUPE" "$QUERY" | duckdb -box
|