Skip to content

Latest commit

 

History

History
39 lines (22 loc) · 12.1 KB

headroom.md

File metadata and controls

39 lines (22 loc) · 12.1 KB

Context compression with Headroom

The optional Headroom proxy that compresses tool output, logs and file content before they reach the model: how it is wired in, the pip versus Docker install methods, the two compression percentages it reports, and the version-pinning and minimum-release-age checks each install method enforces.

Context compression (Headroom)

headroom_enabled (on by default, all modes) runs Headroom — a local HTTP proxy that compresses tool outputs, logs, and file content before they reach the model — in front of the Anthropic API for the job, and points every anthropics/claude-code-action call in it at that proxy via ANTHROPIC_BASE_URL. Review, triage, and interactive runs routinely push large diffs, CI logs, and repo-file reads through Claude Code, which is exactly the repetitive, JSON/log-shaped content Headroom compresses well.

Set headroom_enabled: false explicitly if you use use_bedrock, use_vertex, use_foundry, or anthropic_federation_rule_id, or run on a self-hosted runner with neither a usable Python nor Docker. The four alternative-provider inputs route Claude Code's requests to their own provider directly, bypassing ANTHROPIC_BASE_URL and the proxy silently — setting any of them alongside the default headroom_enabled: true fails validation loudly rather than starting a proxy that compresses nothing (see "Mutually exclusive" below). A runner with neither Python nor Docker has nowhere to run either install method, and the proxy startup step fails the job outright rather than silently skipping compression.

headroom_enabled: false # opt out; on by default

How it's wired. A Start Headroom compression proxy step installs and starts the proxy, waits for it to report ready, then exports ANTHROPIC_BASE_URL=http://127.0.0.1:<headroom_port> to $GITHUB_ENV. Each federation fallback attempt and its follow-on calls override that URL to reach Anthropic directly; OAuth and API-key attempts use the proxy. anthropic_federation_fallbacks is therefore compatible with Headroom, unlike the standalone anthropic_federation_rule_id input.

headroom_install_method (default pip) picks how that step actually runs the proxy. pip installs headroom-ai[proxy] — the minimal extra for running just the proxy, without the AST-aware code-compression or Bedrock backend support the Docker image bundles unconditionally — into a dedicated venv (never the system Python directly: modern ubuntu-latest images enforce PEP 668 and refuse a bare pip install outright, and a venv avoids colliding with anything else in the job regardless), then runs it as a background process. ubuntu-latest ships Python 3 already, so this needs no separate runtime install, and it's the lighter of the two options — no container image to pull, no bundled extras this feature doesn't use. docker instead pulls and runs a pinned image (headroom_image) as a container: heavier, but the only option on a self-hosted runner that has Docker but not a usable Python (the pip method is, in exactly the mirrored way, the only option on a self-hosted runner with Python but no Docker — pick whichever your runner actually has).

headroom_install_method: docker # only if your runner lacks a usable Python; pip is the default

Savings are visible on the run itself, not just reachable by a caller reading an output. A Verify Headroom actually proxied this run step reads the proxy's own /stats after the run and writes a ### Headroom context compression section straight to the job's Step Summary — the tab everyone already looks at — reporting the request count, tokens saved, and two distinct compression figures (or a clear warning if nothing was proxied despite headroom_enabled: true). The same numbers are also exposed as the headroom_tokens_saved, headroom_compression_percent, and headroom_total_percent_saved outputs, for a caller that wants to gate on or record them programmatically, alongside the existing headroom_proxied boolean.

Two different compression percentages are reported, because they answer different questions. headroom_compression_percent is Headroom's own avg_compression_pct — the unweighted average of each individual request's own compression ratio, so one huge, highly-compressible request among many small, barely-compressible ones drags the average down even though most of the actual token volume was saved. headroom_total_percent_saved is the aggregate instead, computed as total_tokens_removed / total_tokens_before * 100 across every proxied request this run — the share of all tokens actually sent that Headroom removed, weighted by real token volume rather than by request count. A synthetic local test with a deliberate mix of one large, highly-compressible request and several small, moderately-compressible ones confirmed the two figures genuinely diverge (the aggregate landed well above the average, exactly as the weighting predicts), so both are shown side by side rather than picking one as "the" figure.

headroom_show_savings (on by default) also posts the same numbers directly on the pull request or issue, as a short standalone comment — the Step Summary above is only visible to someone who opens the run in the Actions tab, which most people looking at a PR never do. This is deliberately a separate comment, not an addition to the existing progress-comment mechanism above: that comment is deleted outright on a successful run, precisely when there would be real savings to show. It's sticky, not one-per-run: a repeated push to the same pull request deletes this bot's own prior Headroom comment before posting a fresh one, the same delete-then-recreate pattern the progress comment uses for its own stale entries, so a long-lived PR never accumulates a near-duplicate comment per push. The comment is skipped when nothing was proxied (a zero-savings comment on every run would be noise) and in generic mode, which posts nothing to GitHub by design regardless of this setting. Set headroom_show_savings: false to keep the numbers in the Step Summary only.

