headroom/docs/output-token-reduction-guide.md
Tejas Chopra 6904d47a01
feat(proxy): hot-reload live env knobs so a reused proxy picks them up without a restart (#1090)
## Description

A small class of env vars is read by the proxy **live, per request** —
the output-shaper family (`HEADROOM_OUTPUT_SHAPER`,
`HEADROOM_VERBOSITY_LEVEL`, `HEADROOM_EFFORT_ROUTER`,
`HEADROOM_MECHANICAL_EFFORT`, `HEADROOM_VERBOSITY_AUTOTUNE`,
`HEADROOM_OUTPUT_HOLDOUT`), or captured at import
(`HEADROOM_INTERCEPT_READ_MIN_CHARS`). The proxy reads them from its own
process environment, fixed at launch. But `headroom wrap` reuses an
already-running proxy (it restarts only on startup-config drift), so a
value exported *after* the proxy started silently no-op'd — e.g. `export
HEADROOM_OUTPUT_SHAPER=1` had zero effect on a reused proxy on `:8787`.

This PR makes those live knobs **hot-reloadable**: `headroom wrap`
pushes them to the running proxy, which applies them in memory — no
restart (a restart would cold-start the ML stack, drop in-flight
requests, and lose CCR/router caches).

_No linked issue._

## Type of Change

- [x] Bug fix (non-breaking change that fixes an issue)
- [x] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [x] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)

## Changes Made

- `headroom/proxy/runtime_env.py` (new): single source of truth
registering the live knobs + a thread-safe process-global override
store. `getenv()` (override-then-env) is a drop-in for `os.environ.get`;
behaviour is byte-identical when no override is set.
- Readers rerouted through `runtime_env.getenv`: `output_shaper.py`, the
anthropic holdout read, and the ast-grep threshold (now a live read, not
an import-time constant).
- Proxy: loopback-only `POST /admin/runtime-env` applies overrides in
memory; `/health` → `config.runtime_env` surfaces the live values so
reuse is observable.
- `wrap`: after attaching to a proxy (all call sites), best-effort push
of the session's **explicitly-set** knobs. No-ops if nothing is set,
`--no-proxy`, the proxy is unreachable, or it predates the endpoint
(404). Only explicitly-set knobs are pushed, so a session never clobbers
another with a default it never asked for.
- Docs: README + output-token-reduction guide document the
global-override caveat.

## Testing

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

### Test Output

```text
$ python -m pytest tests/test_runtime_env.py -q
16 passed

$ python -m pytest tests/test_runtime_env.py tests/test_output_shaper.py -q
50 passed

$ ruff check headroom/proxy/runtime_env.py headroom/proxy/output_shaper.py headroom/proxy/handlers/anthropic.py headroom/proxy/interceptors/astgrep.py headroom/proxy/server.py headroom/cli/wrap.py
All checks passed!

$ mypy headroom/proxy/runtime_env.py
Success: no issues found in 1 source file
```

## Real Behavior Proof

- Environment: local macOS, Python 3.12 `.venv`, branch
`fix/runtime-env-hot-reload` at the PR head.
- Exact command / steps: ran the test suites above. The 16 new
`test_runtime_env` tests exercise the registry/store, overrides reaching
the shaper + the ast-grep threshold, the `POST /admin/runtime-env` apply
+ `/health` reflect + loopback-only 404 + 400-on-non-object, and the
wrap push payload / no-op / error-swallow paths.
- Observed result: 50 passed; ruff + mypy clean on the changed modules;
an override set via the endpoint is read by `getenv()` at the shaper and
surfaced in `/health` config.
- Not tested: a literal two-terminal manual session (start a proxy,
`headroom wrap` a second session, `export HEADROOM_OUTPUT_SHAPER=1`,
confirm the reused proxy picks it up). The behaviour is covered by the
endpoint + wrap-push integration tests, but was not exercised by hand
here.

## 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
- [x] 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

- **Inherent caveat (documented):** overrides are global to the proxy —
one process serves every attached wrapper, so the last explicit setting
wins. No mechanism (restart or hot-reload) can give two sessions on one
shared proxy different output-shaper settings.
- **Scope:** startup-captured settings (`HEADROOM_TARGET_RATIO` etc.)
are intentionally out of scope — a fresh proxy already gets them and
they ride the existing `/health` config channel.
- **Merge blocker:** this branch is currently **CONFLICTING with
`main`** and needs a rebase/merge before it can land.
- CHANGELOG.md left unchanged — releases are managed by release-please
from conventional commits.
2026-06-18 09:50:50 -07:00

5.2 KiB
Raw Blame History

Output Token Reduction — User Guide

A plain-English guide to cutting the tokens the model writes back.

Why this exists

Headroom normally shrinks the prompt you send. This feature shrinks what the model returns. That matters because:

  • Output tokens cost 5× more than input on Opus-class models.
  • A lot of model output is waste: "Great, let me help with that…" intros, re-printing code you already showed it, restating tool results, and long internal "thinking" even on trivial steps.

You don't change any code. It runs in the Headroom proxy.

Turn it on

export HEADROOM_OUTPUT_SHAPER=1     # off by default
headroom proxy --port 8787

If a proxy is already running (e.g. headroom wrap claude attaches to one on port 8787 instead of starting a fresh one), it reads this switch from the environment it was launched with — so exporting it afterwards wouldn't reach it. headroom wrap handles this for you: it hot-syncs your current output settings to the running proxy (loopback POST /admin/runtime-env), applied immediately with no restart. Set the variables before you run wrap. Because one proxy is shared by every session attached to it, these settings are global — the most recent explicit value wins.

That's it. Two things now happen on every request:

  1. Verbosity steering — a short "be terse, don't restate context" instruction is added to the end of the system prompt. (The end, so your prompt cache still works.)
  2. Effort routing — if a turn is just the model continuing after a tool ran (e.g. it read a file and there were no errors), Headroom turns the model's "thinking effort" down for that one turn. Real questions and error-handling turns keep full effort.

The verbosity dial (levels 04)

Level What the model is told Good for
0 (off) disable steering
1 Skip the intro/outro chit-chat people who read everything
2 Also: don't restate code/output already on screen default — safe
3 Also: conclusions only, skip the reasoning people who skim
4 Bare minimum, fragments OK maximum savings, terse

Set it by hand if you want:

export HEADROOM_VERBOSITY_LEVEL=3

Or — better — let Headroom learn it from your habits (next section).

Let Headroom pick the level for you

People rarely say "be brief." They show it: they interrupt long answers, or reply so fast they couldn't have read the whole thing. headroom learn --verbosity reads your past sessions and picks a level from those signals.

# Preview what it found (doesn't change anything)
headroom learn --verbosity

# Save it — the proxy uses this level from now on
headroom learn --verbosity --apply

Example output:

Verbosity — headroom
  Interrupts:  29  (11% of turns)        ← push-back signal
  Fast-skips:  31 / 119 long answers (26% unread)   ← strongest signal
  >> Recommended verbosity level: 3 (confidence: high)

Add --llm-judge to have an LLM double-check the level (needs an API key).

See how much you saved

Here's the honest part. We can't directly measure output savings — we never see what the model would have written without our nudge. So Headroom reports an estimate with a confidence range, never a fake exact number:

headroom output-savings
Output-token reduction
  Method:    ESTIMATED (synthetic control)
  Requests:  1,240 shaped
  Saved:     ~410,000 output tokens
  Reduction: 28.0%   (95% CI 24.1% … 31.9%)
  • ESTIMATED = compared against a baseline of your past (unshaped) sessions.
  • MEASURED = the gold standard, if you opt into a holdout (below).

Want a measured number?

Leave a slice of traffic unshaped as a control group:

export HEADROOM_OUTPUT_HOLDOUT=0.1     # 10% of conversations stay unshaped

Now headroom output-savings compares shaped vs unshaped directly and reports a measured reduction. The trade-off: you give up the savings on that 10%.

On the dashboard

Open http://localhost:8787/dashboard. Next to the input-compression card you'll see an Output Tokens Saved card showing the token count, the percent, a measured/estimated badge, and the confidence range.

FAQ

Will this make answers worse? At level 2 (default), no — in our tests the model finds the same bugs and writes the same fixes; it just stops re-printing code and skipping the "let me…" intro. Levels 34 are terser by design; that's why learning the level per user matters.

Does it break prompt caching? No. The steering text is added at the end of the system prompt and is byte-stable, so your cached prefix is untouched.

Is it safe with extended thinking / tool loops? Yes. It never disables thinking outright (that can error), it only lowers effort on routine turns, and it never adds settings the model doesn't support.

How do I turn it off? Unset HEADROOM_OUTPUT_SHAPER (or set it to 0) and restart the proxy. You can also send x-headroom-bypass: true on a request to skip it for that call.


Deep dive (design + the counterfactual math): proposals/output-token-reduction.md