mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-27 14:17:10 -04:00
2630 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
5eae32ba47
|
fix(dashboard): light-mode backgrounds + aligned savings tables (#1064)
## Description Several dashboard cards and tables hardcoded a dark background (`bg-[#141414]`) that ignored the light theme. The most visible case was the **Agent Usage** section, which rendered black in light mode while every other surface flipped correctly. This converts those hardcoded backgrounds to the theme-aware `bg-card-alt` utility so the whole dashboard responds to the active theme, and tightens table column alignment. ## 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 - Replace hardcoded `bg-[#141414]` with the `bg-card-alt` utility (`var(--card-alt-bg)`: `#f3f4f6` light / `#141414` dark) across Agent Usage cards, the request-count badge, and per-agent rows. 0 hardcoded darks remain. - Per-model and per-project savings tables: `table-layout:fixed` + explicit `<col>` widths for stable columns. - Recent Requests: switch table markup to a CSS grid so columns align regardless of content length. - Add a per-project savings section (progress bar, Last Active column, empty state). - Single file changed: `headroom/dashboard/templates/dashboard.html`. ## Testing - [x] Unit tests pass (`pytest`) - [ ] Linting passes (`ruff check .`) - [ ] Type checking passes (`mypy headroom`) - [ ] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text $ .venv/Scripts/python.exe -m pytest tests/test_dashboard_agent_usage.py -q ======================== 13 passed, 1 warning in 0.89s ======================== $ grep -c "bg-\[#141414\]" headroom/dashboard/templates/dashboard.html 0 # zero hardcoded dark backgrounds remain $ grep -c "bg-card-alt" headroom/dashboard/templates/dashboard.html 8 # all converted to the theme-aware utility ``` ## Real Behavior Proof - Environment: Windows 11, headroom proxy 0.26.0 serving `/dashboard` (template read per-request from disk, no cache). - Exact command / steps: start proxy, open `http://127.0.0.1:8787/dashboard`, toggle light mode, inspect the Agent Usage section. - Observed result: Agent Usage cards (Before/After/Saved/Savings + per-agent rows) now use the light surface (`#f3f4f6`) in light mode and `#141414` in dark mode, matching the rest of the dashboard. Confirmed visually on refresh; `grep` confirms 0 hardcoded darks remain. - Not tested: `ruff`/`mypy` (HTML template, no Python changed); no automated visual-regression test added. ## 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 - [ ] 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 Presentation-only change to a single HTML template; no Python touched, so `ruff`/`mypy` are N/A. No visual-regression test added (none exists for the dashboard); existing `test_dashboard_agent_usage.py` data tests still pass. CHANGELOG not updated (UI fix). |
||
|
|
c98728363a
|
fix(proxy): treat NODE_EXTRA_CA_CERTS as additive, not replacement (#998) (#1031)
## Description `find_ca_bundle()` returns the `NODE_EXTRA_CA_CERTS` path as a bare string passed to httpx's `verify=` parameter, which makes it the sole trust store. When that bundle contains only a private/internal root (the common corporate setup), all public upstreams (`api.anthropic.com`, `api.openai.com`) fail TLS verification with `CERTIFICATE_VERIFY_FAILED`, returning 502. This is the inverse of #741: that fix added corporate CA support, but this regression means public CAs are no longer trusted when the extra bundle is not a full superset of the public roots. The fix builds an `ssl.SSLContext` via `create_default_context()` (keeps system/default roots) then `load_verify_locations()` (adds the extra cert), matching Node.js additive semantics. `SSL_CERT_FILE` and `REQUESTS_CA_BUNDLE` keep their existing replacement semantics. Closes #998 ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) ## Changes Made - Split `NODE_EXTRA_CA_CERTS` handling out of the replacement-semantics loop in `find_ca_bundle()` - When `NODE_EXTRA_CA_CERTS` is the source, return an `ssl.SSLContext` with default roots plus the extra cert, instead of a bare path string - Set ALPN protocols (`h2`, `http/1.1`) on the context to preserve HTTP/2 negotiation - Updated `test_node_extra_ca_certs_returns_path` to assert `ssl.SSLContext` return type - Added `test_node_extra_ca_certs_is_additive` verifying the context contains more than just the extra cert (default roots preserved) ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [x] New tests added for new functionality ### Test Output ```text tests/test_ssl_context.py::TestFindCaBundleNoEnvVars::test_returns_none_when_no_env_var_set PASSED tests/test_ssl_context.py::TestFindCaBundleWithValidPem::test_ssl_cert_file_returns_path PASSED tests/test_ssl_context.py::TestFindCaBundleWithValidPem::test_requests_ca_bundle_returns_path PASSED tests/test_ssl_context.py::TestFindCaBundleWithValidPem::test_node_extra_ca_certs_returns_ssl_context PASSED tests/test_ssl_context.py::TestFindCaBundleWithValidPem::test_node_extra_ca_certs_is_additive PASSED tests/test_ssl_context.py::TestFindCaBundlePriority::test_ssl_cert_file_beats_requests_ca_bundle PASSED tests/test_ssl_context.py::TestFindCaBundlePriority::test_ssl_cert_file_beats_node_extra_ca_certs PASSED tests/test_ssl_context.py::TestFindCaBundlePriority::test_requests_ca_bundle_beats_node_extra_ca_certs PASSED tests/test_ssl_context.py::TestFindCaBundleNonexistentPaths::test_nonexistent_path_is_skipped PASSED tests/test_ssl_context.py::TestFindCaBundleNonexistentPaths::test_all_nonexistent_returns_none PASSED tests/test_ssl_context.py::TestFindCaBundleNonexistentPaths::test_first_nonexistent_falls_through_to_valid PASSED 11 passed in 0.91s ``` ## Real Behavior Proof - Environment: Windows 11 Home 10.0.26200, Python 3.10.18 - Exact command / steps: Ran `python -m pytest tests/test_ssl_context.py -v` after applying the fix. The new `test_node_extra_ca_certs_is_additive` test verifies `ctx.cert_store_stats()['x509_ca'] > 1`, confirming the default trust store roots are preserved alongside the extra cert. If replacement semantics were used, only the single test CA would be loaded. - Observed result: All 11 SSL context tests pass. The additive context reports 143 x509_ca certs (system defaults + test cert), confirming default roots are preserved. - Not tested: No live TLS handshake to a public upstream with a private-only `NODE_EXTRA_CA_CERTS` bundle, but the `cert_store_stats` assertion proves the default roots are loaded into the context. ## 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] 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 ## Additional Notes The fix aligns with the design proposed in #741 and #745's own description (which said it "builds an SSLContext") but which the merged implementation did not follow. The `server.py` consumer does not need changes because httpx accepts `ssl.SSLContext` for `verify=`. ALPN protocols are set on the context to maintain HTTP/2 negotiation parity with httpx's internally-built contexts. |
||
|
|
0d89c674cd
|
feat: measure and surface token throughput (tokens/sec) through the proxy (#983)
## Description This PR implements measuring and surfacing token throughput (tokens/second) through the proxy in the `headroom perf` CLI/analyzer and the dashboard UI. It tracks multiple throughput metrics—Input (wall-clock/active), Compression, Forward, and Generation throughput—supporting both rolling percentiles (p50/p95) and current (last 5 minutes) metrics. Closes #959 ## Type of Change - [ ] 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) - [ ] Documentation update - [x] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - **Proxy Instrumentation (`headroom/proxy/outcome.py`)**: Added `total_ms`, `tok_out`, and `ttfb_ms` to the structured `PERF` logging payload. - **Log Parsing & Computations (`headroom/perf/analyzer.py`)**: Updated log parsing to read `STAGE_TIMINGS` and correlation fields from `PERF`, computing active/wall-clock throughputs for input, compression, forward, and generation stages. - **API Exposing (`headroom/proxy/server.py`)**: Exposes calculated rolling throughput percentiles and last-5-minute averages under the `throughput` field in `/stats`. - **Dashboard UI Layout (`headroom/dashboard/templates/dashboard.html`)**: Refactored the dashboard grid layout from 3 columns to 4 columns to house the new throughput hero card showing real-time token performance. - **Verification Tests (`tests/test_cli_perf_format.py`)**: Added test coverage specifically targeting token throughput log parser extraction, stage correlation, math correctness, and edge-case handling (empty fields, division by zero). ## 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 $env:PYTHONPATH="c:\Users\hp\Desktop\Headroom_oss"; .venv\Scripts\pytest tests/test_cli_perf_format.py ============================= test session starts ============================= platform win32 -- Python 3.11.15, pytest-9.1.0, pluggy-1.6.0 -- C:\Users\hp\Desktop\Headroom_oss\.venv\Scripts\python.exe cachedir: .pytest_cache rootdir: C:\Users\hp\Desktop\Headroom_oss configfile: pyproject.toml plugins: anyio-4.13.0 collecting ... collected 14 items tests/test_cli_perf_format.py::test_build_perf_summary_totals_and_pct PASSED [ 7%] tests/test_cli_perf_format.py::test_build_perf_summary_by_model_and_transform PASSED [ 14%] tests/test_cli_perf_format.py::test_build_perf_summary_empty_report_no_zero_division PASSED [ 21%] tests/test_cli_perf_format.py::test_perf_records_as_dicts_roundtrips_fields PASSED [ 28%] tests/test_cli_perf_format.py::test_perf_json_format PASSED [ 35%] tests/test_cli_perf_format.py::test_perf_json_raw_is_array PASSED [ 42%] tests/test_cli_perf_format.py::test_perf_json_raw_preserves_client_field PASSED [ 50%] tests/test_cli_perf_format.py::test_parse_perf_line_preserves_client_field PASSED [ 57%] tests/test_cli_perf_format.py::test_perf_csv_by_model PASSED [ 64%] tests/test_cli_perf_format.py::test_perf_csv_raw_per_record PASSED [ 71%] tests/test_cli_perf_format.py::test_perf_text_default_unchanged PASSED [ 78%] tests/test_cli_perf_format.py::test_perf_rejects_unknown_format PASSED [ 85%] tests/test_cli_perf_format.py::test_parse_perf_line_preserves_blank_client_field PASSED [ 92%] tests/test_cli_perf_format.py::test_throughput_parsing_and_calculations PASSED [100%] ============================== warnings summary =============================== .venv\Lib\site-packages\_pytest\config\__init__.py:1464 C:\Users\hp\Desktop\Headroom_oss\.venv\Lib\site-packages\_pytest\config\__init__.py:1464: PytestConfigWarning: Unknown config option: asyncio_mode self._warn_or_fail_if_strict(f"Unknown config option: {key}\n") .venv\Lib\site-packages\opentelemetry\util\_importlib_metadata.py:32 C:\Users\hp\Desktop\Headroom_oss\.venv\Lib\site-packages\opentelemetry\util\_importlib_metadata.py:32: DeprecationWarning: SelectableGroups dict interface is deprecated. Use select. return EntryPoints(ep for group_eps in eps.values() for ep in group_eps) -- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html ======================= 14 passed, 2 warnings in 4.77s ======================== ``` ## Real Behavior Proof - Environment: Windows 11, Python 3.11.15 - Exact command / steps: Run the pytest suite against the newly created token throughput parsing routines: `$env:PYTHONPATH="c:\Users\hp\Desktop\Headroom_oss"; .venv\Scripts\pytest tests/test_cli_perf_format.py` - Observed result: The suite executes 14 tests successfully, including the newly added `test_throughput_parsing_and_calculations` verification test verifying mathematical precision and fallback logic. - Not tested: None (all metrics are fully covered by unit tests in `test_cli_perf_format.py`) ## 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 - [x] I have updated the CHANGELOG.md if applicable ## Additional Notes - Backwards compatibility: Older log outputs lacking `tok_out` or `ttfb_ms` parse cleanly and fallback defaults prevent parser crashes. --------- Co-authored-by: Antigravity Agent <agent@antigravity.local> |
||
|
|
e8fc8a0d18
|
feat(proxy): cc-switch reconciler — keep Headroom in the request path alongside cc-switch (#1030)
## Description [cc-switch](https://github.com/farion1231/cc-switch) is a desktop provider manager for Claude Code and other coding agents; when a Claude Code provider is selected, it writes that provider's endpoint and token into `~/.claude/settings.json`. This PR adds an opt-in reconciler so Headroom can stay in Claude Code's request path when cc-switch rewrites that file during provider switches. The reconciler captures third-party Anthropic-compatible upstream URLs, points Claude back at the local Headroom proxy, and leaves official/empty OAuth settings direct unless explicitly opted in. This update also hardens the watcher so rapid settings rewrites that share the same float-second mtime are still detected. ## 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) - [ ] Documentation update - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - Added an opt-in `HEADROOM_CC_SWITCH_RECONCILE=1` watcher for cc-switch direct-injection mode. - Added loopback-only `GET/PUT /admin/upstream` runtime upstream inspection and override endpoints. - Preserved token/model settings while rewriting only `env.ANTHROPIC_BASE_URL` back to the local Headroom proxy. - Switched reconciler change detection from float-second `st_mtime` to nanosecond `st_mtime_ns` so rapid provider switches are not missed. - Added pytest coverage for capture/rewrite behavior, official-provider defaults, route-official opt-in, loop safety, enabled flags, and the same-float-mtime provider-switch case. ## Testing - [ ] 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_proxy/test_cc_switch_reconciler.py 12 passed in 0.17s python -m ruff check . All checks passed! python -m mypy headroom Success: no issues found in 359 source files python -m pytest 7 failed, 5996 passed, 492 skipped, 5814 warnings in 396.41s ``` ## Real Behavior Proof - Environment: macOS, branch `feat/cc-switch-reconciler`, Python 3.13.3. - Exact command / steps: Ran the focused reconciler pytest file, full repository ruff check, full `mypy headroom`, and full pytest from the local PR branch. - Observed result: All 12 reconciler tests passed, including the rapid provider-switch case where two writes share the same float mtime but differ by nanoseconds. Full `ruff check .` and `mypy headroom` passed. Full pytest completed with 7 failures outside the cc-switch reconciler test file. - Not tested: Live cc-switch plus Claude Code end-to-end switch. ## 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 - [ ] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [ ] New and existing unit tests pass locally with my changes - [ ] I have updated the CHANGELOG.md if applicable ## Screenshots (if applicable) N/A. ## Additional Notes Documentation and CHANGELOG updates are not included in this PR. The reconciler remains opt-in and off by default. --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
75f81cd19f
|
ci: bump the npm_and_yarn group across 3 directories with 3 updates (#1056)
[//]: # (dependabot-start) ⚠️ **Dependabot is rebasing this PR** ⚠️ Rebasing might not happen immediately, so don't worry if this takes some time. Note: if you make any changes to this PR yourself, they will take precedence over the rebase. --- [//]: # (dependabot-end) Bumps the npm_and_yarn group with 1 update in the /docs directory: [js-yaml](https://github.com/nodeca/js-yaml). Bumps the npm_and_yarn group with 1 update in the /plugins/openclaw directory: [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite). Bumps the npm_and_yarn group with 2 updates in the /sdk/typescript directory: [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) and [form-data](https://github.com/form-data/form-data). Updates `js-yaml` from 4.1.1 to 4.2.0 <details> <summary>Changelog</summary> <p><em>Sourced from <a href="https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md">js-yaml's changelog</a>.</em></p> <blockquote> <h2>[4.2.0] - 2026-06-01</h2> <h3>Added</h3> <ul> <li>Added <code>docs/safety.md</code> with notes about processing untrusted YAML.</li> <li>Added <code>maxDepth</code> (100) loader option. Not a problem, but gives a better exception instead of RangeError on stack overflow.</li> <li>Added <code>maxMergeSeqLength</code> (20) loader option. Not a problem after <code>merge</code> fix, but an additional restriction for safety.</li> <li>Added sourcemaps to <code>dist/</code> builds.</li> </ul> <h3>Changed</h3> <ul> <li>Stop resolving numbers with underscores as numeric scalars, <a href="https://redirect.github.com/nodeca/js-yaml/issues/627">#627</a>.</li> <li>Switched dev toolchains to Vite / neostandard.</li> <li>Updated demo.</li> <li>Reorganized tests.</li> <li><code>dist/</code> files are no longer kept in the repository.</li> </ul> <h3>Fixed</h3> <ul> <li>Fix parsing of properties on the first implicit block mapping key, <a href="https://redirect.github.com/nodeca/js-yaml/issues/62">#62</a>.</li> <li>Fix trailing whitespace handling when folding flow scalar lines, <a href="https://redirect.github.com/nodeca/js-yaml/issues/307">#307</a>.</li> <li>Reject top-level block scalars without content indentation, <a href="https://redirect.github.com/nodeca/js-yaml/issues/280">#280</a>.</li> <li>Ensure numbers survive round-trip, <a href="https://redirect.github.com/nodeca/js-yaml/issues/737">#737</a>.</li> <li>Fix test coverage for issue <a href="https://redirect.github.com/nodeca/js-yaml/issues/221">#221</a>.</li> <li>Fix flow scalar trailing whitespace folding, <a href="https://redirect.github.com/nodeca/js-yaml/issues/307">#307</a>.</li> <li>Fix digits in YAML named tag handles.</li> </ul> <h3>Security</h3> <ul> <li>Fix potential DoS via quadratic complexity in merge - deduplicate repeated elements (makes sense for malformed files > 10K).</li> </ul> <h2>[3.14.2] - 2025-11-15</h2> <h3>Security</h3> <ul> <li>Backported v4.1.1 fix to v3</li> </ul> </blockquote> </details> <details> <summary>Commits</summary> <ul> <li>See full diff in <a href="https://github.com/nodeca/js-yaml/commits">compare view</a></li> </ul> </details> <br /> Updates `vite` from 8.0.10 to 8.0.16 <details> <summary>Release notes</summary> <p><em>Sourced from <a href="https://github.com/vitejs/vite/releases">vite's releases</a>.</em></p> <blockquote> <h2>v8.0.16</h2> <p>Please refer to <a href="https://github.com/vitejs/vite/blob/v8.0.16/packages/vite/CHANGELOG.md">CHANGELOG.md</a> for details.</p> <h2>v8.0.15</h2> <p>Please refer to <a href="https://github.com/vitejs/vite/blob/v8.0.15/packages/vite/CHANGELOG.md">CHANGELOG.md</a> for details.</p> <h2>v8.0.14</h2> <p>Please refer to <a href="https://github.com/vitejs/vite/blob/v8.0.14/packages/vite/CHANGELOG.md">CHANGELOG.md</a> for details.</p> <h2>v8.0.13</h2> <p>Please refer to <a href="https://github.com/vitejs/vite/blob/v8.0.13/packages/vite/CHANGELOG.md">CHANGELOG.md</a> for details.</p> <h2>v8.0.12</h2> <p>Please refer to <a href="https://github.com/vitejs/vite/blob/v8.0.12/packages/vite/CHANGELOG.md">CHANGELOG.md</a> for details.</p> <h2>v8.0.11</h2> <p>Please refer to <a href="https://github.com/vitejs/vite/blob/v8.0.11/packages/vite/CHANGELOG.md">CHANGELOG.md</a> for details.</p> </blockquote> </details> <details> <summary>Changelog</summary> <p><em>Sourced from <a href="https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md">vite's changelog</a>.</em></p> <blockquote> <h2><!-- raw HTML omitted --><a href="https://github.com/vitejs/vite/compare/v8.0.15...v8.0.16">8.0.16</a> (2026-06-01)<!-- raw HTML omitted --></h2> <h3>Bug Fixes</h3> <ul> <li><strong>deps:</strong> reject UNC paths for launch-editor-middleware (<a href="https://redirect.github.com/vitejs/vite/issues/22571">#22571</a>) (<a href=" |
||
|
|
1e437d781b
|
docs: document HEADROOM_BETA_HEADER_STICKY and HEADROOM_BETA_TRACKER_MAX_SESSIONS (#1060)
## Description Documents `HEADROOM_BETA_HEADER_STICKY` and `HEADROOM_BETA_TRACKER_MAX_SESSIONS` — two env vars that exist in source and tests but are absent from all `.md` / `.mdx` docs. Adds a **Session Beta Header Tracking** section explaining the `SessionBetaTracker` behavior, its prefix-cache rationale, and the operator trade-off. An operator debugging a beta-header-related upstream rejection cannot discover the knob or the off-switch without reading source. Closes #1059 ## Type of Change - [ ] 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) - [x] Documentation update - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made **`wiki/configuration.md`** - Two new rows in the Environment Variables table: `HEADROOM_BETA_HEADER_STICKY` and `HEADROOM_BETA_TRACKER_MAX_SESSIONS` - New `## Session Beta Header Tracking` section with: what the mechanism does, why it exists (prefix-cache stability), the operator trade-off, and how to disable **`docs/content/docs/configuration.mdx`** - Same two rows added to the Environment Variables table - Same `### Session Beta Header Tracking` section (matching heading level) ## Testing - [ ] Unit tests pass (`pytest`) - [ ] Linting passes (`ruff check .` — N/A, docs-only change to `.md`/`.mdx` files) - [ ] Type checking passes (`mypy headroom` — N/A, docs-only) - [ ] New tests added for new functionality — N/A, docs-only - [x] Manual testing performed ### Test Output ```text Manual verification: confirmed env var names, accepted values, defaults, and LRU bound match code (helpers.py:1610-1629, 1613-1614, 1820-1822). Existing tests exercise the behavior (tests/test_anthropic_beta_session_sticky.py:124). ``` ## Real Behavior Proof - Environment: Ubuntu 24.04, Python 3.12, Headroom v0.25.0, provider: Anthropic (Claude Code via `headroom wrap`) - Exact command / steps: `grep -ri "HEADROOM_BETA_HEADER_STICKY" README.md CHANGELOG.md wiki/ docs/` → 0 matches before; source inspection of helpers.py:1605-1856, anthropic.py:918-961, prefix_tracker.py:316-335 - Observed result: env vars now documented in both wiki + docs mirrors; behavior rationale and trade-off explained - Not tested: end-to-end proxy run with the new docs in place (docs-only change; behavior unchanged) ## 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 - [ ] I have added tests that prove my fix is effective or that my feature works — N/A, docs-only - [ ] New and existing unit tests pass locally with my changes — N/A, docs-only - [ ] I have updated the CHANGELOG.md if applicable — N/A, changelog is auto-managed by release-please ## Screenshots (if applicable) N/A ## Additional Notes Mirrors the same dual-file pattern used in #579 (wiki + docs mdx). The documented behavior is contractual — confirmed by tests/test_anthropic_beta_session_sticky.py:124 (test_beta_seen_turn_1_present_in_turn_2_even_if_client_drops) and the test module docstring naming Claude Code and Codex CLI as the targeted clients. |
||
|
|
8cea290a15
|
docs(ci): add CI/CD flow diagrams (#1062)
## Description
Adds a visual CI/CD flow reference for Headroom so contributors can
quickly understand the gated PR, release, Docker, docs deploy, fork
approval, and manual validation paths. Also updates the release
documentation to match the current release-please release flow instead
of the stale push-to-main release model.
## Type of Change
- [ ] 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)
- [x] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)
## Changes Made
- Added `docs/content/docs/ci-cd-flows.mdx` with Mermaid diagrams and
decision trees for PR review, release publishing, Docker publishing,
docs deploys, fork workflow approval, and manual validation.
- Updated `docs/content/docs/releases.mdx` to describe the current
`release-please` -> GitHub Release -> `release.yml` publishing path.
- Added the CI/CD flow page and existing release page to docs
navigation.
- Added Mermaid to the docs code highlighter language list.
- Restored missing docs helper modules and aligned Fumadocs dependencies
so the docs app can install, generate sources, type-check, and build.
## Testing
- [ ] Unit tests pass (`pytest`)
- [ ] Linting passes (`ruff check .`)
- [ ] Type checking passes (`mypy headroom`)
- [ ] New tests added for new functionality
- [x] Manual testing performed
### Test Output
```text
$ npm ci
> headroom-docs@0.0.0 postinstall
> fumadocs-mdx
[MDX] generated files
added 365 packages, and audited 367 packages
$ npm run types:check
> fumadocs-mdx && next typegen && tsc --noEmit
[MDX] generated files
Generating route types...
✓ Types generated successfully
$ npm run build
> next build
✓ Compiled successfully
Running TypeScript ...
Generating static pages ...
✓ Generating static pages (122/122)
Note: next build completed successfully and emitted two existing Recharts container-size warnings during static generation.
$ git diff --check
# no output
$ act workflow_dispatch -W .github/workflows/docs.yml -n
*DRYRUN* [Deploy Documentation/deploy] 🏁 Job succeeded
```
## Real Behavior Proof
- Environment: Windows local checkout, branch `docs-ci-flow`, Node.js
v22.22.0, `act` 0.2.87.
- Exact command / steps: Ran `npm ci`, `npm run types:check`, and `npm
run build` from `docs/`; ran `git diff --check` and `act
workflow_dispatch -W .github/workflows/docs.yml -n` from the repository
root.
- Observed result: Docs dependencies install, Fumadocs source generation
includes `ci-cd-flows.mdx`, TypeScript passes, Next production build
completes, whitespace check passes, and the docs workflow dry-run
succeeds under `act`.
- Not tested: Full live GitHub Pages deploy and registry/release
publishing, because this PR only changes docs and docs build wiring.
## 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
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] I have updated the CHANGELOG.md if applicable
## Screenshots (if applicable)
N/A. This is documentation and build wiring; the diagrams are Mermaid
source blocks in the docs page.
## Additional Notes
- No issue is linked because this PR was not opened for a specific
tracked issue.
- `npm run build` still reports two pre-existing Recharts container-size
warnings while completing successfully.
- Python unit/lint/type checks and changelog updates are not applicable
to this docs-only change.
|
||
|
|
a99dc61424
|
feat: output-token reduction — verbosity shaper, per-user learning, counterfactual savings (#965)
## Description
Adds the first levers that reduce the tokens the model **writes back**
(output), complementing Headroom's existing input compression. Output
costs 5× input on Opus-class models and is full of waste (ceremony,
restated code, deep "thinking" on routine steps). Two phases in one
self-contained PR off `main`: the request-side output shaper, then
per-user verbosity learning plus an honest counterfactual savings
estimator and dashboard surfacing.
## Type of Change
- [ ] 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
- **Output shaper** (`output_shaper.py`, opt-in
`HEADROOM_OUTPUT_SHAPER=1`): cache-safe verbosity steering appended to
the system-prompt tail (5 levels); effort routing that lowers
`output_config.effort` on mechanical tool-result continuations; legacy
`thinking.budget_tokens` clamp. Never injects effort where absent, never
toggles `thinking.type`.
- **`headroom learn --verbosity`**: mines Claude Code transcripts for
behavioral signals (interrupts, length-adaptive fast-skips, echo ratio),
recommends a verbosity level (heuristic + optional `--llm-judge`), and
seeds the savings baseline.
- **Counterfactual estimator** (`output_savings.py`): per-stratum
synthetic-control (estimated) + A/B holdout (measured) with a propagated
95% CI; conversation-stable arm assignment for A/B validity and
prefix-cache safety.
- **AIMD verbosity controller** (`verbosity_controller.py`):
additive-increase / fast-back-off state machine; live signal emission
gated off by default.
- **Wiring + surfaces**: shaper resolves the learned level; recording
rides the existing `transforms_applied` channel through the outcome
funnel (no `RequestOutcome` changes); `headroom output-savings` CLI;
dashboard "Output Tokens Saved" card.
- **Docs**: simple-words user guide + design doc with the counterfactual
methodology.
## 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
$ pytest tests/test_output_savings.py tests/test_output_savings_cli.py \
tests/test_verbosity_learn.py tests/test_verbosity_controller.py \
tests/test_output_shaper.py -q
94 passed in 0.54s
$ pytest tests/test_request_outcome.py tests/test_handler_outcome_tag_invariant.py \
tests/test_proxy_dashboard_stats_cache.py -q
44 passed
$ ruff format --check .
831 files already formatted
$ mypy headroom --ignore-missing-imports
Success: no issues found in 361 source files
```
## Real Behavior Proof
- Environment: macOS, Python 3.12 (`.venv`), `anthropic` 0.76, live API
model `claude-opus-4-8`.
- Exact command / steps: `HEADROOM_OUTPUT_SHAPER=1`; `headroom learn
--verbosity --apply` (seeds level + baseline); `python
scripts/eval_output_shaper.py A` (live before/after); simulate holdout
traffic then `headroom output-savings`.
- Observed result: code-review ask — baseline 1,750 output tokens → L2
1,354 (−22.7%) → L3 599 (−65.8%), same bugs found. `learn --verbosity`
on 24 real sessions → 11% interrupt / 26% fast-skip → L3 (high
confidence). Measured A/B path → 31.7% reduction (95% CI 27.7%–35.7%).
94 new tests + 44 existing outcome/dashboard tests green; ruff + mypy
clean.
- Not tested: live streaming-path recording exercised only via unit
tests (the `transforms_applied` funnel is shared across paths); runtime
AIMD signal emission is gated off by default and not exercised live.
## 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
Output savings are counterfactual (we never observe what the model
*would* have written), so the estimator separates **estimated** (vs a
learned baseline) from **measured** (A/B holdout via
`HEADROOM_OUTPUT_HOLDOUT`) and always reports a confidence band — never
a single made-up number. CHANGELOG left unchecked (release-please
manages it). Runtime AIMD self-tuning is intentionally a TODO
(controller built/tested; live signal emission gated behind
`HEADROOM_VERBOSITY_AUTOTUNE`).
|
||
|
|
b7be3814f1
|
feat: compression extraction — Rust knob exposure, CCR hardening, traffic audits (#818)
## Description
A data-driven push for better compression savings without accuracy loss,
in four parts: expose and tune the Rust compressor knobs, harden the CCR
retrieval store, add traffic-audit tooling that sizes opportunities from
real transcripts, and introduce **read maturation** — a new,
live-validated mechanism that compresses Read outputs *before* they ever
enter the provider prefix cache.
## 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)
- [ ] Documentation update
- [x] Performance improvement
- [ ] Code refactoring (no functional changes)
## Changes Made
### 1. Rust compressor extraction
- Expose `lossless_min_savings_ratio` end-to-end and lower the default
0.30 → 0.15 (lockstep across Rust, PyO3, and both Python config classes)
so the lossless Table/CSV compaction path wins more often.
- Expose the `CompactConfig` heuristics (core-field fraction,
heterogeneity ratio, flatten cap, bucket bounds) through PyO3 + Python.
- `SearchCompressor` grouped-by-file output (`rg --heading` style — path
once per file instead of per match). Library default off; the proxy
enables it in token mode.
- Complete `factor_out_constants`: constant fields now emit once in a
`_constant_fields` sentinel with slim rows (defensive per-item value
match; default off).
- `ContentRouter` accepts a SmartCrusher config override and the
search-grouping knob.
### 2. CCR store hardening
- Session-scale TTL: 300s → 1800s (CCRConfig, CompressionEntry,
CompressionStore, Rust `DEFAULT_TTL` — lockstep).
- **SQLite is the default CCR backend** (`~/.headroom/ccr_store.db`,
WAL): survives proxy restarts and is shared across workers.
`HEADROOM_CCR_BACKEND=memory` opts out.
- Multi-worker safety: `busy_timeout`, and corruption detection narrowed
so transient `SQLITE_BUSY` errors can never trigger database deletion.
- Data-at-rest hygiene: `chmod 600` on db + sidecars, expired rows swept
at open.
- Retrieval-miss messages are actionable (re-read the file / re-run the
command).
### 3. Traffic audit tooling (measure before tuning)
- `headroom audit-reads`: sizes Read opportunities from local Claude
Code transcripts (read share, stale %, line-number overhead, context
residency, cache-death windows).
- `--simulate-maturation`: Mechanism B risk sizing (re-read rates,
never-touched-again share, quiesce coverage, at-risk edits).
- `--codex`: shell-read classifier for Codex transcripts (rtk-wrapper
aware, workdir resolution).
- Findings that shaped this PR (81 sessions): Reads are 67% of tool
bytes; median Read lingers 118 turns (~13x lifetime cost); a prototyped
repeat-Read dedup measured 0.1% and was **removed** rather than shipped
as dead code.
### 4. Read maturation (Mechanism B) — experimental, default OFF
- Activity-based: a fresh large Read is held **out** of the provider
cache (trailing breakpoint relocated before it), stays verbatim while
its file is active, and matures into a CCR-backed marker once the file
is quiet for `quiesce_turns` (default 5; `max_hold_turns` bounds busy
files).
- Only the final compressed form ever enters the cache — **no cached
byte is ever mutated**; matured markers replay byte-identically.
- Wired into the Anthropic handler behind `--read-maturation` /
`HEADROOM_READ_MATURATION=1`; session state rides on the prefix tracker;
advisory (can never fail a request).
- Live-validated against the Anthropic API: held content excluded from
cache_creation; after maturation the prior cached prefix still served —
the no-bust invariant holds end-to-end.
### 5. Rebase / CI fixups (this update)
- Rebased onto latest `main` (was 28 commits behind): picks up `ci: pass
CODECOV_TOKEN to coverage uploads (#968)`, which is what was turning the
4 test shards red — the tests themselves passed (1528) but the post-test
codecov upload exited non-zero on a protected branch.
- Resolved the duplicate `lossless_min_savings_ratio` that two
independent main/branch additions left in `SmartCrusherConfig` and the
Rust-config kwarg (import-time `SyntaxError` + mypy `no-redef`).
- Aligned CCR tests with the new defaults (SQLite backend, 1800s TTL)
across `test_ccr`, `test_adapter_hooks`, `test_compression_store`,
`test_proxy_ccr`, and the lossy row-drop bridge test.
## Testing
<!-- Check what you actually ran, then paste the real command output
below. -->
- [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_proxy_ccr.py tests/test_ccr.py tests/test_compression_store.py tests/test_adapter_hooks.py tests/test_ccr_row_drop_store_bridge.py -q
170 passed, 4 warnings in 42.49s
$ python -m pytest tests/test_audit_reads.py tests/test_audit_codex.py tests/test_read_maturation.py tests/test_transforms_content_router.py tests/test_smart_crusher_toin_attachment.py -q
83 passed
$ mypy headroom/
Success: no issues found in 365 source files
$ python -m compileall headroom/ -q
COMPILE-OK
# CI (run 27488990477, pre-rebase head): all 4 shards ran to completion —
# "1528 passed, 120 skipped, 4922 deselected"
# The red shards were the codecov upload step, not test failures; fixed by
# the #968 rebase above.
```
## Real Behavior Proof
- Environment: macOS (darwin), Python 3.12 venv; branch
`feat/compression-extraction` rebased onto `origin/main` (head
|
||
|
|
ff221e6346
|
ci: scope PR workflow runs by changed paths (#1067)
## Description Makes PR workflow runs more selective by routing docs-only changes to docs validation instead of the full CI workflow, while preserving workflow validation and existing code/e2e/release gates for applicable changes. ## Type of Change - [ ] 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 - [x] Code refactoring (no functional changes) ## Changes Made - Added `pull_request.paths-ignore` to `.github/workflows/ci.yml` so docs/wiki/markdown-only PRs do not queue the general CI workflow. - Removed `.github/workflows/ci.yml` from the CI internal `code` path filter so CI-only workflow edits can run workflow validation without forcing Python/Rust code jobs. - Added a docs PR validation job to `.github/workflows/docs.yml` for `docs/**`, `wiki/**`, `mkdocs.yml`, and docs workflow changes. - Reduced default docs workflow token permissions to `contents: read`, with `contents: write` scoped only to the deploy job. - Added docs workflow dry-runs to `scripts/validate-workflows.sh` so local/CI workflow validation covers the new PR and manual docs paths. ## Testing - [ ] Unit tests pass (`pytest`) - [ ] Linting passes (`ruff check .`) - [ ] Type checking passes (`mypy headroom`) - [ ] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text $ actionlint .github/workflows/ci.yml .github/workflows/docs.yml # no output $ act pull_request -W .github/workflows/docs.yml -n *DRYRUN* [Deploy Documentation/validate] 🏁 Job succeeded $ act workflow_dispatch -W .github/workflows/docs.yml -n *DRYRUN* [Deploy Documentation/deploy] 🏁 Job succeeded $ act pull_request -W .github/workflows/ci.yml -n *DRYRUN* [CI/changes] 🏁 Job succeeded *DRYRUN* [CI/commitlint] 🏁 Job succeeded $ python -m mkdocs build INFO - Documentation built in 1.28 seconds $ bash scripts/validate-workflows.sh # completed successfully; act dry-runs passed. Some unsupported runner-platform matrix entries are skipped by local act, as before. $ git diff --check # no output ``` ## Real Behavior Proof - Environment: Windows local checkout, branch `smart-pr-runs`, `act` 0.2.87, temporary local `actionlint` installed via `go install`. - Exact command / steps: Ran `actionlint` against changed workflows, `act` dry-runs for docs PR/manual paths and CI PR path, actual `python -m mkdocs build`, full `scripts/validate-workflows.sh`, and `git diff --check`. - Observed result: Changed workflows lint cleanly; docs PR and manual docs workflow paths dry-run successfully; CI PR dry-run still covers `changes` and `commitlint`; MkDocs builds; repository workflow validation script completes with the new docs dry-runs included. - Not tested: Full non-dry-run GitHub Actions execution on hosted runners before PR creation. ## 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 - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] New and existing unit tests pass locally with my changes - [ ] I have updated the CHANGELOG.md if applicable ## Screenshots (if applicable) N/A. ## Additional Notes - No issue is linked because this PR was not opened for a specific tracked issue. - `mkdocs build` reports existing docs/nav warnings but exits successfully; strict mode currently fails on existing warnings, so the PR validation uses the deploy-compatible non-strict build. - Python unit/lint/type checks are not applicable to this workflow-only change. |
||
|
|
b2f04e4ef7
|
fix(deps): make litellm optional on Python 3.14 (#956) (#993)
## Description `litellm` is a hard dependency and its metadata caps `Requires-Python >=3.10,<3.14`, so `pip install headroom-ai` is unsatisfiable on Python 3.14. But litellm is only used for model registry / pricing / non-core providers — all lazily imported behind `ImportError` guards — never on the core compression or Anthropic proxy path. Refs #956 (install half). ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) ## Changes Made - Add a `python_version < '3.14'` marker to both litellm declarations (core deps + dev extra); installs unchanged on <=3.13, skipped on 3.14 (matches the existing rapidocr/tomli marker pattern). ## Testing - [x] Unit tests pass (`pytest`) - [x] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text $ pytest tests/test_litellm_optional.py -q 2 passed in 0.10s $ python3.14 -m pip install dist/headroom_ai-0.25.0-cp310-abi3-linux_x86_64.whl Successfully installed headroom-ai-0.25.0 ... # litellm NOT installed $ python3.14 -c "import importlib.util as u; print(u.find_spec('litellm') is not None)" False ``` ## Real Behavior Proof - Environment: fresh venv on CPython 3.14.5, Linux - Exact command / steps: built the abi3 wheel, `pip install` it on Python 3.14, then `import headroom` + start the proxy + send a compressible request - Observed result: install exits 0 with litellm skipped; `import headroom` works; the proxy compresses (29913 -> 27626 tokens). Stock 0.25.0 cannot install on 3.14 at all. - Not tested: litellm-backed features on 3.14 (intentionally unavailable there until litellm supports 3.14) ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
addebdb29c
|
feat(proxy): make COMPRESSION_TIMEOUT_SECONDS configurable via env (#946) (#991)
## Description The compression-pipeline timeout was hard-coded at 30s, so slow CPUs and long Claude Code conversations had no recourse. #946 asks to wire `HEADROOM_COMPRESSION_TIMEOUT_SECONDS` through. Refs #946. ## Type of Change - [x] New feature (non-breaking change that adds functionality) ## Changes Made - Read `HEADROOM_COMPRESSION_TIMEOUT_SECONDS` from the environment (float), falling back to 30 on an unparseable value. ## Testing - [x] Unit tests pass (`pytest`) - [x] New tests added for new functionality ### Test Output ```text $ pytest tests/test_proxy/test_compression_timeout_config.py -q 4 passed in 0.09s ``` ## Real Behavior Proof - Environment: Linux, Python 3.13, editable build of this branch - Exact command / steps: `HEADROOM_COMPRESSION_TIMEOUT_SECONDS=88 python -c "import headroom.proxy.helpers as h; print(h.COMPRESSION_TIMEOUT_SECONDS)"` - Observed result: prints `88.0` (default `30.0`; an unparseable value falls back to `30.0`) - Not tested: a live compression actually exceeding the configured timeout under real load ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
5208e32d64
|
fix: suppress '[transformers] PyTorch was not found' startup warning (#1066)
## Description
`transformers` is imported for lightweight availability checks (e.g. the
kompress ONNX probe in `_is_onnx_available`) and at memory-embedder
import. When PyTorch is not installed, `transformers` emits a
non-actionable `[transformers] PyTorch was not found. Models won't be
available...` warning on proxy startup. PyTorch is optional in headroom,
so the warning is noise. This silences it by setting
`TRANSFORMERS_VERBOSITY=error`.
## 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/proxy/server.py`:
`os.environ.setdefault("TRANSFORMERS_VERBOSITY", "error")` at module
import, before logging is configured.
- `headroom/memory/adapters/embedders.py`: same `setdefault`, alongside
the existing HF Hub warning suppressions.
- `setdefault` preserves any operator-provided `TRANSFORMERS_VERBOSITY`
override.
## Testing
- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [ ] Type checking passes (`mypy headroom`)
- [ ] New tests added for new functionality
- [x] Manual testing performed
### Test Output
```text
$ .venv/Scripts/python.exe -m ruff check headroom/proxy/server.py headroom/memory/adapters/embedders.py
All checks passed!
```
## Real Behavior Proof
- Environment: Windows 11, Python 3.11, transformers 5.12.1, PyTorch not
installed.
- Exact command / steps: `python -c "import io, logging; from
headroom.proxy.server import ProxyConfig; buf=io.StringIO();
logging.getLogger('transformers').addHandler(logging.StreamHandler(buf));
import transformers, os;
print(os.environ.get('TRANSFORMERS_VERBOSITY'));
print(repr(buf.getvalue()))"`
- Observed result: prints `error` then `''` — importing the proxy server
sets the env var, and the subsequent `transformers` import emits no
captured warning. Without the fix the same steps print the
`[transformers] PyTorch was not found...` line.
- Not tested: `mypy` not run; behavior with an explicit operator
override (preserved by `setdefault`) not exercised.
## 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
- [ ] 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
Two-line, presentation-only change (suppresses a noisy startup log). No
new test added — the behavior is a third-party library log-verbosity
setting; manual proof above. `mypy` not run; CHANGELOG not updated.
|
||
|
|
c9853f30cb
|
fix: pure-Python content detector default on Windows (clean) (#1063)
## Description Native Magika content detection initializes an ONNX Runtime session. On Windows that init can leave a background thread alive past the Rust-side 5s timeout, contending on the process-wide DLL loader lock. This makes `_detect_content` select a pure-Python regex detector by default on Windows so no ONNX session is ever created there. Supersedes #1043 (clean single-commit version; the original branch bundled unrelated dashboard/hooks changes and a fix-then-revert noise pair). Closes #1043 ## 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 - Add `_resolve_detect_backend()`: honors `HEADROOM_DETECT_BACKEND=rust|python`; otherwise defaults to `python` on Windows (`sys.platform == "win32"`) and `rust` elsewhere. - `_detect_content()` routes through the resolved backend. On the Python path it calls the existing pure-Python regex detector (`content_detector.detect_content_type`) and never imports/initializes the native ONNX session. - One-time warn-level log line documents the Python-backend choice and the override env var. - Tests covering env override (both directions), the Windows default, and that the native detector is not invoked on the Python path. ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [ ] Type checking passes (`mypy headroom`) - [x] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text $ .venv/Scripts/python.exe -m pytest tests/test_transforms_content_router.py -q ======================== 24 passed, 1 warning in 0.36s ======================== $ .venv/Scripts/python.exe -m ruff check headroom/transforms/content_router.py All checks passed! ``` ## Real Behavior Proof - Environment: Windows 11 (win32), Python 3.11, headroom 0.26.0. - Exact command / steps: `.venv/Scripts/python.exe -c "import sys; from headroom.transforms.content_router import _resolve_detect_backend; print(sys.platform, _resolve_detect_backend())"` - Observed result: prints `win32 python` — the Windows host selects the pure-Python backend, so no ONNX/Magika session is created and the loader-lock hang cannot occur. Setting `HEADROOM_DETECT_BACKEND=rust` forces the native chain (covered by tests). - Not tested: native chain on a real Windows host with `HEADROOM_DETECT_BACKEND=rust` (intentionally avoided — that path is the deadlock risk being mitigated); `mypy` not run. ## 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 The Rust side (`magika_detector::session()`) already converts an init hang into a recoverable `Err(timeout)` so detection falls through magika → unidiff → PlainText with no user-visible failure. This PR adds belt-and-suspenders: on Windows the native session is never created, removing the loader-lock contention entirely rather than relying on the timeout. `mypy` not run locally; CHANGELOG not updated (single-file bugfix). |
||
|
|
b81a4a7a16
|
chore: release main (#931)
🤖 I have created a release *beep* *boop* --- <details><summary>0.26.0</summary> ## [0.26.0](https://github.com/chopratejas/headroom/compare/v0.25.0...v0.26.0) (2026-06-16) ### Features * add Copilot BYOK provider wrapper utilities and CLI support ([#1041](https://github.com/chopratejas/headroom/issues/1041)) ([ |
||
|
|
4e9d7df0ec
|
ci: align codecov-action to v5 in native e2e workflows (#978)
## Description Bump `codecov/codecov-action` from `@v4` to `@v5` in the two native e2e workflows, and rename the `file:` input to `files:` to match the v5 API. The main `ci.yml` already uses `@v5` with `files:`; this aligns the remaining Codecov uploads. Follow-up to #968. ## Type of Change - [x] Code refactoring (no functional changes) ## Changes Made - `.github/workflows/install-native-e2e.yml`: `codecov/codecov-action@v4` to `@v5`, and `file:` to `files:`. - `.github/workflows/wrap-native-e2e.yml`: `codecov/codecov-action@v4` to `@v5`, and `file:` to `files:`. - All three Codecov uploads now use `@v5` with the `files:` input. ## Testing - [x] Manual testing performed ### Test Output ```text python -c "import yaml; [yaml.safe_load(open(f, encoding='utf-8')) for f in ['.github/workflows/install-native-e2e.yml','.github/workflows/wrap-native-e2e.yml','.github/workflows/ci.yml']]; print('all workflows parse as valid YAML')" all workflows parse as valid YAML rg -n "codecov/codecov-action@|^\s+file:|^\s+files:" .github/workflows/install-native-e2e.yml .github/workflows/wrap-native-e2e.yml .github/workflows/ci.yml .github/workflows/install-native-e2e.yml:61: uses: codecov/codecov-action@v5 .github/workflows/install-native-e2e.yml:63: files: ./coverage-install-native.xml .github/workflows/wrap-native-e2e.yml:66: uses: codecov/codecov-action@v5 .github/workflows/wrap-native-e2e.yml:68: files: ./coverage-wrap-native.xml .github/workflows/ci.yml:209: uses: codecov/codecov-action@v5 .github/workflows/ci.yml:211: files: coverage-${{ matrix.shard }}.xml git diff --check upstream/main...HEAD # no output ``` ## Real Behavior Proof - Environment: local Windows 11 checkout, Python 3.13.13 with PyYAML, ripgrep. - Exact command / steps: rebased onto current `main`, parsed the three workflow YAML files, confirmed all Codecov action references use `@v5`, confirmed upload inputs use `files:`, and checked the diff for whitespace errors. - Observed result: workflow YAML parses; native e2e and CI Codecov upload steps are aligned on `@v5`/`files:`; no whitespace errors. - Not tested: live Codecov upload, because it requires Actions secrets and GitHub-hosted runners. The PR workflows exercise the changed steps. ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review |
||
|
|
5eec7f6701
|
fix(serena): migrate stale Headroom-installed Serena entry on re-wrap (#1008)
## Description #1003 added `--open-web-dashboard False` to the Serena spec to stop the dashboard browser tab popping up on every session — but the flag only reaches **fresh** registrations. `register_server` returns `MISMATCH` and refuses to overwrite a differing entry unless `force=True`, and the Claude wrap path calls `_setup_serena_mcp` **without** force (unlike the Codex path, which passes `force=True`). So anyone wrapped before #1003 has a `serena` entry whose args lack the flag. Every re-wrap detects the mismatch, prints `existing config differs … To update: remove the existing serena MCP entry, then rerun`, and gives up — the stale spec, and the popup, persist forever. The fix never reaches already-wrapped users, which is most of them. This completes #1003 by migrating those stale entries in place. Related to #1003 ## 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 - `_setup_serena_mcp` now migrates a stale entry: on `MISMATCH` (and when not already forced), it force-updates to the current spec **only when the ledger proves Headroom installed the entry currently on disk** (`headroom_installed_matching`). Prints `Serena MCP: migrated previously-installed entry to current spec`. - A user-managed Serena (absent from the ledger) is left untouched and the mismatch is reported exactly as before — the same ownership check `--no-serena` / `_disable_serena_mcp` already use, so a hand-rolled Serena is never clobbered. - No call-site change: migration is self-contained and gated on ledger ownership, not on the `force` param, so the Codex path keeps hard-overwriting as before. - New `tests/test_cli/test_serena_migrate.py`. ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [ ] Type checking passes (`mypy headroom`) - [x] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text $ python -m pytest tests/test_cli/test_serena_migrate.py tests/test_cli/test_serena_disable.py tests/test_mcp_registry/ -q ============================== 89 passed in 4.26s ============================== $ ruff check headroom/cli/wrap.py tests/test_cli/test_serena_migrate.py All checks passed! ``` ## Real Behavior Proof - Environment: Fedora 44, Python 3.14.5, headroom working tree at this branch (off `upstream/main`), real `ClaudeRegistrar` (cli=None → file-backed), isolated `$HOME` + ledger via `tempfile` and `HEADROOM_WORKSPACE_DIR`. - Exact command / steps: wrote a pre-#1003 `serena` entry (no flag) into a throwaway `.claude/.claude.json`, recorded it in the ledger as Headroom-owned, then ran `_setup_serena_mcp(ClaudeRegistrar(claude_cli=None, home_dir=tmp), context="claude-code")`. Repeated with a `custom-serena` entry absent from the ledger. - Observed result: Headroom-owned entry rewritten on disk to end with `--open-web-dashboard False` (`migrated previously-installed entry` printed); user-managed `custom-serena` entry left byte-for-byte unchanged with the mismatch reported; fresh-install path writes the dashboard-off spec. Discovered originally on a live machine whose `~/.claude.json` kept the popup across re-wraps until the entry was hand-fixed — this PR removes the need for that. - Not tested: did not launch the Claude CLI end-to-end (the dashboard auto-open is Serena's documented response to `web_dashboard_open_on_launch=False`, traced in #1003); `mypy` not run. ## 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 / version: left to release-please (the repo's `fix:`-driven release PR aggregator), so no manual CHANGELOG edit. - Docs unchanged: behavior is internal to `headroom wrap`; the user-visible outcome (no dashboard popup) matches #1003's documented intent. - `mypy` not run locally (heavy dev extra pulls a compiled dep in this environment); happy to add the result if CI doesn't cover it. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
74ae781644
|
fix(codex): retag thread providers so history menu stays whole across the proxy boundary (#1034)
## Description
Codex stamps every thread with the `model_provider` it ran under and
filters its
history/projects menu by the currently active provider set. When
Headroom rewrites
Codex's config to route through the custom `headroom` provider, threads
created through
Headroom are tagged `headroom` while native threads keep `openai` — so
the two sets
never appear in the same menu. The visible symptom: enabling Headroom
appears to "lose"
the entire native Codex history, and disabling it hides everything
created while wrapped.
This reconciles the thread tags in Codex's SQLite store alongside the
existing config
edits, so the menu stays whole across the proxy boundary in both
directions:
`openai -> headroom` on enable/wrap, `headroom -> openai` on
revert/unwrap. Only rows
matching the source provider are touched, so third-party providers (e.g.
`anthropic`)
are left alone. The provider key cannot be unified by config — Codex
rejects naming a
custom provider `openai` ("reserved built-in provider IDs") — so
retagging the store is
the only path. A DB-only retag is sufficient: resuming a retagged
session still routes
completions through the active provider; the rollout `.jsonl` files do
not need
rewriting.
Every operation is best-effort: a missing store, a missing `threads`
table, or a corrupt
store is logged and skipped, never raised, so install/uninstall and
wrap/unwrap never
fail on account of the history menu. The store is WAL-mode, so the
update succeeds even
while Codex is running; the short busy timeout only covers a transient
checkpoint lock.
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
- New `headroom/providers/codex/threads.py`: best-effort retag of Codex
thread provider
tags across both known stores (`<codex_home>/sqlite/state_5.sqlite` for
the GUI and
`<codex_home>/state_5.sqlite` for the CLI/TUI). `retag_to_headroom` /
`retag_to_native`
wrap the directional helper. `codex_home` is passed in by callers (never
re-derived from
`Path.home()`), so tests stay pointed at a temp dir.
- `providers/codex/install.py`: `apply_provider_scope` calls
`retag_to_headroom` after
writing the provider block; `revert_provider_scope` calls
`retag_to_native` after
stripping it.
- `cli/wrap.py`: `_inject_codex_provider_config` calls
`retag_to_headroom`;
`unwrap_codex` calls `retag_to_native` once the config restore reports a
`restored`/`cleaned`/`removed` status.
- Tests: `tests/test_provider_codex_threads.py` (retag direction,
threads-table no-op,
missing/corrupt store best-effort) and a wrap/unwrap round-trip
integration test in
`tests/test_cli/test_wrap_codex.py`.
## 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
$ uv run --extra dev pytest tests/test_provider_codex_threads.py tests/test_cli/test_wrap_codex.py tests/test_install/test_providers.py -q
... 88 passed, 2 failed
# The 2 failures are TestInjectAvoidsDuplicateTopLevelKeys::* — pre-existing on a clean
# upstream/main checkout, unrelated to this change: they `import tomllib`, which is
# stdlib only on Python 3.11+, and this environment runs Python 3.10.18.
$ uv run --extra dev ruff check headroom/cli/wrap.py headroom/providers/codex/threads.py \
headroom/providers/codex/install.py tests/test_cli/test_wrap_codex.py tests/test_provider_codex_threads.py
All checks passed!
$ uv run --extra dev mypy headroom/providers/codex/threads.py headroom/providers/codex/install.py
Success: no issues found in 2 source files
```
## Real Behavior Proof
- Environment: macOS, Codex GUI v148 + Codex CLI, Python 3.10.18.
- Exact command / steps: The root cause and fix were confirmed live in
the Headroom
desktop app, which performs the identical SQLite retag. Connecting Codex
to Headroom
hid ~140 native (`openai`) threads from the history menu; running
`UPDATE threads SET model_provider='headroom' WHERE
model_provider='openai'` on the live
store (`~/.codex/sqlite/state_5.sqlite`) made the full menu reappear,
and resuming a
retagged session still routed completions through the active provider.
This Python port
is a 1:1 of that logic, exercised by the unit + wrap/unwrap integration
tests above.
- Observed result: full Codex history menu restored across
enable/disable; third-party
provider rows untouched; the real `~/.codex` stores were snapshotted
before/after the
test run and were not mutated by the tests.
- Not tested: an end-to-end `headroom wrap codex` run against a live
Codex GUI in this CI
environment (no Codex install here); covered instead by the integration
test invoking
the real `wrap`/`unwrap` Click commands against a temp `$HOME`.
## 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
## Screenshots (if applicable)
N/A — behavior is in Codex's own history menu; covered by the proof
above.
## Additional Notes
- Documentation / CHANGELOG: N/A — internal behavior with no user-facing
surface beyond
the restored menu.
- The 2 failing `TestInjectAvoidsDuplicateTopLevelKeys` tests are
pre-existing on
upstream/main and fail only because this environment runs Python 3.10
(no `tomllib`);
they are unrelated to this change.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
c65e321ea2
|
ci: bump the uv group across 1 directory with 5 updates (#1020)
Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) --- <details> <summary>Dependabot commands and options</summary> <br /> You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore <dependency name> major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore <dependency name> minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore <dependency name>` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore <dependency name>` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore <dependency name> <ignore condition>` will remove the ignore condition of the specified dependency and ignore conditions You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/chopratejas/headroom/network/alerts). </details> Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
0932b8bef4
|
feat: Add support for Mistral Vibe CLI (#935)
## Description Add `headroom wrap vibe` / `headroom unwrap vibe` support for Mistral Vibe CLI so Vibe can launch through Headroom's proxy, compression, and observability path. ## Type of Change - [x] New feature (non-breaking change that adds functionality) ## Changes Made - Added `headroom.providers.mistral_vibe` provider runtime helpers. - Added `headroom wrap vibe` command support and matching unwrap handling. - Configured `VIBE_PROVIDERS` so Vibe routes through the Headroom proxy. - Added tests covering launch, custom ports, no-proxy behavior, code-graph/learn-memory flags, verbose mode, invalid-command handling, and provider JSON structure. - Updated `CHANGELOG.md`. ## 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 pytest -v tests/test_cli/test_wrap_vibe.py # 10 passed ``` ## Real Behavior Proof - Environment: Linux, Python 3.13.13, local checkout from the PR branch. - Exact command / steps: Ran the Vibe wrapper tests and manually launched Mistral Vibe through `headroom wrap vibe` with `VIBE_PROVIDERS` pointing at the Headroom proxy. - Observed result: Vibe launched through Headroom's proxy configuration, and the wrapper tests passed. - Not tested: RTK hook support for Vibe. Persistent installs may eventually hold an expired Vibe auth token because Vibe reads its auth token from the environment at startup; opening another port or removing the persistent install is the current workaround. ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review --------- Co-authored-by: Vibe Nuage Agent <vibe@mistral.ai> Co-authored-by: JerrettDavis <mxjerrett@gmail.com> |
||
|
|
e20f16b1a6
|
fix: route v1internal code assist requests to cloudcode-pa.googleapis… (#821)
## Description This PR fixes routing of Google Cloud Code Assist authentication, onboarding, and experiment list endpoints. Specifically, endpoints under `/v1/v1internal:*` (e.g. `/v1/v1internal:fetchAvailableModels`) are now correctly routed to the Cloud Code target (`https://cloudcode-pa.googleapis.com`) and **normalized** to `/v1internal:*` prior to forwarding. This resolves 404/403 errors on the upstream service which does not accept `/v1/v1internal:*` request paths. Closes #821 ## 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 - Modified `headroom/providers/proxy_routes.py` to strip the `v1/` prefix and normalize the path to `/v1internal:*` for Cloud Code routes. - Modified `tests/test_provider_proxy_routes.py` to add assertions verifying route and path normalization. ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [x] Type checking passes (`mypy headroom/providers/proxy_routes.py`) - [x] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text ================================= test session starts ================================= platform linux -- Python 3.14.5, pytest-9.0.3, pluggy-1.6.0 -- /home/alex/projects/github.com/Djabx/headroom/.venv/bin/python3 cachedir: .pytest_cache rootdir: /home/alex/projects/github.com/Djabx/headroom configfile: pyproject.toml plugins: anyio-4.12.1, cov-7.1.0, asyncio-1.4.0, langsmith-0.8.15 asyncio: mode=Mode.AUTO, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function collected 13 items tests/test_provider_proxy_routes.py::test_provider_passthrough_routes_forward_expected_targets PASSED [ 7%] tests/test_provider_proxy_routes.py::test_proxy_route_helpers_prefer_legacy_targets_and_gemini_passthrough PASSED [ 15%] tests/test_provider_proxy_routes.py::test_provider_specific_routes_delegate_to_expected_proxy_handlers PASSED [ 23%] tests/test_provider_proxy_routes.py::test_openai_response_websocket_aliases_delegate_to_openai_ws_handler PASSED [ 30%] tests/test_provider_proxy_routes.py::test_openai_response_subpath_passthrough_returns_502_on_http_failure PASSED [ 38%] tests/test_provider_proxy_routes.py::test_openai_response_subpath_passthrough_uses_openai_target PASSED [ 46%] tests/test_provider_proxy_routes.py::test_openai_response_subpath_aliases_and_chatgpt_auth_use_expected_targets PASSED [ 53%] tests/test_provider_proxy_routes.py::test_gemini_batch_embed_contents_passthrough_uses_gemini_target PASSED [ 61%] tests/test_provider_proxy_routes.py::test_v1_models_fetches_codex_registry_under_chatgpt_auth PASSED [ 69%] tests/test_provider_proxy_routes.py::test_v1_models_falls_back_to_synthetic_list_under_chatgpt_auth PASSED [ 76%] tests/test_provider_proxy_routes.py::test_v1_models_get_single_dynamic_under_chatgpt_auth PASSED [ 84%] tests/test_provider_proxy_routes.py::test_v1_models_still_forwards_under_non_chatgpt_auth PASSED [ 92%] tests/test_provider_proxy_routes.py::test_v1_models_routes_claude_code_gateway_discovery_to_anthropic PASSED [100%] ================================= 13 passed in 0.63s ================================= ``` ## Real Behavior Proof - Environment: Linux, Python 3.14.5 - Exact command / steps: `pytest tests/test_provider_proxy_routes.py` which utilizes `fastapi.testclient.TestClient` to dispatch requests. - Observed result: Both `/v1internal` and `/v1/v1internal` endpoints are correctly routed to the Cloud Code target (`https://cloudcode.test`) and normalize their paths to `/v1internal`, avoiding 404/403 errors on the upstream service. - Not tested: Actual production Cloud Code endpoints (simulated via TestClient/fakes). ## 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 ## Screenshots (if applicable) Add screenshots to help explain your changes. ## Additional Notes <!-- Mention any N/A checklist items, tradeoffs, follow-ups, or maintainer context. --> <!-- headroom-maintainer-template-completion:start --> ## Description This PR prepares `fix: route v1internal code assist requests to cloudcode-pa.googleapis…` for review by documenting the intended change, validation evidence, and remaining merge-readiness context. Linked issues: None declared. ## Type of Change - [x] Bug fix - [ ] New feature - [ ] Documentation - [ ] Refactor - [ ] Tests only ## Changes Made - Commit: fix: route v1internal code assist requests to cloudcode-pa.googleapis… - Touches `headroom/providers/proxy_routes.py` - Touches `tests/test_provider_proxy_routes.py` ## Testing - [x] GitHub checks reviewed - [x] Metadata/template validation - [ ] Local functional testing ### Test Output ```text gh pr view 821 --repo chopratejas/headroom --json statusCheckRollup - PR Governance / template: FAILURE - PR Governance / template: FAILURE - PR Governance / template: FAILURE - PR Governance / template: FAILURE - PR Governance / label: SUCCESS - PR Governance / label: SUCCESS - PR Governance / label: SUCCESS - PR Governance / label: SUCCESS - external / GitGuardian Security Checks: SUCCESS ``` ## Real Behavior Proof - Environment: GitHub PR metadata and checks for `chopratejas/headroom` PR #821. - Exact command / steps: Reviewed PR title, commits, changed files, linked issues, labels, and check rollup; appended this maintainer template completion block without replacing the author's original description. - Observed result: PR body now contains all required governance sections, checked readiness fields, and a non-placeholder validation evidence block. - Not tested: This pass updated PR metadata only; code validation remains represented by the linked GitHub checks and any author-provided evidence above. ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review <!-- headroom-maintainer-template-completion:end --> |
||
|
|
7edb27ab24
|
feat(proxy): compress AWS Bedrock InvokeModel requests via configurable upstream (#720)
## Description
Clients that speak **Bedrock to a local gateway** can't get proxy-level
compression. Claude Code launched with `CLAUDE_CODE_USE_BEDROCK=1` (and
any AWS SDK pointed at a custom endpoint) POSTs
`/model/{id}/invoke[-with-response-stream]` to
`AWS_ENDPOINT_URL_BEDROCK_RUNTIME`, never `/v1/messages`. Those requests
fell through the catch-all and were forwarded **verbatim — no
compression**.
`--backend bedrock` is the opposite direction: it accepts Anthropic
input and re-signs to AWS. It can't accept Bedrock-format input or
forward to a custom upstream. So the "client speaks Bedrock → local
re-signing gateway → AWS" topology (internal gateways, LiteLLM,
LocalStack; see #510) got nothing.
This adds a Bedrock InvokeModel passthrough that compresses the request
body with the **same** `anthropic_pipeline` used for `/v1/messages` —
the Bedrock InvokeModel body for Anthropic models *is* the Anthropic
Messages shape (`{anthropic_version, system, messages, max_tokens, …}`,
model in the URL), so there's no translation and no new compression
logic. The routes register **only** when `--bedrock-api-url` is set, so
default behavior is completely unchanged.
**Limitation (important):** rewriting the body invalidates the caller's
**SigV4** signature (it covers a hash of the body). Point
`--bedrock-api-url` at a gateway that re-signs or doesn't verify the
inbound signature (an internal gateway, LiteLLM, LocalStack, a corporate
Bedrock proxy) — **never raw AWS**, which would 403. For direct-to-AWS
compression, use `--backend bedrock` (which re-signs). The two are
complementary. This is documented in the flag help, the handler
docstring, the proxy docs, and the CHANGELOG.
Closes #734. Refs #510 (the Bedrock slice of the provider-agnostic
umbrella).
## Type of Change
- [ ] 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)
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)
## Changes Made
- New `--bedrock-api-url` flag (env: `BEDROCK_TARGET_API_URL`). When
set, registers `POST /model/{id}/invoke` and `POST
/model/{id}/invoke-with-response-stream`.
- `BedrockHandlerMixin` compresses the request body via the existing
`anthropic_pipeline`, then forwards to the configured upstream,
preserving path/query.
- Responses forwarded byte-faithfully (non-streaming JSON and the
streaming AWS event-stream alike — neither is parsed or mutated, since
all compression is request-side).
- `{model_id:path}` captures inference-profile ids with
dots/colons/slashes (e.g.
`us.anthropic.claude-sonnet-4-5-20250929-v1:0`).
- Fail-open: a malformed body or compression error forwards verbatim
rather than erroring.
- Routes register only when the flag is set — default behavior
unchanged.
- Files: `headroom/proxy/handlers/bedrock.py` (new —
`BedrockHandlerMixin`); `headroom/providers/proxy_routes.py` (gated
route registration); `headroom/cli/proxy.py`,
`headroom/proxy/server.py`, `headroom/proxy/models.py` (flag + config
wiring); `docs/content/docs/proxy.mdx`, `CHANGELOG.md` (docs).
## Testing
- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [ ] Type checking passes (`mypy headroom`)
- [x] New tests added for new functionality
- [x] Manual testing performed
### Test Output
```text
$ pytest tests/test_proxy/test_bedrock_passthrough.py -q
.............. [100%]
14 passed in 12.46s
```
`tests/test_proxy/test_bedrock_passthrough.py` (14 tests) covers: route
gating (absent unless configured), body compression, non-message fields
preserved, inference-profile id capture + re-encoding, byte-faithful
streaming, fail-open on malformed body and on pipeline exceptions,
bypass when `optimize=False` and via the `x-headroom-bypass` header,
upstream connect failure surfacing as a 502, the content-length
regression, outcome recorded with `provider="bedrock"`, and
`BEDROCK_TARGET_API_URL` env wiring. `ruff check`/`format` clean.
## Real Behavior Proof
- Environment: macOS, Python 3.12; forked proxy on `:8788` with
`--bedrock-api-url` pointed at a local re-signing Bedrock gateway;
provider Anthropic Claude on Bedrock.
- Exact command / steps: `headroom proxy --port 8788 --bedrock-api-url
http://127.0.0.1:<gateway>`, then `curl -X POST
http://127.0.0.1:8788/model/claude-haiku-4-5/invoke --data
@bedrock_invoke.json` (a ~52k-token conversation with a large assistant
turn).
- Observed result: valid Claude response returned and the gateway
received the compressed body — proxy `/stats` reports `52,095 → 3,979
tokens` (92.4%, 48,116 removed), and the gateway's reported
`input_tokens: 3709` confirms the compressed body reached the model.
- Not tested: raw direct-to-AWS (out of scope by design — SigV4; use
`--backend bedrock`); non-Anthropic Bedrock model bodies (e.g.
Titan/Llama) — only the Anthropic Messages-shaped invoke body is
handled.
<details><summary>Proxy <code>/stats</code> output + content-length bug
note</summary>
```json
"compression": {
"requests_compressed": 1,
"avg_compression_pct": 92.4,
"best_detail": "52,095 → 3,979 tokens",
"total_tokens_removed": 48116
}
```
The first iteration of this proof surfaced a real bug — a shrunk body
still carried the inbound `Content-Length`, so httpx raised `Too little
data for declared Content-Length`. Fixed by dropping
`content-length`/`content-encoding` on the rewritten path so httpx
recomputes them; covered by a regression test.
</details>
## 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
- [x] I have updated the CHANGELOG.md if applicable
## Additional Notes
`mypy headroom` is left unchecked above — type checking runs in the CI
matrix rather than locally on my side; the new code carries type hints
on all public functions. Design spec / feature request: #734.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
e36fccd8cf
|
refactor: DRY cache logic, add thread safety, fix Bash exclusion (#704)
## Description Four targeted improvements to ContentRouter and configuration, refactoring ~120 lines of duplicated cache logic into a shared helper and fixing several correctness issues. ### 1. DRY: Extract `_compress_block_content` helper The two-tier cache lookup + compression logic was duplicated ~60 lines per path (tool_result blocks and text blocks in `_process_content_blocks`). Extracted into a single, shared helper method. Net reduction of ~80 lines; no behavioural change. ### 2. Thread-safe `CompressionCache` `CompressionCache` is read/modified from `ThreadPoolExecutor` workers during parallel compression in `apply()`. Added a `threading.Lock` guarding all read-modify-write operations so concurrent cache misses for the same content do not produce duplicate compression work and metrics counters stay consistent. ### 3. Remove duplicate Kompress fallback for SmartCrusher The SMART_CRUSHER strategy block had an inline Kompress fallback that ran when SmartCrusher produced no savings. The unified post-strategy fallback block already covers the same case — the inline copy was a duplicate Kompress invocation. Removed it; the post-strategy handler now owns all fallback decisions for both SMART_CRUSHER and CODE_AWARE. Also added a guard preventing duplicate Kompress when CODE_AWARE's inline fallback fires alongside the unified block. ### 4. Fix Bash exclusion contradiction in `DEFAULT_EXCLUDE_TOOLS` The docstring on `DEFAULT_EXCLUDE_TOOLS` explicitly states "Bash is NOT excluded — its outputs (build logs, test output) are ideal compression targets." But both "Bash" and "bash" were still in the frozenset. Removed them so code matches the documented intent. 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 - [x] Code refactoring (no functional changes) ## Changes Made - `headroom/config.py`: Remove Bash/bash from `DEFAULT_EXCLUDE_TOOLS` - `headroom/transforms/content_router.py`: Extract `_compress_block_content` helper; unified post-strategy fallback block; threading.Lock on CompressionCache; CODE_AWARE duplicate guard - `headroom/client.py`: Replace silent `except Exception: pass` with `logger.debug(..., exc_info=True)` - `tests/test_compression_cache.py`: Add 2 concurrency regression tests - `tests/test_transforms/test_content_router.py`: Add 14 tests covering Bash exclusion, SmartCrusher fallback chain, and `_compress_block_content` shared path ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [x] Formatting passes (`ruff format --check .`) - [x] New tests added for new functionality - [ ] Manual testing performed ### Test Output ```text # 14 new tests added across 3 test classes: # TestExcludeTools: 3 tests (Bash not in DEFAULT_EXCLUDE_TOOLS) # TestSmartCrusherFallback: 4 tests (fallback chain, no duplicate Kompress, JSON direct hit, CODE_AWARE path) # TestCompressBlockContent: 5 tests (skip set, result cache, ratio gating, route counts, transforms tracking) # TestCompressionCache: 2 tests (concurrent hits/misses consistency, stable hash ops no race) # Local run (43 tests pass): $ pytest tests/test_compression_cache.py tests/test_transforms/test_content_router.py -v ...43 passed... # ruff check: $ ruff check headroom/client.py headroom/config.py headroom/transforms/content_router.py tests/test_compression_cache.py tests/test_transforms/test_content_router.py All checks passed! # ruff format: $ ruff format --check headroom/client.py headroom/config.py headroom/transforms/content_router.py tests/test_compression_cache.py tests/test_transforms/test_content_router.py 5 files already formatted ``` ## Real Behavior Proof - Environment: Python 3.12, Linux (CI), headroom with headroom._core Rust extension compiled - Exact command / steps: CI run https://github.com/chopratejas/headroom/actions/runs/27326150021 — 13/16 jobs pass; 2 failures were lint+commitlint (both fixed in subsequent commits); 1 failure is pre-existing test(4) which monkeypatches time.time() but the CompressionCache uses time.monotonic() — unrelated to our changes - Observed result: All 14 new tests pass in CI; SmartCrusher fallback chain deterministically shows [smart_crusher, kompress] or [smart_crusher, kompress, log] when SmartCrusher produces no savings, with no duplicate entries - Not tested: fork-PR CI path where GitHub secrets are not available; local Windows environment where headroom._core Rust extension is not built ## 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 The pre-existing CI failure in `test (4)` is `test_compression_cache_handles_hits_skips_evictions_and_clear` in `tests/test_transforms_content_router.py`. It monkeypatches `time.time()` but the `CompressionCache` (content_router-local, line 191) uses `time.monotonic()` for TTL — the monkeypatched clock never advances, and `is_skipped()` always returns True. This failure exists on `main` and is unrelated to our changes (we only modified the other CompressionCache in `headroom/cache/compression_cache.py`). --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
64ca95361a
|
fix: --disable-kompress should not override fallback_strategy to PASSTHROUGH (#1046)
## Description `--disable-kompress` correctly disabled the ML model via `enable_kompress = False`, but it also forced `router_config.fallback_strategy = CompressionStrategy.PASSTHROUGH`. That override suppressed ContentRouter's rule-based passes — including the `exclude_tools` gate — for content that falls through to the fallback, so `HEADROOM_EXCLUDE_TOOLS` had no effect when `--disable-kompress` was set. Removing the override leaves `fallback_strategy` at its default (`KOMPRESS`); ContentRouter keeps running its rule-based passes and only Kompress inference is disabled. Closes #955 ## 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/proxy/server.py`: removed the `router_config.fallback_strategy = CompressionStrategy.PASSTHROUGH` line inside the `if config.disable_kompress:` block; `enable_kompress = False` is kept. - `headroom/proxy/server.py`: dropped the now-unused `CompressionStrategy` import (it was only referenced by the removed line). - `tests/test_proxy_disable_kompress.py`: updated the assertion to expect `fallback_strategy == CompressionStrategy.KOMPRESS` (the default), matching the corrected behaviour. ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [ ] Type checking passes (`mypy headroom`) - [ ] New tests added for new functionality - [ ] Manual testing performed ### Test Output ```text $ pytest tests/test_proxy_disable_kompress.py -v ============================= test session starts ============================== platform darwin -- Python 3.13.7, pytest-9.1.0, pluggy-1.6.0 rootdir: /.../headroom configfile: pyproject.toml plugins: anyio-4.14.0, asyncio-1.4.0 collected 2 items tests/test_proxy_disable_kompress.py::test_disable_kompress_config_keeps_optimization_but_disables_ml_fallback PASSED [ 50%] tests/test_proxy_disable_kompress.py::test_disable_kompress_defaults_to_existing_kompress_behavior PASSED [100%] ============================== 2 passed in 1.37s ============================== $ ruff check headroom/proxy/server.py tests/test_proxy_disable_kompress.py All checks passed! ``` ## Real Behavior Proof - Environment: local clone, Python 3.13.7 venv, headroom core deps + fastapi/uvicorn/httpx. - Exact command / steps: built the proxy router via `create_app(ProxyConfig(optimize=True, ...))` with `disable_kompress` set and inspected the resulting `ContentRouter` config; ran `pytest tests/test_proxy_disable_kompress.py -v` and `ruff check` on the changed files. - Observed result: with `--disable-kompress`, `enable_kompress` is `False` and `fallback_strategy` is `KOMPRESS` (the default) instead of `PASSTHROUGH`; ContentRouter stays in the pipeline. Both config tests pass and lint is clean. - Not tested: full live-proxy `/stats` run against an LLM backend. The issue reporter observed `router_content_router_activations` 0→22 and exclude-tool hits 0→10 after this change (see #955). ## 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 - [ ] 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 This removes the override line plus its now-unused import, and updates the existing test that asserted the old behaviour; no new test was added because the corrected behaviour is covered by that existing test. The live `/stats` reproduction is described in #955. |
||
|
|
0ddd4ed9e9
|
fix(learn): scan subagent and workflow transcripts (#1045)
## Description `headroom learn` only scanned top-level main Claude Code sessions (`<project>/<uuid>.jsonl`). Nested subagent and workflow transcripts under `<project>/<uuid>/subagents/**` were not opened, which hid a large amount of tool-call failure and token-spend activity from failure mining and downstream analysis. This change makes the Claude scanner descend into nested transcripts by default and tag each `SessionData` with its source. `--main-only` restores the previous top-level-only scan scope. ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) ## Changes Made - Updated `ClaudeCodePlugin.scan_project` to discover nested subagent and workflow transcripts by default. - Added source tagging for `main`, `subagent`, and `workflow` sessions. - Added `--main-only` and `include_subagents` plumbing so callers can opt back into top-level-only scanning. - Added the `include_subagents` scanner parameter to Codex/Gemini as a documented no-op because those scanners use flat session layouts. - Added regression tests for nested discovery, source tagging, parallel scanning, and CLI flag threading. ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check`) - [x] Type checking passes (`mypy headroom`) - [x] New tests added for new functionality ### Test Output ```text Full learn + CLI suite: # 186 passed, 2 skipped GitHub Actions CI for this PR: # build, build-wheel, lint, tests, e2e, CodeQL, and native wrapper checks passed ``` ## Real Behavior Proof - Environment: local Claude Code corpus with nested subagent/workflow transcripts. - Exact command / steps: Scanned the corpus with the previous top-level-only behavior and then with nested transcript discovery enabled. - Observed result: The scanner saw 24 sessions before and 306 sessions after descending into nested transcripts. - Not tested: Codex/Gemini nested transcript discovery, because those providers currently use flat session layouts and treat `include_subagents` as a no-op. ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review --------- Co-authored-by: JerrettDavis <mxjerrett@gmail.com> |
||
|
|
8662a82e8a
|
Fix Codex ChatGPT /v1/models compatibility metadata (#1048)
## Description Fixes Codex ChatGPT/OAuth `/v1/models` metadata compatibility while keeping Headroom's existing OpenAI-compatible response shape. Headroom's ChatGPT/OAuth model-list route already returned: - `object: "list"` - `data[]` Newer Codex clients also inspect a top-level `models[]` registry metadata array. Without that shape, completions can still work, but clients may emit non-fatal model metadata decode or missing-field warnings before the follow-up `/v1/responses` call. This PR keeps `object`/`data[]` unchanged and adds a Codex-compatible `models[]` array. Upstream registry metadata is preserved where available, and only missing fields are filled with defaults. Closes: N/A ## Type of Change - [x] Bug fix (non-breaking change fixes issue) - [ ] New feature (non-breaking change adds functionality) - [ ] Breaking change (fix or feature would cause existing functionality change) - [ ] Documentation update - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - Add Codex registry metadata generation for the ChatGPT/OAuth `/v1/models` response. - Preserve dynamic upstream registry entries instead of reducing them to slug-only IDs. - Add fallback metadata for known Codex models if upstream registry data is unavailable. - Fill required/default Codex fields when absent, including: - `display_name` - `default_reasoning_level` - `supported_reasoning_levels` - `context_window` - tool/runtime capability flags - Keep the existing OpenAI-compatible `data[]` response shape. - Add tests that assert both OpenAI-compatible `data[]` and Codex-compatible `models[]` shapes. Changed files: - `headroom/providers/proxy_routes.py` - `tests/test_provider_proxy_routes.py` - `tests/test_proxy_codex_route_aliases.py` ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [ ] Type checking passes (`mypy headroom`) - [x] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text ruff check headroom/providers/proxy_routes.py tests/test_provider_proxy_routes.py tests/test_proxy_codex_route_aliases.py # pass pytest tests/test_provider_proxy_routes.py::test_v1_models_fetches_codex_registry_under_chatgpt_auth # pass pytest tests/test_proxy_codex_route_aliases.py # pass pytest tests/test_provider_proxy_routes.py # pass ``` ## Real Behavior Proof - Environment: isolated local Headroom proxy using the patched source. - Exact command / steps: - call `/v1/models` - run one small Codex `/v1/responses` request through the proxy - compare Headroom `/stats` - check logs for Codex model metadata decode or missing-field warnings - Observed result: - `/v1/models` succeeded - `/v1/responses` succeeded - `requests.failed` stayed flat - provider stats and proxy compression accounting increased - no Codex model metadata decode or missing-field warnings observed - Not tested: - full repository `mypy headroom` pass was not run for this submission ## Review Readiness - [x] I have performed a self-review before requesting human 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 - [ ] 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 Checklist items left unchecked are intentionally not applicable or not run for this focused compatibility PR: - no new comments were needed in the implementation - no documentation or changelog update is included for this compatibility fix to an existing route - full-suite `mypy headroom` was not run in the submission pass Co-authored-by: felixboenkost-droid <258905464+felixboenkost-droid@users.noreply.github.com> |
||
|
|
1cfb0b1133
|
test: add native install and wrap e2e workflows (#837)
## Description
Adds native GitHub Actions smoke coverage for `headroom install` and
`headroom wrap ... --prepare-only`, replacing the closed #257 with a
focused branch based on current `main`.
Closes #257
## Type of Change
- [ ] 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
- [x] Code refactoring (no functional changes)
## Changes Made
- Adds native install smoke coverage.
- Adds native wrap prepare-only smoke coverage.
- Keeps the matrix aligned with `init-native-e2e.yml` on Linux and macOS
while Windows native wheel builds are blocked upstream.
- Merged current upstream/main cleanly; after update the branch has no
remaining diff against upstream/main.
## Testing
- [ ] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [ ] Type checking passes (`mypy headroom`)
- [ ] New tests added for new functionality
- [x] Manual testing performed
### Test Output
```text
git diff --check
uv run --frozen ruff check headroom tests scripts --output-format concise
npx --yes --package=@commitlint/cli --package=@commitlint/config-conventional -- commitlint --from upstream/main --to HEAD --config .commitlintrc.json
cargo check -p headroom-core
All local common gates passed. Branch has no remaining diff against upstream/main after the merge update.
```
## Real Behavior Proof
- Environment: Windows 11, Python 3.13.3 via `uv`, Rust/Cargo 1.95.0,
isolated worktree `C:\git\headroom-jd-prs-native-e2e-expansion`.
- Exact command / steps: merged `upstream/main`, ran common local gates,
and pushed `
|
||
|
|
e67ee2af65
|
feat: add Copilot BYOK provider wrapper utilities and CLI support (#1041)
## Description Fix `--model auto` causing `400 The requested model is not supported` errors when using Copilot BYOK mode. `auto` is a Copilot-internal virtual routing token that external providers (Anthropic, OpenAI) do not recognise as a valid model name. In subscription/OAuth mode the wrapper now strips `--model auto` before launching Copilot so its own native auto-selection takes effect. In BYOK mode `auto` is treated as unconfigured and a clear, actionable error message is shown. Closes #972 ## 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/providers/copilot/wrap.py`: added `is_auto_model()` and `strip_auto_model_args()` helpers; updated `model_configured()` to treat `auto` as unconfigured for BYOK - `headroom/providers/copilot/__init__.py`: exported both new helpers via `__all__` - `headroom/cli/wrap.py`: strips `--model auto` in subscription mode before launch; shows specific actionable error in BYOK mode - `tests/test_provider_copilot_wrap.py`: 17 new parametrized test cases for `is_auto_model`, `strip_auto_model_args`, and updated `model_configured` ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [ ] Type checking passes (`mypy headroom`) - [x] New tests added for new functionality - [ ] Manual testing performed ### Test Output ```text $ uv run pytest tests/test_provider_copilot_wrap.py -v platform win32 -- Python 3.14.6, pytest-9.0.3, pluggy-1.6.0 collected 34 items tests/test_provider_copilot_wrap.py::test_is_auto_model[auto-True] PASSED tests/test_provider_copilot_wrap.py::test_is_auto_model[Auto-True] PASSED tests/test_provider_copilot_wrap.py::test_is_auto_model[AUTO-True] PASSED tests/test_provider_copilot_wrap.py::test_strip_auto_model_args[args0-expected0] PASSED tests/test_provider_copilot_wrap.py::test_strip_auto_model_args[args1-expected1] PASSED tests/test_provider_copilot_wrap.py::test_strip_auto_model_args[args2-expected2] PASSED tests/test_provider_copilot_wrap.py::test_model_configured_detects_env_and_cli_variants PASSED ============================= 34 passed in 0.46s ============================== $ ruff check headroom/providers/copilot/wrap.py headroom/cli/wrap.py All checks passed! ``` ## Real Behavior Proof - Environment: Windows 11, Python 3.14.6, headroom-ai 0.25.0 editable install from branch fix-automode-issue - Exact command / steps: ran uv run pytest tests/test_provider_copilot_wrap.py -v and ruff check on all four changed files; reviewed CLI code path for both subscription and BYOK modes - Observed result: 34 passed, ruff All checks passed; --model auto is stripped silently in subscription mode and rejected with a specific actionable error in BYOK mode - Not tested: live end-to-end Copilot CLI session, macOS/Linux keychain auth, Docker/CI token-injection paths ## 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 mypy is not installed in the local venv so type checking was skipped; the code uses standard type hints and passes ruff checks cleanly. --------- Co-authored-by: JerrettDavis <mxjerrett@gmail.com> |
||
|
|
74dff94fb8
|
fix(ci): make PR governance advisory (#1047)
## Description Make the PR Governance workflow advisory for incomplete pull request bodies. The workflow still validates the template, writes the run summary, comments on the PR, and syncs governance labels, but it no longer marks the check red for expected author follow-up. ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) ## Changes Made - Replaced the failing incomplete-template step with a reporting step that exits successfully. - Added a regression test that guards against reintroducing the hard failure path. ## Testing - [x] Unit tests pass (`pytest`) - [x] Manual testing performed ### Test Output ```text pytest scripts/tests/test_pr_governance.py scripts/tests/test_pr_health_labels.py scripts/tests/test_pr_health_workflow.py -q # 7 passed act pull_request_target -W .github/workflows/pr-health.yml -e .github/act/pr-governance-invalid.json -n # Job succeeded act pull_request_target -W .github/workflows/pr-health.yml -e .github/act/pr-governance-valid.json -n # Job succeeded ``` ## Real Behavior Proof - Environment: Windows 11, Python 3.13.13, act 0.2.87, Docker Desktop via npipe. - Exact command / steps: Ran the focused governance/label tests and `act` dry-runs for the valid and invalid PR governance payloads. - Observed result: Tests passed, the invalid payload's reporting step completed successfully, and both PR Governance dry-runs ended with job success. - Not tested: Full non-dry-run `act` execution against GitHub API side-effect steps, to avoid mutating real labels/comments from a local run. ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review |
||
|
|
7dbbb4077e
|
fix(proxy): keep codex image-generation WS turns alive through the relay (#1000)
## Description Image generation through the proxy fails. Driving Codex (`/v1/responses` over WebSocket) through Headroom, an image-generation turn never returns an image — the client retries (`Reconnecting… n/5`) and gives up, while the same prompt works when Codex talks to ChatGPT directly. Root cause: two independent defects on the upstream `websockets.connect()`, both specific to how image generation behaves on the wire: 1. **Pong deadline kills the silent render.** An image turn emits a single `response.image_generation_call.generating` event and then goes silent for 20–60s while the model renders (no data frames). The hard-coded `ping_timeout=20` treats that healthy-but-quiet connection as dead and tears it down as `upstream_error` mid-render, before the image is ready. 2. **1 MiB frame cap drops the image.** The finished image comes back inline as a single base64 frame that exceeds the `websockets` default `max_size=2**20` (1 MiB), raising `PayloadTooBig` exactly as the image lands. They compound: with only ping fixed, the session survives the silent phase (observed ~20s → ~54s) but then dies on the oversized image frame. Normal text/tool turns stream tokens continuously and stay well under 1 MiB, so neither defect affects them — which is why this only ever bit image generation. Closes: N/A (no tracking 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/proxy/handlers/openai.py`: on the upstream `/v1/responses` connect, set `ping_timeout=None` (keep `ping_interval=20` for NAT keepalive) so a long silent render is not torn down on a missing pong, and `max_size=None` so the inline base64 image frame is accepted instead of raising `PayloadTooBig`. - `tests/test_openai_codex_ws_lifecycle.py`: add `test_ws_upstream_connect_allows_large_frames_and_no_pong_deadline`, which captures the upstream connect kwargs and pins `ping_timeout is None` / `max_size is None`. ## Testing <!-- Check what you actually ran, then paste the real command output below. --> - [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 . All checks passed! $ mypy headroom --ignore-missing-imports Success: no issues found in 358 source files $ pytest tests/test_openai_codex_ws_lifecycle.py -q collected 15 items tests/test_openai_codex_ws_lifecycle.py .............. [100%] ============================== 15 passed in 0.72s ============================== $ pytest tests/test_openai_codex_ws_lifecycle.py -k large_frames_and_no_pong -q collected 15 items / 14 deselected / 1 selected tests/test_openai_codex_ws_lifecycle.py . [100%] ======================= 1 passed, 14 deselected in 0.35s ======================= ``` End-to-end (managed Codex image generation through the running proxy): ```text # BEFORE fix: fails at ~20s (unpatched) / ~54s (ping-only) # WS /v1/responses completed (cause=upstream_error, # last_upstream_type=response.image_generation_call.generating) # -> client "Reconnecting… n/5", no image produced # AFTER fix: [codex] Image ready; stopping the turn. Saved image: /tmp/headroom-imagegen-test.png $ file /tmp/headroom-imagegen-test.png PNG image data, 1254 x 1254, 8-bit/color RGB, non-interlaced (908 KB) # proxy session count +1 -> the turn DID traverse the proxy and completed. ``` ## Real Behavior Proof - Environment: macOS, headroom 0.23.0 running as the Codex `model_provider` (proxy on `127.0.0.1:8787`), Codex CLI 0.139.0 driving a managed `/v1/responses` image-generation turn through the proxy. - Exact command / steps: trigger a Codex image-generation turn (gpt-image-2) with the proxy in front; observe the upstream `/v1/responses` WS session in `proxy.log` and whether a PNG is returned. - Observed result: before the change the session dies with `upstream_error` while `last_upstream_type=response.image_generation_call.generating` and no image is produced; after the change a valid 1254×1254 PNG is returned and the session traverses the proxy normally. - Not tested: the full `pytest` suite was not run locally — this machine has no Rust toolchain to rebuild the matching `_core` extension, so the complete suite (incl. the pyo3 tests) is left to CI. The affected `test_openai_codex_ws_lifecycle.py` module was run against the installed extension and passes 15/15; `ruff check .` and `mypy headroom` were run in full and pass. ## 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 ## Screenshots (if applicable) N/A. ## Additional Notes - `ruff check .` (whole repo) and `mypy headroom --ignore-missing-imports` (358 source files) were run locally and pass. The only `pytest` not run locally is the full suite, because the Rust `_core` cannot be rebuilt here without a toolchain; the directly affected lifecycle module passes 15/15 and CI runs the rest. - Documentation / CHANGELOG left unchecked — this is a focused two-line behavioral fix on the upstream WS connect; happy to add a CHANGELOG entry if preferred. - `ping_timeout=None` keeps `ping_interval` for NAT keepalive; if you'd rather bound it, a generous finite value (e.g. 300s) would also fix the render case — happy to switch. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
0dc2e1cb3f
|
feat(bedrock): cross-region + Converse compression; bundle proxy binary in images (#999)
## Description The native Bedrock path (Phase D) compresses + signs Anthropic-on-Bedrock requests, but two real-world cases slipped through, and the native binary that powers it was never shipped. This PR closes those gaps as a focused set of give-backs. Aligns with the Rust migration plan (see below). ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) - [x] New feature (non-breaking change that adds functionality) - [ ] Breaking change - [ ] Documentation update - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - **Cross-region inference-profile detection** via a new `bedrock::vendor` module (`canonical_vendor()`), following the design proposed in #953: strip a known geo prefix (`eu.`/`us.`/`apac.`/`global.`) then match the canonical vendor. Geo-prefixed Anthropic profiles (`eu.anthropic.…`) now get live-zone compression instead of being silently skipped; geo-prefixed non-Anthropic vendors stay correctly excluded. - **Converse-body compression (two parts)**: 1. `run_anthropic_compression` no longer bails to passthrough when the body lacks an InvokeModel `anthropic_version` envelope; envelope re-emit stays gated on successful parse. 2. The **live-zone dispatcher now recognizes Bedrock Converse content blocks**. Converse blocks carry no `type` discriminator (the variant is the key: `{"text": …}` vs Anthropic's `{"type":"text","text":…}`), so real Converse user-message text was still passing through uncompressed. A typeless block whose `text` is a JSON string now routes through the same surgical text path. Anthropic blocks always carry `type`, so the Anthropic path is byte-for-byte unchanged; non-text Converse blocks (`{"image":…}`, `{"toolUse":…}`) stay unrecognized and no-op. - **Correct `/converse` upstream routing**: the non-streaming handler resolved the upstream action from a hard-coded `"invoke"`, so `/converse` requests were forwarded to Bedrock's `/invoke` endpoint. It now resolves the action from the inbound path (`extract_invoke_action`), mirroring the streaming handler's `extract_streaming_action`. SigV4 signs the same URL it forwards, so the signature stays consistent. - **`aws-config` `sso` feature**: SSO profiles now resolve through the default credential chain for SigV4 — the credential chain in `docs/bedrock.md` already promised SSO; this makes the code match. - **Ship the `headroom-proxy` binary in published images** (`Dockerfile`): built in the builder stage (`--locked`, with the cargo registry cache mounted at `CARGO_HOME`) and copied into both the debian and distroless runtime images. - **Docs** (`docs/bedrock.md`): document cross-region inference profiles and a "Running the proxy" section. AWS credentials mount at `/home/nonroot/.aws` (the default nonroot image home) where the SDK looks for `~/.aws`, with a note on the root-image alternative. ## Related issues - Closes #976 — ship the `headroom-proxy` binary in published images (this PR implements the exact fix proposed there). - Addresses the **cross-region inference-profile** half of #953 via its proposed `canonical_vendor()` design. Non-Anthropic vendor compression parity (Nova/GLM/MiniMax/ Kimi) is the natural follow-up — `bedrock::vendor` is the shared resolver it can build on. - Extends the native Bedrock InvokeModel compression requested in #734 (the Bedrock slice of #510) to cross-region profiles and Converse bodies. - Partially enables #181 (native, Python-free packaging): the native binary now ships in the images, though full Python-free distribution remains out of scope. ## Alignment with the Rust migration plan Per `docs/spec/022-rust-migration.md`, the migration is **proxy-first**: `headroom-proxy` is the deployable Rust artifact, native routes replace Python passthroughs one at a time (Stage 4 = provider expansion, Bedrock included), and the binary is meant to be "built, tested, and **released together with the Python package**." Two ways this PR advances that: - The binary-in-images change makes the codebase do what the spec already states (ship the artifact) — closing the gap that forced downstreams to build from source. - Hardening the native Bedrock route (cross-region, Converse routing + body compression) is exactly the Stage-4 provider-expansion work, keeping the native path at parity with real traffic so it can be the default rather than a passthrough. ## Testing - [x] Unit tests pass (`cargo test -p headroom-core -p headroom-proxy` — full suites, 0 failures) - [x] Linting passes (`cargo clippy -p headroom-core -p headroom-proxy --all-targets -- -D warnings`) - [x] Formatting passes (`cargo fmt -- --check`) - [x] New tests added — `bedrock::vendor` (foundation + inference-profile matching), `extract_invoke_action` + converse upstream URL, and live-zone Converse text-block routing (`block_has_string_text_field`, converse-vs-anthropic dispatch equivalence). - [x] Manual testing performed ### Test Output ```text $ cargo test -p headroom-core -p headroom-proxy # all suites: ok, 0 failed $ cargo clippy -p headroom-core -p headroom-proxy --all-targets -- -D warnings # Finished, no warnings $ cargo fmt -- --check # clean # image validation (local, proxy/code extras): $ docker build --target runtime ... # debian: /usr/local/bin/headroom-proxy, --help OK $ docker build --target runtime-slim ... # distroless: binary links + --help OK ``` ## Real Behavior Proof - Environment: native Bedrock proxy against `bedrock-runtime.eu-west-2`, SSO profile, model `eu.anthropic.claude-haiku-4-5-20251001-v1:0`. - Exact command / steps: POST a large multi-turn Converse body to `/model/eu.anthropic.claude-haiku-4-5-20251001-v1:0/converse`; separately build the `runtime` + `runtime-slim` targets and run `/usr/local/bin/headroom-proxy --help`. - Observed result: before — `bedrock_compression_skipped` (geo-prefixed id not recognized), forwarded uncompressed to the wrong `/invoke` upstream; after — geo-prefixed id recognized, `/converse` forwarded to the `/converse` upstream, live-zone dispatcher compresses the Converse user-message text, measurable token savings. Images contain a runnable `headroom-proxy` in both variants. - Not tested: non-Anthropic vendor compression parity (#953 follow-up); Converse `toolResult` nested-text compression (follow-up — only top-level Converse text blocks compress today). ## 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 - [x] New and existing unit tests pass locally with my changes - [x] I have updated the CHANGELOG.md if applicable ## Additional Notes - An earlier revision flipped the EventStream `Accept` default (`*/*`/absent → passthrough); **dropped** — `*/*` is what most clients (incl. reqwest and the proxy's own metrics tests) send while expecting SSE, so forcing passthrough breaks the standard SSE path. - The binary build adds the native-proxy compile to the image build; happy to gate it behind a build arg if maintainers prefer it opt-in. - Addressed a Copilot review round: corrected the `/converse` upstream routing, the stale `run_anthropic_compression` comment, the Dockerfile cargo cache mount + `--locked`, and the nonroot AWS-credentials docs example. |
||
|
|
0d4571f72f
|
docs: fix broken macos-deployment.md link in launchagent example (#985)
## Description The macOS LaunchAgent example README links to `../../../docs/macos-deployment.md`, but that file does not exist — the guide lives at `wiki/macos-deployment.md`. This fixes the broken link (path and text) so "complete documentation" resolves. 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) - [x] Documentation update - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - `examples/deployment/macos-launchagent/README.md`: link target `../../../docs/macos-deployment.md` → `../../../wiki/macos-deployment.md`, and the link text `docs/macos-deployment.md` → `wiki/macos-deployment.md`. ## Testing <!-- Check what you actually ran, then paste the real command output below. --> - [ ] Unit tests pass (`pytest`) - [ ] Linting passes (`ruff check .`) - [ ] Type checking passes (`mypy headroom`) - [ ] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text # The old target does not exist; the real guide is under wiki/: $ git ls-files '*macos-deployment.md' wiki/macos-deployment.md $ ls docs/content/docs | grep -i macos # nothing — no docs/macos-deployment.md $ test -f examples/deployment/macos-launchagent/../../../wiki/macos-deployment.md && echo "new link resolves" new link resolves # This was the only stale reference to docs/macos-deployment.md in the repo: $ grep -rn 'docs/macos-deployment' --include='*.md' --include='*.mdx' . (only the line fixed by this PR, now pointing at wiki/) ``` ## Real Behavior Proof - Environment: local clone at `origin/main`; documentation-only change. - Exact command / steps: ran a relative-link checker across all Markdown/MDX, which flagged `examples/deployment/macos-launchagent/README.md:168` as the only broken internal link; confirmed the guide is at `wiki/macos-deployment.md`; repointed the link there. - Observed result: the new relative path `../../../wiki/macos-deployment.md` resolves to the existing macOS Deployment Guide (which itself documents this exact LaunchAgent setup). - Not tested: N/A — single-line Markdown link fix; no code, build, or runtime behavior involved. ## 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 - [ ] 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 - [ ] 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 Documentation-only change, so `pytest`/`ruff`/`mypy` over the `headroom` package are N/A — the diff contains no Python source. The target guide (`wiki/macos-deployment.md`) covers the same LaunchAgent deployment this example sets up, so it is the correct destination for "complete documentation". |
||
|
|
c2e52fe743
|
feat(policy): batch deep edits through one cache-bust (#856 P3a) (#1015)
## Description #856 P3a (umbrella #904), stacked on the now-merged P2 (#905) and P2b (#944). A net-cost mutation at depth K already busts the provider's cached suffix after K. Every *later* candidate at a deeper slot therefore rides that same cache invalidation for free — mutating it adds no incremental cache-bust cost. Today the P2 break-even gate re-charges each candidate the full invalidated suffix S independently, so a batch of legitimate deep edits is under-admitted: only the first pays for the bust, yet each is billed as if it paid alone. This adds a batch-reclaim floor to the net-cost gate so that once one net-positive deep edit is admitted at slot K, candidates at slot > K are admitted on the write/read economics alone (S charged as 0). Flag-gated under `HEADROOM_NET_COST_POLICY` (the same flag as P2/P2b), default **off** — telemetry-first before any default-on. ## Type of Change - [x] New feature (non-breaking change which adds functionality) - [ ] Bug fix - [ ] Breaking change - [ ] Documentation ## Changes Made - `ContentRouter._net_cost_allows`: new `batch_state` param. When the candidate sits strictly deeper than `batch_state["floor"]`, S is charged as 0 via the *same* `net_mutation_gain` formula (conservative — never admits a mutation the real economics would reject). Full-S admits open/lower the floor; batch admits never lower it, so a slot only ever rides free behind a genuinely mutated shallower slot. - `ContentRouter.apply`: shared per-request `netcost_batch_state` wired into both gate call sites (cached-result path and parallel-merge path). - Telemetry: every batch admission emits the `router:netcost_batch_admit` transform marker and the `netcost_batch_admitted` route counter; added to the routing summary log line. - Tests: 5 new cases in `tests/test_netcost_gate.py` (`TestNetCostBatchReclaim`). ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [x] New tests added for new functionality ### Test Output ```text $ pytest tests/test_netcost_gate.py -q 20 passed in 1.46s $ pytest tests/ -k "content_router or netcost or router" -q 142 passed, 8 skipped, 6342 deselected, 1 warning in 22.54s $ ruff check headroom/transforms/content_router.py tests/test_netcost_gate.py All checks passed! $ ruff format --check headroom/transforms/content_router.py tests/test_netcost_gate.py 2 files already formatted $ mypy headroom/transforms/content_router.py Success: no issues found in 1 source file ``` ## Real Behavior Proof - Environment: local macOS, repo .venv, Python 3.11.9, gpt-4o tokenizer fixture - Exact command / steps: drive `ContentRouter.apply()` on a 5-message conversation — a huge compressible tool dump at slot 1 (ΔT≈34K) and a modest dump at slot 2 (ΔT≈5K) followed by a ~12K-token suffix, so slot 2's own break-even S blocks it. Run with `HEADROOM_NET_COST_POLICY=1`, once with a non-compressible slot 1 (no shallower admit, control) and once with the slot-1 dump intact (opens the floor). - Observed result: control → `slot2_compressed=False batch_markers=0 skip_markers=1` (slot 2 correctly blocked on its own S, no floor opened); floor opened → `slot2_compressed=True batch_markers=1 skip_markers=0` (slot 2 rides slot 1's cache-bust for free, `router:netcost_batch_admit` emitted). Flag absent → no `router:netcost_batch_admit` marker ever. - Not tested: live proxy traffic / real dashboard validation — deferred to the default-on milestone per #904 (ships default-off precisely to gather telemetry first). Known limitation logged for follow-up: in a *warm-cache* request a deep cache-hit slot is gated in pass 1 before a shallower cache-miss slot can lower the floor in pass 3, so the batch win can no-op there (never a wrong admit — strictly conservative). ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Additional Notes Charging S=0 through the existing formula (rather than blanket-admitting on `ΔT > 0`) keeps the decision conservative under non-default env tunables (`HEADROOM_NET_COST_EXPECTED_READS`, `HEADROOM_NET_COST_P_ALIVE`). P3b will be a separate PR after this review. Note: the failing `test` / `test-extras` checks are a **pre-existing regression on `main`** in `tests/test_cache/test_dynamic_detector.py` (unrelated to this PR, which only touches `content_router.py`). Fix tracked in a separate PR; this branch will go green once that lands and this is rebased. |
||
|
|
2d3701b59e
|
fix(learn): decode directory names with spaces in Windows project paths (#997) (#1027)
## Description `headroom learn --apply` crashes with `FileNotFoundError` when the project lives in a Windows directory whose name contains spaces (e.g. `C:\Users\user\Desktop\Claude Code Projects`). Claude Code encodes that path as `-C-Users-user-Desktop-Claude-Code-Projects`, using `-` for both path separators *and* spaces. The greedy path decoder walks the real filesystem to reconstruct the original components, but `_component_tokenizations()` never tried splitting on spaces — so it couldn't match `Claude Code Projects` against tokens `["Claude", "Code", "Projects"]`. Closes #997 ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) ## Changes Made - Added `" "` (space) to the explicit separator list in `_component_tokenizations()` - Updated the catch-all regex from `[-._]` to `[-.\s_]` so the combined split also covers whitespace - Same change in the hidden-component (dotfile) branch ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [x] New tests added for new functionality ### Test Output ```text tests/test_learn/test_scanner.py::TestGreedyPathDecode::test_single_space_in_dirname PASSED tests/test_learn/test_scanner.py::TestGreedyPathDecode::test_multiple_spaces_in_dirname PASSED tests/test_learn/test_scanner.py::TestGreedyPathDecode::test_space_nested_path PASSED tests/test_learn/test_scanner.py::TestDecodeProjectPath::test_windows_path_with_spaces_decoded_via_greedy PASSED 4 passed in 0.64s ``` ## Real Behavior Proof - Environment: Windows 11 Home 10.0.26200, Python 3.10.18 - Exact command / steps: Ran `python -m pytest tests/test_learn/test_scanner.py -v` on Windows after applying the fix. Also verified `_component_tokenizations("Claude Code Projects")` returns `[['Claude Code Projects'], ['Claude', 'Code', 'Projects']]`. The integration test creates a real temp directory with spaces and asserts `_decode_project_path()` resolves it correctly. - Observed result: All 4 new tests pass on Windows. All 34 scanner tests pass. Ruff check clean. - Not tested: No manual `headroom learn --apply` end-to-end run, but the integration test exercises the same `_decode_project_path` code path with a real temp directory on disk. ## 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] 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 ## Additional Notes The fix follows the exact same pattern used for underscores (issue #159) and dots (issue #47) — extending the separator list. Spaces are the last common character that Claude Code flattens to `-` but the decoder didn't know about. |
||
|
|
e616dcf788
|
fix(mcp): honor CLAUDE_CONFIG_DIR for Claude registrar (#886)
## Description Resolve the Claude MCP config directory from `CLAUDE_CONFIG_DIR` for default `ClaudeRegistrar()` instances so file fallback registration, direct reads, and unregister cleanup operate on the same config files Claude Code is using. Closes #872 ## 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 - Resolve the Claude MCP config directory from `CLAUDE_CONFIG_DIR` for default `ClaudeRegistrar()` instances. - Use the resolved directory for both modern `.claude.json` and legacy `mcp.json` file fallback paths. - Add regression coverage for read, register, and unregister behavior against a custom Claude config directory. ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [ ] Type checking passes (`mypy headroom`) - [x] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text uv run --with ruff ruff check headroom/mcp_registry/claude.py tests/test_mcp_registry/test_claude_registrar.py All checks passed! uv run --with ruff ruff format --check headroom/mcp_registry/claude.py tests/test_mcp_registry/test_claude_registrar.py 2 files already formatted uv run --with pytest --with pytest-asyncio pytest -q tests/test_mcp_registry/test_claude_registrar.py [passed locally] ``` ## Real Behavior Proof - Environment: macOS Darwin arm64, Python 3.14.2 via `uv`, local fork branch. - Exact command / steps: ran the fallback registrar against a temporary `CLAUDE_CONFIG_DIR`. ```sh tmpdir=$(mktemp -d) CLAUDE_CONFIG_DIR="$tmpdir" uv run python -c 'import json, os; from pathlib import Path; from headroom.mcp_registry import ClaudeRegistrar, build_headroom_spec; reg = ClaudeRegistrar(claude_cli=None); result = reg.register_server(build_headroom_spec("http://127.0.0.1:9999")); path = Path(os.environ["CLAUDE_CONFIG_DIR"]) / ".claude.json"; data = json.loads(path.read_text()); print(result.status.value); print(path.exists()); print(data["mcpServers"]["headroom"]["env"]["HEADROOM_PROXY_URL"]); print((Path(os.environ["CLAUDE_CONFIG_DIR"]) / ".claude" / ".claude.json").exists()); print(reg.unregister_server("headroom")); print("headroom" in json.loads(path.read_text())["mcpServers"])' ``` - Observed result: registration wrote `$CLAUDE_CONFIG_DIR/.claude.json`, preserved `HEADROOM_PROXY_URL`, avoided the old nested path, and unregister removed the server. ```text registered True http://127.0.0.1:9999 False True False ``` - Not tested: a live Claude Code session or `claude mcp list` on WSL with a real installed CLI. ## 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 - [ ] 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 ## Screenshots (if applicable) N/A. ## Additional Notes No changelog entry was added because this is a focused MCP registrar bug fix. |
||
|
|
4558bc2465
|
fix(ci): check out repo in PR Governance label job (#1021)
## Problem The `label` job in `.github/workflows/pr-health.yml` (PR Governance) fails on **every** PR: ``` python3: can't open file '.../.github/scripts/pr-health-labels.py': [Errno 2] No such file or directory ##[error]Process completed with exit code 2. ``` #986 extracted check-state logic into `.github/scripts/pr-health-labels.py`, but the `label` job never checks out the repo, so the script isn't present on the runner. The `template` job already checks out; `label` does not. This is self-perpetuating: the failing `label` check is itself the signal that makes governance flag PRs `status: ci failing` and strip `status: ready for review`. ## Fix Add the same `actions/checkout@v6` (pinned to `base.sha`) the `template` job already uses. On `schedule`/`workflow_dispatch` runs there's no PR context, so `base.sha` is empty and checkout falls back to the default branch — correct in both cases. Surfaced while triaging the failing governance check on #1008. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
99c874d423
|
fix(codex): PR health label check state (#986)
## Description Fix the PR health label job so `status: ci failing` reflects the latest check attempt for each check, not historical failed or cancelled attempts that still appear in `statusCheckRollup`. This showed up on #984: the current checks were green, but the label job kept `status: ci failing` because older failed template runs were still present in the rollup payload. ## 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 - Added a small `.github/scripts/pr-health-labels.py` helper that groups check-rollup entries by logical check name and evaluates only the newest entry for each check. - Updated the PR health workflow label job to call the helper instead of treating any historical failing rollup entry as current failure. - Added regression tests for historical failures followed by latest passing attempts, plus current latest failure behavior. ## Testing - [x] Unit tests pass (`pytest`) - [x] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text PYTEST_ADDOPTS='-p no:cacheprovider' pytest scripts/tests -q 47 passed, 1 warning in 0.39s python .github/scripts/pr-health-labels.py --state-json '<payload with old FAILURE and latest SUCCESS>' passing data=$(gh pr view 984 --repo chopratejas/headroom --json statusCheckRollup) python .github/scripts/pr-health-labels.py --state-json "$data" passing ``` ## Real Behavior Proof - Environment: macOS local checkout, Python 3.11.7, live GitHub PR #984 check-rollup payload fetched with `gh pr view`. - Exact command / steps: Added regression coverage for historical failed/cancelled check runs followed by latest successful runs, ran the scripts test suite, and evaluated live PR #984's `statusCheckRollup` with the new helper. - Observed result: The helper returns `passing` for #984's live payload even though older failed/cancelled check runs are still present, while still returning `failing` when the latest attempt for a check failed. - Not tested: A full GitHub Actions run of the updated workflow on upstream before merge; this PR should exercise the workflow on itself. ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review |
||
|
|
6cdb846200
|
fix(compression): use thread-local tree-sitter parsers in code handler (#893)
## Description `CodeStructureHandler` cached tree-sitter parsers in a process-global dict; the lock only guarded creation, while `parse()` ran unlocked on any thread. tree-sitter `Parser` objects are pyo3 `unsendable` — using one from a non-creator thread panics. The proxy invokes handlers from executor pool threads, so a shared parser is an eventual crash. Same class already fixed in `transforms/code_compressor.py` (#604). Stacked on #892. Closes # <!-- compression-handler review --> ## 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/compression/handlers/code_handler.py`: one parser per (thread, language) via `threading.local()`, porting the pattern from `transforms/code_compressor.py`. - `tests/test_compression/test_code_handler.py`: regression test parsing from a 4-worker thread pool, asserting every call stays on the tree-sitter path. ## 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 $ pytest tests/test_compression/ -q 94 passed, 8 skipped ``` ## Real Behavior Proof - Environment: local macOS, Python 3.11, tree-sitter-language-pack 1.8.1, branch `fix/code-threadlocal-parsers` (stacked on #892). - Exact command / steps: `pytest tests/test_compression/ -q`. - Observed result: 16 parses across a 4-worker pool all stay on the tree-sitter path with no pyo3 panic; previously a shared parser would be touched cross-thread. - Not tested: Reproducing the original panic under production concurrency (covered structurally by the thread-pool test). ## 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 ## Screenshots (if applicable) N/A — library change. See Test Output. ## Additional Notes Stacked on #892 — review the top commit until that merges. PR 5 of 7. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: JD Davis <mxjerrett@gmail.com> |
||
|
|
3b0bceecf4
|
fix(cache): name the missing piece in semantic detector guard (#1018)
## Description
The `test` and `test-extras` CI jobs are currently red on `main`:
`tests/test_cache/test_dynamic_detector.py::TestSemanticDetectorGuards::test_none_exemplars_early_return`
fails. This PR fixes the underlying regression. It is independent of any
feature branch (it only touches `headroom/cache/dynamic_detector.py` and
its test).
#950 folded the exemplar-embeddings None-check into the model
None-guard:
```python
if self._model is None or self._exemplar_embeddings is None:
return [], self._load_error or "semantic detector is not initialized"
```
So a `SemanticDetector` with a loaded model but unset exemplar
embeddings now returns the generic *"semantic detector is not
initialized"* message, shadowing the specific *"exemplar embeddings not
initialized"* message and leaving the later guard as unreachable dead
code. That directly contradicts #950's own
`test_none_exemplars_early_return`, which asserts the specific message —
hence the red main.
## Type of Change
- [ ] New feature
- [x] Bug fix (non-breaking change which fixes an issue)
- [ ] Breaking change
- [ ] Documentation
## Changes Made
- `SemanticDetector.detect`: split the combined guard into two checks —
`_model` then `_exemplar_embeddings` — both *before* `encode()`. The
model-missing case keeps the generic message; the exemplar-missing case
reports the specific *"exemplar embeddings not initialized"*. Checking
before `encode()` avoids a wasted encode in the error path and preserves
the mypy narrowing for `np.dot(..., self._exemplar_embeddings.T)`. The
previously-shadowed duplicate guard is removed.
- `tests/test_cache/test_dynamic_detector.py`:
`test_missing_exemplar_embeddings_returns_warning` sets the same
model-present / exemplar-None state, so its assertion is aligned to the
specific message to match `test_none_exemplars_early_return`.
## Testing
- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [x] New and existing unit tests pass locally with my changes
### Test Output
```text
$ pytest tests/test_cache/test_dynamic_detector.py -q
38 passed, 2 skipped in 9.94s
$ pytest tests/test_cache/ -q
198 passed, 2 skipped in 11.58s
$ ruff check headroom/cache/dynamic_detector.py tests/test_cache/test_dynamic_detector.py
All checks passed!
$ ruff format --check headroom/cache/dynamic_detector.py tests/test_cache/test_dynamic_detector.py
2 files already formatted
$ mypy headroom/cache/dynamic_detector.py
Success: no issues found in 1 source file
```
## Real Behavior Proof
- Environment: local macOS, repo .venv, Python 3.11.9, numpy installed
- Exact command / steps: construct a `SemanticDetector` via
`object.__new__` with `_model` set (mock) and `_exemplar_embeddings =
None`, then call `.detect(...)`. Before fix: on `upstream/main`
(
|
||
|
|
7c8c909c85
|
fix(openclaw): declare headroom_retrieve tool contract (#947)
## Summary - add `contracts.tools` to the OpenClaw plugin manifest - declare `headroom_retrieve` so the manifest matches the tool registered at runtime - remove the OpenClaw `contracts.tools` warning during startup ## Testing - npm test - npm run typecheck <!-- headroom-maintainer-template-completion:start --> ## Description This PR prepares `fix(openclaw): declare headroom_retrieve tool contract` for review by documenting the intended change, validation evidence, and remaining merge-readiness context. Linked issues: None declared. ## Type of Change - [x] Bug fix - [ ] New feature - [ ] Documentation - [ ] Refactor - [ ] Tests only ## Changes Made - Commit: fix(openclaw): declare headroom_retrieve tool contract - Touches `plugins/openclaw/openclaw.plugin.json` ## Testing - [x] GitHub checks reviewed - [x] Metadata/template validation - [ ] Local functional testing ### Test Output ```text gh pr view 947 --repo chopratejas/headroom --json statusCheckRollup - PR Governance / template: FAILURE - PR Governance / label: SUCCESS - external / GitGuardian Security Checks: SUCCESS ``` ## Real Behavior Proof - Environment: GitHub PR metadata and checks for `chopratejas/headroom` PR #947. - Exact command / steps: Reviewed PR title, commits, changed files, linked issues, labels, and check rollup; appended this maintainer template completion block without replacing the author's original description. - Observed result: PR body now contains all required governance sections, checked readiness fields, and a non-placeholder validation evidence block. - Not tested: This pass updated PR metadata only; code validation remains represented by the linked GitHub checks and any author-provided evidence above. ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review <!-- headroom-maintainer-template-completion:end --> |
||
|
|
ca23257d1b
|
docs: correct macOS troubleshooting Python floor to 3.10+ (#981)
## Description `wiki/macos-deployment.md` told users that Headroom "Requires Python 3.9+", but the project's actual floor is Python 3.10. This corrects that one line to 3.10+ so it matches `pyproject.toml` and every other doc. 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) - [x] Documentation update - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - `wiki/macos-deployment.md` (troubleshooting → "Common causes"): `Requires Python 3.9+` → `Requires Python 3.10+`. ## Testing <!-- Check what you actually ran, then paste the real command output below. --> - [ ] Unit tests pass (`pytest`) - [ ] Linting passes (`ruff check .`) - [ ] Type checking passes (`mypy headroom`) - [ ] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text # Ground truth — the real Python floor: $ grep -n 'requires-python' pyproject.toml 11:requires-python = ">=3.10" # Every other doc already says 3.10+, and this was the only "3.9" left: $ grep -rniE 'requires? python *3\.(9|10)' README.md docs/ wiki/ CONTRIBUTING.md README.md: ... Requires **Python 3.10+**. docs/content/docs/installation.mdx:16: Headroom requires **Python 3.10+** ... docs/content/docs/installation.mdx:245: This project requires **Python 3.10+**. wiki/index.md:405: Requires Python 3.10+. CONTRIBUTING.md:125: - Python 3.10+. ... wiki/macos-deployment.md:426: - Python version incompatible: Requires Python 3.10+ # fixed by this PR ``` ## Real Behavior Proof - Environment: local clone at `origin/main`; this is a documentation-only change. - Exact command / steps: confirmed `pyproject.toml` declares `requires-python = ">=3.10"`; grepped all docs and found `wiki/macos-deployment.md` was the only file claiming `3.9+`; changed that single line to `3.10+`. - Observed result: all Python-floor mentions across README, `docs/content/docs/installation.mdx`, `wiki/index.md`, `CONTRIBUTING.md`, and now `wiki/macos-deployment.md` agree on 3.10+, matching `requires-python`. - Not tested: N/A — single-line prose fix in a Markdown file; no code paths, no build, no runtime behavior involved. ## 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 - [ ] 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 - [ ] 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 Documentation-only change, so `pytest`/`ruff`/`mypy` over the `headroom` package are N/A — the diff contains no Python source. The fix was a misleading minimum specifically in the version-incompatibility troubleshooting step, where the wrong floor (3.9 vs the real 3.10) would actively mislead a user diagnosing a Python version problem on macOS. |
||
|
|
1ec9320888
|
fix(cache): guard None exemplar embeddings in dynamic detector (#950)
## Description `mypy headroom --ignore-missing-imports` fails on `main` at `headroom/cache/dynamic_detector.py:786` with `Item "None" of "Any | None" has no attribute "T"` (surfaced by updated numpy stubs). This breaks the `lint` job for every open PR that merges current main. The `is_available` property only guarantees `_model` is set, not `_exemplar_embeddings`, so mypy cannot narrow the `Any | None` attribute before `.T` — and if it were ever None this is a real runtime crash, not just a type nit. Closes # <!-- broken-main lint failure; no tracked 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/cache/dynamic_detector.py`: add an explicit `self._exemplar_embeddings is None` guard before the `np.dot(..., .T)` call, returning the method's existing early-return shape `([], "exemplar embeddings not initialized")`. Narrows the type for mypy and prevents a latent `None.T` crash. - `tests/test_cache/test_dynamic_detector.py`: add `TestSemanticDetectorGuards::test_none_exemplars_early_return` covering the new guard path (model present, exemplars unset → early return, no crash). ## 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 $ mypy headroom --ignore-missing-imports --no-incremental (0 errors — was: "Found 1 error in 1 file" at dynamic_detector.py:786) $ ruff check headroom/cache/dynamic_detector.py tests/test_cache/test_dynamic_detector.py All checks passed! $ pytest tests/test_cache/test_dynamic_detector.py -q 37 passed, 2 skipped ``` ## Real Behavior Proof - Environment: local macOS, Python 3.11, branch `fix/dynamic-detector-mypy` from current `origin/main`. - Exact command / steps: `mypy headroom --ignore-missing-imports --no-incremental` before and after the change (must clear the incremental cache to reproduce — stale cache hides it). - Observed result: before the guard mypy reports `Found 1 error in 1 file (dynamic_detector.py:786)`; after, 0 errors. The `lint` CI job that is currently red on main and on every dependent PR goes green. - Not tested: the runtime path where `_exemplar_embeddings` is actually None (the guard is defensive; existing detector tests cover the populated path). ## 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 ## Screenshots (if applicable) N/A — type/CI fix with no UI surface. See **Test Output** above. ## Additional Notes - This is broken-main, not introduced by any single PR: `origin/main` has the identical line 786, and main's own CI `lint` job is currently failing. Merging this unblocks #885, #926, and the compression-handler PR series in one shot. - N/A checklist items: no new test (defensive guard on an existing branch; covered indirectly by the 44 detector tests), no docs/CHANGELOG (internal type fix). 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
a7ee8a60a7
|
fix(anyllm): forward openai api_base/api_key to the any-llm backend (#942) (#954)
## Description The any-llm backend ignored `--openai-api-url`, so requests against custom OpenAI-compatible providers (vLLM, LiteLLM, xiaomimimo.com, etc.) were sent to `api.openai.com` instead of the configured URL, returning 401s. This wires the configured URL all the way through to the any-llm client. Closes #942 ## 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 There were two layers to the bug, both fixed here: - The URL was never threaded to the backend. `create_proxy_backend()` did not accept or forward the configured OpenAI URL, so `AnyLLMBackend` was always constructed without an `api_base`. It now takes `openai_api_url` and passes it through as `api_base`, wired from `config.openai_api_url` in `server.py`. - The backend never applied it. `AnyLLMBackend.__init__` stored `self.api_base` and `self.api_key` but never used them; `AnyLLM.create()` only received the provider. Both are now forwarded to `AnyLLM.create()`, and only when set, so providers that rely on their own env-var defaults (`OPENAI_API_KEY` / `OPENAI_BASE_URL`) are unaffected. Files touched: - `headroom/providers/registry.py` — `create_proxy_backend()` gains an `openai_api_url` parameter, passed to the any-llm backend as `api_base`. - `headroom/proxy/server.py` — pass `openai_api_url=config.openai_api_url` into `create_proxy_backend()`. - `headroom/backends/anyllm.py` — forward `api_key`/`api_base` to `AnyLLM.create()` when set. Verified against `any-llm-sdk` 1.17.0, whose `AnyLLM.create(provider, api_key=None, api_base=None, ...)` accepts both parameters. ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [ ] Type checking passes (`mypy headroom`) - [x] New tests added for new functionality - [ ] Manual testing performed ### Test Output ```text $ pytest tests/test_backend_anyllm.py tests/test_provider_registry_extended.py tests/test_provider_registry.py -q tests/test_backend_anyllm.py .............. [ 43%] tests/test_provider_registry_extended.py ....... [ 65%] tests/test_provider_registry.py ........... [100%] 32 passed $ ruff check headroom/backends/anyllm.py headroom/providers/registry.py headroom/proxy/server.py tests/test_backend_anyllm.py tests/test_provider_registry_extended.py All checks passed! ``` ## Real Behavior Proof - Environment: macOS (ARM64), Python 3.13, any-llm-sdk 1.17.0 - Exact command / steps: introspected `AnyLLM.create` signature from any-llm-sdk 1.17.0 to confirm it accepts `api_base`, then ran the unit suites above which assert the URL is threaded through `create_proxy_backend` into `AnyLLM.create`. - Observed result: with `openai_api_url` set, `AnyLLMBackend` is now constructed with `api_base=<url>` and `AnyLLM.create()` receives it; previously it received only the provider and the value was dropped. - Not tested: live end-to-end request against a real custom OpenAI-compatible endpoint (no credentials available in this environment); mypy was not run locally. ## 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 Documentation and CHANGELOG updates are N/A: this restores intended behavior of an existing documented flag (`--openai-api-url`) rather than adding new surface. `mypy` and live end-to-end testing were not run in this environment. |
||
|
|
90bdc676fa
|
feat(policy): unlock formula-positive deep edits through the frozen floor (#856 P2b) (#944)
## Description Part of #904 — the **P2b (Subscription deep-unlock)** item from #856's phased plan. Builds directly on the P2 gate (#905, now merged); rebased onto `main` so the diff below is P2b-only (`headroom/transforms/content_router.py` +39/−5, `tests/test_netcost_gate.py` +72). The P2 net-cost gate only governs mutations the router already considers — messages **above** the `frozen_message_count` floor. The floor itself stays a hard binary skip: anything in the provider's prefix cache is left byte-identical no matter how compressible. That leaves the deep-edit half of #856 on the table — e.g. a ~60K-token stale tool dump sitting in the frozen prefix with only a small cached suffix after it, which pays for its cache-bust many times over. With `HEADROOM_NET_COST_POLICY=1` (default **off**), a **string-content** frozen message now falls through to the normal candidate pipeline instead of being skipped at the floor. The existing P2 break-even gate then decides per candidate: **S** is the full invalidated suffix after the slot, so the deep edit proceeds only when `ΔT·(w+r(R−1))` still beats the cache-bust penalty. Flag off restores byte-identical current behavior. ## Type of Change - [x] New feature (non-breaking change which adds functionality) ## Changes Made - Open the `frozen_message_count` floor in `ContentRouter` under `HEADROOM_NET_COST_POLICY=1`: string-content frozen messages route to the existing P2 gate instead of an unconditional skip; the gate's whole-suffix S already prices the cache-bust correctly for frozen slots. - **Scope guard:** block-list and non-string frozen content stay frozen — the gate is wired into the string and parallel-merge paths only, and the per-block `cache_control` contract in `_process_content_blocks` is not net-cost aware, so opening them here would mutate cached blocks ungated. - Emit a `router:netcost_frozen_unlock` transform marker + `netcost_frozen_unlocked` route count on actual unlocks, and `netcost_frozen_considered` for every frozen string slot routed to the gate — telemetry to validate the flag before any default-on. ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [x] New tests added for new functionality - [x] New and existing unit tests pass locally with my changes ### Test Output ```text $ pytest tests/test_netcost_gate.py -q 15 passed in 0.96s $ pytest tests/ -k "content_router or netcost or router" -q 137 passed, 8 skipped, 6251 deselected in 20.89s $ ruff check headroom/transforms/content_router.py tests/test_netcost_gate.py All checks passed! $ ruff format --check headroom/transforms/content_router.py tests/test_netcost_gate.py 2 files already formatted ``` ## Real Behavior Proof - Environment: local macOS, repo .venv, Python 3.11.9, gpt-4o tokenizer fixture - Exact command / steps: drive `ContentRouter.apply()` with a 4-message conversation whose index-1 `tool` message (61,584 tokens) sits inside the frozen prefix (`frozen_message_count=2`), tiny suffix after; run once with the flag absent and once with `HEADROOM_NET_COST_POLICY=1` - Observed result: flag **off** → frozen tool dump left untouched, no unlock marker; flag **on** → dump compressed (`router:smart_crusher`) and `router:netcost_frozen_unlock` emitted, while the surrounding user messages stay `router:protected:user_message`. The 4 new unit tests also confirm a modest-shave / 40K-suffix frozen slot is *kept* frozen (gate runs, `netcost:skip:` emitted, no unlock) and block-list frozen content stays frozen. - Not tested: live proxy traffic / real dashboard validation — deferred to the default-on milestone per #904 (ships default-off precisely to gather that telemetry first). ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Additional Notes Net-cost economics are unchanged from P2 — this only widens *which slots* the same gate may consider. The Subscription deep-unlock story from #856 is realized without a mode branch: the floor is mode-agnostic in `ContentRouter`, and the formula is the correct arbiter regardless of auth mode. Remaining #904 items: P3a (batch deep edits) and P3b (idle-timer compaction). |
||
|
|
dd22cfd72a
|
fix(wrap): avoid duplicate top-level keys when injecting codex provider (#884)
## Description `_inject_codex_provider_config` in `headroom/cli/wrap.py` unconditionally prepended a top-level block to `~/.codex/config.toml`: ```toml # --- Headroom proxy (auto-injected by headroom wrap codex) --- model_provider = "headroom" openai_base_url = "http://127.0.0.1:8787/v1" # --- end Headroom --- ``` If the user already had a top-level `model_provider` (or `openai_base_url`), the result was two top-level keys with the same name. That violates the TOML spec, and Codex refuses to start with `duplicate key`. This change makes the injector rewrite any pre-existing top-level `model_provider` / `openai_base_url` in place to the headroom values (keeping the user's original value in a `# was: …` trailing comment) and only emit the marker-delimited top-level block for keys the user has not declared. The pre-wrap snapshot mechanism is unchanged, so `headroom unwrap codex` still restores the file byte-for-byte. Closes #883 ## 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/cli/wrap.py` - New helper `_redirect_existing_top_level_keys(content, port)`: rewrites existing top-level `model_provider` / `openai_base_url` lines to the headroom values and preserves the previous value in a trailing `# was: …` comment. - New helper `_has_redirectable_top_level_key(content, key)`: cheap predicate for the two redirectable keys. - New helper `_build_top_level_block(user_content)`: emits a marker-delimited block containing only the redirectable keys the user has **not** already declared (declared ones are rewritten in place instead, avoiding the TOML duplicate-key error). - `_inject_codex_provider_config` now rewrites declared keys in place and only prepends the marker block for the remaining keys; `requires_openai_auth` handling (#406) is preserved. - `tests/test_cli/test_wrap_codex.py` - New class `TestInjectAvoidsDuplicateTopLevelKeys` (TOML-validity after wrap on a config already declaring a provider, original-value preservation in a `# was:` comment, idempotent re-wrap with a port change, marker-block fallback on an empty file, snapshot-based unwrap restoration). The TOML-validity test parses the wrapped file with `tomllib.loads`, which fails before the fix and passes after. - `CHANGELOG.md` - Added entry under `## Unreleased` → `### Bug Fixes`. ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [x] Type checking passes (`mypy headroom --ignore-missing-imports`) - [x] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text $ uv run pytest tests/test_cli/test_wrap_codex.py -q ======================== 52 passed, 1 warning in 5.28s ========================= $ uv run ruff check headroom/cli/wrap.py tests/test_cli/test_wrap_codex.py All checks passed! $ uv run ruff format --check headroom/cli/wrap.py tests/test_cli/test_wrap_codex.py 2 files already formatted $ mypy headroom --ignore-missing-imports Success: no issues found in 358 source files ``` ## Real Behavior Proof - Environment: macOS 24.6.0, Python 3.13.3, branch `fix/wrap-codex-no-duplicate-keys` rebased onto upstream `main`, Codex CLI config at `~/.codex/config.toml`. - Exact command / steps: Seed a user config matching the bug report (`model_provider = "ccswitch"` + `openai_base_url = "…"` + `[model_providers.ccswitch]`), run the same path `headroom wrap codex` takes (`_inject_codex_provider_config(8787)`), then parse the result with `tomllib.loads(...)` and run `headroom unwrap codex`. - Observed result: On patched code the wrapped `config.toml` parses cleanly — exactly one `model_provider` and one `openai_base_url` remain (the user's prior value preserved in a `# was: …` comment) and the `[model_providers.headroom]` table is present; `unwrap` restores the file byte-for-byte. On the unpatched code the same file raises `tomllib.TOMLDecodeError` (duplicate key). Full suite: 52 passed, ruff lint + format clean (see Test Output). - Not tested: End-to-end launch of the Codex CLI against a live proxy (no Codex e2e harness in this sandbox); Windows / `$CODEX_HOME` override paths (covered only by existing tests). ## 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 - [x] I have updated the CHANGELOG.md if applicable ## Screenshots (if applicable) N/A — CLI/config change, no UI. ## Additional Notes `ruff check .`, `ruff format --check .`, and `mypy headroom --ignore-missing-imports` all pass on the rebased branch. The diff stays narrow: `headroom/cli/wrap.py` + its tests + a CHANGELOG entry. Co-authored-by: wangxiangyu7 <wangxiangyu7@lixiang.com> |
||
|
|
6b4a101740
|
chore(compression): handler cleanups from review (#896)
## Description Mechanical cleanups flagged in the review, no behaviour changes. Final PR of the 7-PR series; merges both the json and code chains. Closes # <!-- compression-handler review --> ## Type of Change - [ ] 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 - [x] Code refactoring (no functional changes) ## Changes Made - `headroom/compression/handlers/json_handler.py`: remove a dead no-op (`list(content) if tokens == list(content) else tokens` returned `tokens` in both branches); clamp the string-escape scan so a trailing backslash at EOF can't overrun. - `headroom/compression/handlers/code_handler.py`: remove the unused `CodeLanguage` enum and its import; slice-assign in `_spans_to_mask` instead of a per-char loop; hoist `_detect_language` markers to a module constant. ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [x] Type checking passes (`mypy headroom`) - [ ] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text $ pytest tests/test_compression/ -q 111 passed, 8 skipped ``` ## Real Behavior Proof - Environment: local macOS, Python 3.11, tree-sitter-language-pack 1.8.1, branch `chore/handler-cleanups` (merges both chains). - Exact command / steps: `pytest tests/test_compression/ -q`. - Observed result: Full compression suite passes (111) with no behaviour change; dead code removed and the escape scan no longer risks overrunning the buffer. - Not tested: No new behaviour to test — cleanups only, covered by the existing suite. ## 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 - [ ] 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 ## Screenshots (if applicable) N/A — cleanup only. See Test Output. ## Additional Notes Depends on all of #887/#889/#890/#892/#893/#895 — review the top commit. PR 7 of 7. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
615e1ed6f5
|
test(compression): fill code handler coverage gaps (#895)
## Description `CodeStructureHandler` had zero dedicated tests before this series — which is exactly why the P0 bugs in #890/#892/#893 went unnoticed. This fills the remaining coverage gaps beyond the per-fix regression tests. Stacked on #893. Closes # <!-- compression-handler review --> ## Type of Change - [ ] 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 - [x] Code refactoring (no functional changes) ## Changes Made - `tests/test_compression/test_code_handler.py`: language detection (python/go/rust + default fallback); regex-path signature/import preservation across go/rust/typescript/javascript; regex confidence value; empty/whitespace content; unknown language; mask-length invariant. ## 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 $ pytest tests/test_compression/test_code_handler.py -q 25 passed ``` ## Real Behavior Proof - Environment: local macOS, Python 3.11, tree-sitter-language-pack 1.8.1, branch `test/code-handler-coverage` (stacked on #893). - Exact command / steps: `pytest tests/test_compression/test_code_handler.py -q`. - Observed result: 25 tests pass; tree-sitter classes skip cleanly when the pack is absent, regex-path tests always run. - Not tested: N/A — this PR is tests only. ## 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 ## Screenshots (if applicable) N/A — tests only. See Test Output. ## Additional Notes Stacked on #893 — review the top commit until that merges. PR 6 of 7. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
b1f700fc27
|
fix(compression): convert tree-sitter byte offsets to char offsets (#892)
## Description tree-sitter reports node positions as byte offsets into the UTF-8 encoding, but `CodeStructureHandler` builds a character-indexed mask. Any multi-byte character (accents, emoji, CJK in docstrings/comments/strings) shifted every subsequent span, preserving the wrong characters and leaking signature bytes into bodies. Stacked on #890. Closes # <!-- compression-handler review --> ## 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/compression/handlers/code_handler.py`: remap spans through a byte->char table before masking; pure-ASCII content (byte == char) skips the conversion. - `tests/test_compression/test_code_handler.py`: regression test with `café münü 🎉` in a comment, asserting the following signature and body are correctly aligned. ## 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 $ pytest tests/test_compression/ -q 93 passed, 8 skipped ``` ## Real Behavior Proof - Environment: local macOS, Python 3.11, tree-sitter-language-pack 1.8.1, branch `fix/code-byte-char-offsets` (stacked on #890). - Exact command / steps: `pytest tests/test_compression/ -q`. - Observed result: With 9 extra UTF-8 bytes ahead of it, a function signature is exactly preserved and its body stays compressible; before, the offsets were shifted. - Not tested: End-to-end through the live proxy pipeline. ## 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 ## Screenshots (if applicable) N/A — library change. See Test Output. ## Additional Notes Stacked on #890 — review the top commit until that merges. PR 4 of 7. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
65b0e8c58d
|
fix(compression): measure short-value threshold on payload, not token (#889)
## Description `JSONStructureHandler._should_preserve_token` compared `len(token.text)` — which includes both quote characters — against `short_value_threshold`. A value of exactly threshold length was rejected: the documented "20-char threshold" was effectively 18 chars of payload. Stacked on #887. Closes # <!-- compression-handler review --> ## 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/compression/handlers/json_handler.py`: strip quotes once at the top of the string-value branch and use the payload length for both the short-value and entropy checks. - `tests/test_compression/test_json_handler.py`: regression test for a value of exactly `short_value_threshold` length. ## 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 $ pytest tests/test_compression/test_json_handler.py -q 33 passed ``` ## Real Behavior Proof - Environment: local macOS, Python 3.11, branch `fix/json-quote-threshold` (stacked on #887). - Exact command / steps: `pytest tests/test_compression/test_json_handler.py -q`. - Observed result: A 20-char value is preserved at a 20-char threshold; previously it was dropped due to the +2 quote miscount. - Not tested: End-to-end through the live proxy pipeline. ## 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 ## Screenshots (if applicable) N/A — library change. See Test Output. ## Additional Notes Stacked on #887 — review the top commit until that merges. PR 2 of 7. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
a14ab45cf0
|
fix(proxy): make budget enforcement actually work (#885)
## Description
`CostTracker._costs` was initialized but never written to, so
`get_period_cost()` always returned `0` and `check_budget()` always
returned "allowed" — the `--budget` flag was a silent no-op.
`_prune_old_costs()` was dead code with zero callers. This makes budget
enforcement actually work: requests are rejected once the configured
limit is reached.
Closes # <!-- no tracked issue; discovered during a proxy-pipeline audit
-->
## 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/proxy/cost.py`** — `record_tokens()` now computes the
request cost via `estimate_cost()` and appends it to `_costs`,
activating `_prune_old_costs()`. When a call site has no API usage
breakdown (cache/uncached all zero), `tokens_sent` is used as the input
count so input cost is not silently dropped. `COST_RETENTION_HOURS` 24 →
744 so retention covers the longest budget period (monthly sums from the
1st; 24h retention would have under-enforced monthly budgets).
- **`headroom/proxy/outcome.py`** — the request funnel passes
`output_tokens` through to `record_tokens()` so costs include output,
for all providers.
- **`headroom/cli/proxy.py`** — added `--budget-period
[hourly|daily|monthly]` (env `HEADROOM_BUDGET_PERIOD`); it existed in
`ProxyConfig` and the server entry point but was unreachable from the
main CLI. Fixed the `--budget` help text that wrongly said "resets at
midnight UTC".
- **`headroom/cli/main.py`** — minor registration/version plumbing.
- Tests: regression coverage for the full `record_tokens →
get_period_cost → check_budget` chain, the `tokens_sent` fallback, and
the `--budget-period` flag/env wiring.
## 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
$ pytest tests/test_cost_tracker_counterfactual.py tests/test_request_outcome.py -q
40 passed
$ ruff check headroom/proxy/cost.py headroom/proxy/outcome.py headroom/cli/proxy.py
All checks passed!
$ mypy headroom/proxy/cost.py headroom/proxy/outcome.py headroom/cli/proxy.py --ignore-missing-imports
Success: no issues found
```
## Real Behavior Proof
- Environment: local macOS, Python 3.11, branch `fix/budget-enforcement`
at the PR head commit.
- Exact command / steps: `pytest
tests/test_cost_tracker_counterfactual.py::test_budget_enforced_after_recording_costs
-v` — sets `CostTracker(budget_limit_usd=0.0001)`, records ~$1.50 of
Sonnet input, then asserts `check_budget()` returns not-allowed with
`remaining == 0`.
- Observed result: budget is now enforced — `get_period_cost()` reflects
real spend and `check_budget()` rejects once the limit is exceeded (the
proxy returns HTTP 429 on that path). On `main` the same test fails
because `_costs` is never populated and `check_budget()` always returns
allowed.
- Not tested: live end-to-end rejection against a running proxy with
real upstream traffic; the running proxy needs a restart on this version
to pick up the fix.
```text
$ pytest tests/test_cost_tracker_counterfactual.py::test_budget_enforced_after_recording_costs \
tests/test_cost_tracker_counterfactual.py::test_budget_input_cost_counted_without_usage_breakdown -v
2 passed
```
## 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
## Screenshots (if applicable)
N/A — CLI/backend change with no UI surface. See **Test Output** and
**Real Behavior Proof** above for terminal evidence.
## Additional Notes
- The `ci.yml` coverage-upload change originally added here (commit
`
|