Compression is lossy, so no run writes back text it read through a tool result. The compressed copy of an issue description can have its short words removed or be replaced by a placeholder, and a model that reads a description and writes it back with gh issue edit --body saves that copy over the real text for good. Triage therefore never edits the description: it ends its run with its summary between <triage-summary> tags and the action merges the summary into the description it reads from GitHub itself, in the runner, where nothing compresses it (scripts/triage-issue-body.sh, the Append triage summary to the issue body step). Any new behaviour that changes text a person wrote has to be built the same way, with the read and the write done by the action and not by the model.

Mutually exclusive with use_bedrock, use_vertex, use_foundry, and workload identity federation. Each of those makes Claude Code call its own provider directly — confirmed directly in Headroom's own documentation for the Bedrock case: CLAUDE_CODE_USE_BEDROCK=1 bypasses ANTHROPIC_BASE_URL and the proxy entirely, silently. Because headroom_enabled defaults to true, a job that sets any of these four inputs must now also set headroom_enabled: false explicitly — otherwise the run fails validation before any install (pip or Docker) happens, rather than starting a proxy that ends up compressing nothing.

claude_code_oauth_token compatibility is confirmed, not assumed. Headroom's own source only names ANTHROPIC_API_KEY/ANTHROPIC_AUTH_TOKEN as recognised Claude auth env vars, not CLAUDE_CODE_OAUTH_TOKEN, which is what this repository's OAuth-token credential actually sets — so this looked like a real gap until it was tested against a live run. It works: a real run authenticated purely via claude_code_oauth_token, with no API key configured, reached the proxy (requests.total incremented) and reached Anthropic's own real API through it — confirmed unambiguously by a genuine subscription-tier 429 ("You've hit your weekly limit") coming back through the proxy, which cannot happen unless the real OAuth credential reached the real API. A Verify Headroom actually proxied this run step still checks the proxy's own request count after every run and reports it as the headroom_proxied output — false with a ::warning:: naming the credential type if zero requests went through — as a standing regression check against a future upstream change, not because the compatibility question itself remains open.

Version pinning. Both install methods pin an exact, immutable version — never a floating tag or range — matching how this action pins anthropics/claude-code-action and actions/checkout. headroom_version (pip) defaults to an exact headroom-ai version string; headroom_image (docker) defaults to a digest-pinned ghcr.io/headroomlabs-ai/headroom@sha256:.... Each pin is also tracked by its own anchor file, neither of which action.yml actually installs from — python/requirements-headroom.txt for Dependabot's pip ecosystem, docker/headroom.Dockerfile (never built) for its docker ecosystem, since a bare docker run inside a shell step is otherwise invisible to it. When either anchor file's pin bumps, update the matching action.yml default (headroom_version or headroom_image) to match in the same PR: Dependabot changes only the anchor, and scripts/headroom-pins.sh, run by npm test, fails until headroom_image's default equals the Dockerfile's FROM image reference and headroom_version's default equals the version python/requirements-headroom.txt pins, in action.yml and in each reusable workflow under .github/workflows that declares them as workflow_call inputs, since a reusable workflow's own default overrides action.yml's for its callers. The two pins are kept in sync to the same underlying Headroom release by hand — headroom_version and headroom_image's digest should always resolve to the identical version.

Both install methods enforce this organisation's minimum-release-age policy — the same 7-day floor its npm/pnpm config already applies to local installs, extended here to a package this action installs at runtime on every consumer's job. Before installing, each method independently verifies headroom_version/headroom_image was actually published at least 7 days ago, and refuses to proceed otherwise, guarding against a compromised maintainer account publishing a malicious release before the wider community has had time to notice. This is a fixed constant, not a configurable input, matching how the org-wide npm/pnpm policy itself is a single global setting rather than something tuned per project.

The two methods get there differently. pip queries PyPI's own public release metadata directly — a single, fully public, unauthenticated JSON endpoint. docker cannot use GitHub's own package-version API the same way: that one requires authentication a consuming repository's ambient token will not generally have against a third-party org's package (confirmed directly — an unauthenticated request against it returns 401). Instead it talks to the OCI registry itself, the same protocol docker pull uses for a public image with no login at all: it discovers the registry's own token realm from the WWW-Authenticate challenge on an unauthenticated request, rather than assuming ghcr.io's own shape — confirmed live against both ghcr.io and Docker Hub, whose realms are on entirely different hosts with different service names, so this isn't specific to the default headroom_image; it works for any digest-pinned image you point that input at. From there: an anonymous pull token, the pinned manifest (resolving the linux/amd64 platform out of a multi-arch index, matching GitHub-hosted runners), and that platform's config blob, which every OCI image carries a created timestamp on by spec. More round trips than pip's single call, but every step is genuinely anonymous and requires nothing from the consuming repository beyond outbound network access.