Commit graph

2630 commits

Author SHA1 Message Date
r00t
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).
2026-06-17 11:43:29 -05:00
Shengbo_Wang
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.
2026-06-17 11:41:49 -05:00
Shlok Tiwari
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>
2026-06-17 09:42:38 -05:00
Zhenjia ZHOU
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>
2026-06-17 08:41:16 -05:00
dependabot[bot]
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 &gt; 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="50b951225b">50b9512</a>)</li>
<li>reject windows alternate paths (<a
href="https://redirect.github.com/vitejs/vite/issues/22572">#22572</a>)
(<a
href="dc245c71e5">dc245c7</a>)</li>
</ul>
<h2><!-- raw HTML omitted --><a
href="https://github.com/vitejs/vite/compare/v8.0.14...v8.0.15">8.0.15</a>
(2026-06-01)<!-- raw HTML omitted --></h2>
<h3>Features</h3>
<ul>
<li>send 408 on request timeout (<a
href="https://redirect.github.com/vitejs/vite/issues/22476">#22476</a>)
(<a
href="c85c9eeb9a">c85c9ee</a>)</li>
<li>update rolldown to 1.0.3 (<a
href="https://redirect.github.com/vitejs/vite/issues/22538">#22538</a>)
(<a
href="646dbedd28">646dbed</a>)</li>
</ul>
<h3>Bug Fixes</h3>
<ul>
<li>capitalize error messages and remove spurious space in parse error
(<a
href="https://redirect.github.com/vitejs/vite/issues/22488">#22488</a>)
(<a
href="85a0eff1c8">85a0eff</a>)</li>
<li><strong>deps:</strong> update all non-major dependencies (<a
href="https://redirect.github.com/vitejs/vite/issues/22511">#22511</a>)
(<a
href="2686d7d0b7">2686d7d</a>)</li>
<li><strong>dev:</strong> fix html-proxy cache key mismatch for /@fs/
HTML paths (<a
href="https://redirect.github.com/vitejs/vite/issues/21762">#21762</a>)
(<a
href="47c4213f13">47c4213</a>)</li>
<li><strong>glob:</strong> error on relative glob in virtual module when
no files match (<a
href="https://redirect.github.com/vitejs/vite/issues/22497">#22497</a>)
(<a
href="5c8e98f8b5">5c8e98f</a>)</li>
<li><strong>optimizer:</strong> close the rolldown bundle when write()
rejects (<a
href="https://redirect.github.com/vitejs/vite/issues/22528">#22528</a>)
(<a
href="e3cfb9deec">e3cfb9d</a>)</li>
<li><strong>resolve:</strong> provide onWarn for viteResolvePlugin in JS
plugin containers (<a
href="https://redirect.github.com/vitejs/vite/issues/22509">#22509</a>)
(<a
href="40985f1c09">40985f1</a>)</li>
</ul>
<h3>Miscellaneous Chores</h3>
<ul>
<li><strong>deps:</strong> update rolldown-related dependencies (<a
href="https://redirect.github.com/vitejs/vite/issues/22566">#22566</a>)
(<a
href="3052a67d93">3052a67</a>)</li>
</ul>
<h3>Code Refactoring</h3>
<ul>
<li>correct logic in <code>collectAllModules</code> function (<a
href="https://redirect.github.com/vitejs/vite/issues/22562">#22562</a>)
(<a
href="6978a9ceb9">6978a9c</a>)</li>
</ul>
<h2><!-- raw HTML omitted --><a
href="https://github.com/vitejs/vite/compare/v8.0.13...v8.0.14">8.0.14</a>
(2026-05-21)<!-- raw HTML omitted --></h2>
<h3>Features</h3>
<ul>
<li>update rolldown to 1.0.2 (<a
href="https://redirect.github.com/vitejs/vite/issues/22484">#22484</a>)
(<a
href="96efc88570">96efc88</a>)</li>
</ul>
<h3>Bug Fixes</h3>
<ul>
<li><strong>deps:</strong> update all non-major dependencies (<a
href="https://redirect.github.com/vitejs/vite/issues/22471">#22471</a>)
(<a
href="98b8163213">98b8163</a>)</li>
<li><strong>dev:</strong> handle errors when sending messages to vite
server (<a
href="https://redirect.github.com/vitejs/vite/issues/22450">#22450</a>)
(<a
href="e8e9a34dcf">e8e9a34</a>)</li>
<li><strong>html:</strong> handle trailing slash paths in
transformIndexHtml (<a
href="https://redirect.github.com/vitejs/vite/issues/22480">#22480</a>)
(<a
href="5d94d1bffd">5d94d1b</a>)</li>
<li><strong>optimizer:</strong> pass oxc jsx options to transformSync in
dependency scan (<a
href="https://redirect.github.com/vitejs/vite/issues/22342">#22342</a>)
(<a
href="b3132dacea">b3132da</a>)</li>
</ul>
<h3>Miscellaneous Chores</h3>
<ul>
<li><strong>deps:</strong> update rolldown-related dependencies (<a
href="https://redirect.github.com/vitejs/vite/issues/22470">#22470</a>)
(<a
href="7cb728eb62">7cb728e</a>)</li>
<li>remove irrelevant commits from changelog (<a
href="2c69495f25">2c69495</a>)</li>
</ul>
<h3>Code Refactoring</h3>
<ul>
<li><strong>glob:</strong> do not rewrite import path for absolute base
(<a
href="https://redirect.github.com/vitejs/vite/issues/22310">#22310</a>)
(<a
href="0ae2844ab6">0ae2844</a>)</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="f94df87ff0"><code>f94df87</code></a>
release: v8.0.16</li>
<li><a
href="dc245c71e5"><code>dc245c7</code></a>
fix: reject windows alternate paths (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/22572">#22572</a>)</li>
<li><a
href="50b951225b"><code>50b9512</code></a>
fix(deps): reject UNC paths for launch-editor-middleware (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/22571">#22571</a>)</li>
<li><a
href="8d1b0195fd"><code>8d1b019</code></a>
release: v8.0.15</li>
<li><a
href="2686d7d0b7"><code>2686d7d</code></a>
fix(deps): update all non-major dependencies (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/22511">#22511</a>)</li>
<li><a
href="3052a67d93"><code>3052a67</code></a>
chore(deps): update rolldown-related dependencies (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/22566">#22566</a>)</li>
<li><a
href="e3cfb9deec"><code>e3cfb9d</code></a>
fix(optimizer): close the rolldown bundle when write() rejects (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/22528">#22528</a>)</li>
<li><a
href="6978a9ceb9"><code>6978a9c</code></a>
refactor: correct logic in <code>collectAllModules</code> function (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/22562">#22562</a>)</li>
<li><a
href="646dbedd28"><code>646dbed</code></a>
feat: update rolldown to 1.0.3 (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/22538">#22538</a>)</li>
<li><a
href="85a0eff1c8"><code>85a0eff</code></a>
fix: capitalize error messages and remove spurious space in parse error
(<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/22488">#22488</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/vitejs/vite/commits/v8.0.16/packages/vite">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="50b951225b">50b9512</a>)</li>
<li>reject windows alternate paths (<a
href="https://redirect.github.com/vitejs/vite/issues/22572">#22572</a>)
(<a
href="dc245c71e5">dc245c7</a>)</li>
</ul>
<h2><!-- raw HTML omitted --><a
href="https://github.com/vitejs/vite/compare/v8.0.14...v8.0.15">8.0.15</a>
(2026-06-01)<!-- raw HTML omitted --></h2>
<h3>Features</h3>
<ul>
<li>send 408 on request timeout (<a
href="https://redirect.github.com/vitejs/vite/issues/22476">#22476</a>)
(<a
href="c85c9eeb9a">c85c9ee</a>)</li>
<li>update rolldown to 1.0.3 (<a
href="https://redirect.github.com/vitejs/vite/issues/22538">#22538</a>)
(<a
href="646dbedd28">646dbed</a>)</li>
</ul>
<h3>Bug Fixes</h3>
<ul>
<li>capitalize error messages and remove spurious space in parse error
(<a
href="https://redirect.github.com/vitejs/vite/issues/22488">#22488</a>)
(<a
href="85a0eff1c8">85a0eff</a>)</li>
<li><strong>deps:</strong> update all non-major dependencies (<a
href="https://redirect.github.com/vitejs/vite/issues/22511">#22511</a>)
(<a
href="2686d7d0b7">2686d7d</a>)</li>
<li><strong>dev:</strong> fix html-proxy cache key mismatch for /@fs/
HTML paths (<a
href="https://redirect.github.com/vitejs/vite/issues/21762">#21762</a>)
(<a
href="47c4213f13">47c4213</a>)</li>
<li><strong>glob:</strong> error on relative glob in virtual module when
no files match (<a
href="https://redirect.github.com/vitejs/vite/issues/22497">#22497</a>)
(<a
href="5c8e98f8b5">5c8e98f</a>)</li>
<li><strong>optimizer:</strong> close the rolldown bundle when write()
rejects (<a
href="https://redirect.github.com/vitejs/vite/issues/22528">#22528</a>)
(<a
href="e3cfb9deec">e3cfb9d</a>)</li>
<li><strong>resolve:</strong> provide onWarn for viteResolvePlugin in JS
plugin containers (<a
href="https://redirect.github.com/vitejs/vite/issues/22509">#22509</a>)
(<a
href="40985f1c09">40985f1</a>)</li>
</ul>
<h3>Miscellaneous Chores</h3>
<ul>
<li><strong>deps:</strong> update rolldown-related dependencies (<a
href="https://redirect.github.com/vitejs/vite/issues/22566">#22566</a>)
(<a
href="3052a67d93">3052a67</a>)</li>
</ul>
<h3>Code Refactoring</h3>
<ul>
<li>correct logic in <code>collectAllModules</code> function (<a
href="https://redirect.github.com/vitejs/vite/issues/22562">#22562</a>)
(<a
href="6978a9ceb9">6978a9c</a>)</li>
</ul>
<h2><!-- raw HTML omitted --><a
href="https://github.com/vitejs/vite/compare/v8.0.13...v8.0.14">8.0.14</a>
(2026-05-21)<!-- raw HTML omitted --></h2>
<h3>Features</h3>
<ul>
<li>update rolldown to 1.0.2 (<a
href="https://redirect.github.com/vitejs/vite/issues/22484">#22484</a>)
(<a
href="96efc88570">96efc88</a>)</li>
</ul>
<h3>Bug Fixes</h3>
<ul>
<li><strong>deps:</strong> update all non-major dependencies (<a
href="https://redirect.github.com/vitejs/vite/issues/22471">#22471</a>)
(<a
href="98b8163213">98b8163</a>)</li>
<li><strong>dev:</strong> handle errors when sending messages to vite
server (<a
href="https://redirect.github.com/vitejs/vite/issues/22450">#22450</a>)
(<a
href="e8e9a34dcf">e8e9a34</a>)</li>
<li><strong>html:</strong> handle trailing slash paths in
transformIndexHtml (<a
href="https://redirect.github.com/vitejs/vite/issues/22480">#22480</a>)
(<a
href="5d94d1bffd">5d94d1b</a>)</li>
<li><strong>optimizer:</strong> pass oxc jsx options to transformSync in
dependency scan (<a
href="https://redirect.github.com/vitejs/vite/issues/22342">#22342</a>)
(<a
href="b3132dacea">b3132da</a>)</li>
</ul>
<h3>Miscellaneous Chores</h3>
<ul>
<li><strong>deps:</strong> update rolldown-related dependencies (<a
href="https://redirect.github.com/vitejs/vite/issues/22470">#22470</a>)
(<a
href="7cb728eb62">7cb728e</a>)</li>
<li>remove irrelevant commits from changelog (<a
href="2c69495f25">2c69495</a>)</li>
</ul>
<h3>Code Refactoring</h3>
<ul>
<li><strong>glob:</strong> do not rewrite import path for absolute base
(<a
href="https://redirect.github.com/vitejs/vite/issues/22310">#22310</a>)
(<a
href="0ae2844ab6">0ae2844</a>)</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="f94df87ff0"><code>f94df87</code></a>
release: v8.0.16</li>
<li><a
href="dc245c71e5"><code>dc245c7</code></a>
fix: reject windows alternate paths (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/22572">#22572</a>)</li>
<li><a
href="50b951225b"><code>50b9512</code></a>
fix(deps): reject UNC paths for launch-editor-middleware (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/22571">#22571</a>)</li>
<li><a
href="8d1b0195fd"><code>8d1b019</code></a>
release: v8.0.15</li>
<li><a
href="2686d7d0b7"><code>2686d7d</code></a>
fix(deps): update all non-major dependencies (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/22511">#22511</a>)</li>
<li><a
href="3052a67d93"><code>3052a67</code></a>
chore(deps): update rolldown-related dependencies (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/22566">#22566</a>)</li>
<li><a
href="e3cfb9deec"><code>e3cfb9d</code></a>
fix(optimizer): close the rolldown bundle when write() rejects (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/22528">#22528</a>)</li>
<li><a
href="6978a9ceb9"><code>6978a9c</code></a>
refactor: correct logic in <code>collectAllModules</code> function (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/22562">#22562</a>)</li>
<li><a
href="646dbedd28"><code>646dbed</code></a>
feat: update rolldown to 1.0.3 (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/22538">#22538</a>)</li>
<li><a
href="85a0eff1c8"><code>85a0eff</code></a>
fix: capitalize error messages and remove spurious space in parse error
(<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/22488">#22488</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/vitejs/vite/commits/v8.0.16/packages/vite">compare
view</a></li>
</ul>
</details>
<br />

Updates `form-data` from 4.0.5 to 4.0.6
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/form-data/form-data/blob/master/CHANGELOG.md">form-data's
changelog</a>.</em></p>
<blockquote>
<h2><a
href="https://github.com/form-data/form-data/compare/v4.0.5...v4.0.6">v4.0.6</a>
- 2026-06-12</h2>
<h3>Commits</h3>
<ul>
<li>[Fix] escape CR, LF, and <code>&quot;</code> in field names and
filenames <a
href="8dff42c6da"><code>8dff42c</code></a></li>
<li>[Dev Deps] update <code>@ljharb/eslint-config</code>,
<code>auto-changelog</code>, <code>tape</code> <a
href="f31d21ef10"><code>f31d21e</code></a></li>
<li>[Deps] update <code>hasown</code>, <code>mime-types</code> <a
href="92ae0eb5da"><code>92ae0eb</code></a></li>
<li>[Dev Deps] update <code>js-randomness-predictor</code> <a
href="67b0f65c2e"><code>67b0f65</code></a></li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="64190db548"><code>64190db</code></a>
v4.0.6</li>
<li><a
href="92ae0eb5da"><code>92ae0eb</code></a>
[Deps] update <code>hasown</code>, <code>mime-types</code></li>
<li><a
href="f31d21ef10"><code>f31d21e</code></a>
[Dev Deps] update <code>@ljharb/eslint-config</code>,
<code>auto-changelog</code>, <code>tape</code></li>
<li><a
href="8dff42c6da"><code>8dff42c</code></a>
[Fix] escape CR, LF, and <code>&quot;</code> in field names and
filenames</li>
<li><a
href="67b0f65c2e"><code>67b0f65</code></a>
[Dev Deps] update <code>js-randomness-predictor</code></li>
<li>See full diff in <a
href="https://github.com/form-data/form-data/compare/v4.0.5...v4.0.6">compare
view</a></li>
</ul>
</details>
<br />


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>
2026-06-16 23:07:34 -07:00
Mateus Borges
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.
2026-06-16 23:06:41 -07:00
JD Davis
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.
2026-06-16 23:05:15 -07:00
Tejas Chopra
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`).
2026-06-16 21:06:43 -07:00
Tejas Chopra
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
7cb0f43b); GitHub Actions CI run 27488990477 for the test shards
- Exact command / steps: rebased onto latest main (clean, 13 commits
replayed, 0 conflicts); ran the pytest suites and mypy above locally;
inspected CI shard logs to confirm the failure was the codecov upload,
not the test phase
- Observed result: 253 targeted tests pass locally; mypy clean on 365
files; CI test phase reports `1528 passed, 120 skipped`; the only red
step (codecov `upload-coverage` → "Token required because branch is
protected") is resolved by the rebased-in #968 CODECOV_TOKEN fix
- Not tested: the read-maturation live-API no-bust validation
(`tests/test_live/`) was not re-run in this rebase pass (requires
provider keys); it was validated when the feature first landed, and no
maturation code changed in the rebase — only CCR-default test assertions
and the duplicate-field resolution

## 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 is generated by release-please from the conventional commits,
so the CHANGELOG box is intentionally left unchecked. "Manual testing
performed" is unchecked deliberately — see `Real Behavior Proof` → `Not
tested` for the exact boundary (the live-API maturation validation was
not re-run in this rebase pass).

### Follow-ups (tracked, not in this PR)

- Mechanism B provider extensions: OpenAI-family wiring (no breakpoint
hold — bounded near-tail bust) and the Codex runtime read-detector (the
audit classifier is the prototype).
- Pilot enablement playbook: run `audit-reads --simulate-maturation` on
target traffic → pick `quiesce_turns` → enable via env → watch cache hit
rate + `read_maturation:N` transform tags.
2026-06-16 20:21:13 -07:00
JD Davis
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.
2026-06-16 19:11:45 -07:00
Eyal Mizrachi
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>
2026-06-16 21:11:12 -05:00
Eyal Mizrachi
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>
2026-06-16 21:10:01 -05:00
r00t
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.
2026-06-16 21:05:25 -05:00
r00t
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).
2026-06-16 20:48:48 -05:00
github-actions[bot]
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))
([e67ee2a](e67ee2af65))
* add dashboard agent usage stats
([#814](https://github.com/chopratejas/headroom/issues/814))
([6d3f39f](6d3f39f213))
* Add support for Mistral Vibe CLI
([#935](https://github.com/chopratejas/headroom/issues/935))
([0932b8b](0932b8bef4))
* attribute reread waste to over-compression via marker check
([#901](https://github.com/chopratejas/headroom/issues/901))
([f928576](f9285766dd))
* **bedrock:** cross-region + Converse compression; bundle proxy binary
in images ([#999](https://github.com/chopratejas/headroom/issues/999))
([0dc2e1c](0dc2e1cb3f))
* **dashboard:** surface compression-vs-cache net impact in Prefix Cache
panel ([#913](https://github.com/chopratejas/headroom/issues/913))
([2a4d300](2a4d300841))
* **evals:** adversarial-input robustness grid for compressors
([#918](https://github.com/chopratejas/headroom/issues/918))
([5939004](5939004185))
* **parser:** detect re-issued identical tool calls as reread waste
([#909](https://github.com/chopratejas/headroom/issues/909))
([7d4ae86](7d4ae86ec0))
* **policy:** batch deep edits through one cache-bust
([#856](https://github.com/chopratejas/headroom/issues/856) P3a)
([#1015](https://github.com/chopratejas/headroom/issues/1015))
([c2e52fe](c2e52fe743))
* **policy:** consume net-cost mutation gate in ContentRouter
([#856](https://github.com/chopratejas/headroom/issues/856) P2)
([#905](https://github.com/chopratejas/headroom/issues/905))
([553ade4](553ade4ec6))
* **proxy:** compress AWS Bedrock InvokeModel requests via configurable
upstream ([#720](https://github.com/chopratejas/headroom/issues/720))
([7edb27a](7edb27ab24))


### Bug Fixes

* **anthropic:** strip styled Claude model ids
([#651](https://github.com/chopratejas/headroom/issues/651))
([0c5c89d](0c5c89d05c))
* **anyllm:** forward openai api_base/api_key to the any-llm backend
([#942](https://github.com/chopratejas/headroom/issues/942))
([#954](https://github.com/chopratejas/headroom/issues/954))
([a7ee8a6](a7ee8a60a7))
* **cache:** guard None exemplar embeddings in dynamic detector
([#950](https://github.com/chopratejas/headroom/issues/950))
([1ec9320](1ec9320888))
* **cache:** name the missing piece in semantic detector guard
([#1018](https://github.com/chopratejas/headroom/issues/1018))
([3b0bcee](3b0bceecf4))
* **ci:** check out repo in PR Governance label job
([#1021](https://github.com/chopratejas/headroom/issues/1021))
([4558bc2](4558bc2465))
* **ci:** make PR governance advisory
([#1047](https://github.com/chopratejas/headroom/issues/1047))
([74dff94](74dff94fb8))
* **codex:** compute waste signals on the OpenAI Responses path
([#898](https://github.com/chopratejas/headroom/issues/898))
([b9e2761](b9e27614c6))
* **codex:** poll /wham/usage for subscription limits (handshake no
longer sends x-codex-* headers)
([#924](https://github.com/chopratejas/headroom/issues/924))
([8c00f71](8c00f7103c))
* **codex:** PR health label check state
([#986](https://github.com/chopratejas/headroom/issues/986))
([99c874d](99c874d423))
* **codex:** retag thread providers so history menu stays whole across
the proxy boundary
([#1034](https://github.com/chopratejas/headroom/issues/1034))
([74ae781](74ae781644))
* **codex:** write canonical hooks feature flag and migrate deprecated
codex_hooks ([#743](https://github.com/chopratejas/headroom/issues/743))
([dff6a19](dff6a19946))
* **compression:** convert tree-sitter byte offsets to char offsets
([#892](https://github.com/chopratejas/headroom/issues/892))
([b1f700f](b1f700fc27))
* **compression:** correct JSON array item counting and entropy gate
([#887](https://github.com/chopratejas/headroom/issues/887))
([d6f0f0f](d6f0f0f642))
* **compression:** keep container bodies compressible in code handler
([#890](https://github.com/chopratejas/headroom/issues/890))
([16ed73b](16ed73bca6))
* **compression:** measure short-value threshold on payload, not token
([#889](https://github.com/chopratejas/headroom/issues/889))
([65b0e8c](65b0e8c58d))
* **compression:** use thread-local tree-sitter parsers in code handler
([#893](https://github.com/chopratejas/headroom/issues/893))
([6cdb846](6cdb846200))
* **gemini:** surface functionResponse payloads to waste-signal
detection ([#897](https://github.com/chopratejas/headroom/issues/897))
([9b0c840](9b0c840dd7))
* **learn:** decode directory names with spaces in Windows project paths
([#997](https://github.com/chopratejas/headroom/issues/997))
([#1027](https://github.com/chopratejas/headroom/issues/1027))
([2d3701b](2d3701b59e))
* **learn:** scan subagent and workflow transcripts
([#1045](https://github.com/chopratejas/headroom/issues/1045))
([0ddd4ed](0ddd4ed9e9))
* **openclaw:** declare headroom_retrieve tool contract
([#947](https://github.com/chopratejas/headroom/issues/947))
([7c8c909](7c8c909c85))
* **policy:** correct warm-cache penalty in net_mutation_gain to (S +
dT) ([#903](https://github.com/chopratejas/headroom/issues/903))
([0632eba](0632eba6c3))
* **proxy:** add native Bedrock converse-stream route
([#917](https://github.com/chopratejas/headroom/issues/917))
([b08ec15](b08ec15b0d))
* **proxy:** keep codex image-generation WS turns alive through the
relay ([#1000](https://github.com/chopratejas/headroom/issues/1000))
([7dbbb40](7dbbb4077e))
* **proxy:** make budget enforcement actually work
([#885](https://github.com/chopratejas/headroom/issues/885))
([a14ab45](a14ab45cf0))
* **proxy:** read RTK gain stats globally by default
([#957](https://github.com/chopratejas/headroom/issues/957))
([b70fccb](b70fccbe17))
* route v1internal code assist requests to cloudcode-pa.googleapis…
([#821](https://github.com/chopratejas/headroom/issues/821))
([e20f16b](e20f16b1a6))
* **serena:** stop the Serena dashboard popup and make --no-serena
actually disable Serena
([#1003](https://github.com/chopratejas/headroom/issues/1003))
([919379a](919379a8a1))
* support Copilot Business subscription auth
([#641](https://github.com/chopratejas/headroom/issues/641))
([0b4a4bd](0b4a4bd483))
* wire HEADROOM_EXCLUDE_TOOLS / HEADROOM_TOOL_PROFILES into Click proxy
entrypoint ([#943](https://github.com/chopratejas/headroom/issues/943))
([9b7b436](9b7b436b04))
* **wrap:** avoid duplicate top-level keys when injecting codex provider
([#884](https://github.com/chopratejas/headroom/issues/884))
([dd22cfd](dd22cfd72a))


### Code Refactoring

* DRY cache logic, add thread safety, fix Bash exclusion
([#704](https://github.com/chopratejas/headroom/issues/704))
([e36fccd](e36fccd8cf))
</details>

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-16 15:35:00 -07:00
Dashsoap
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
2026-06-16 16:05:34 -05:00
Eyal Mizrachi
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>
2026-06-16 15:17:13 -05:00
gglucass
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>
2026-06-16 15:13:21 -05:00
dependabot[bot]
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>
2026-06-16 15:02:41 -05:00
Brian Toye
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>
2026-06-16 14:59:51 -05:00
Djabx
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 -->
2026-06-16 14:58:44 -05:00
rongabbay
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>
2026-06-16 14:56:16 -05:00
ulias
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>
2026-06-16 14:50:04 -05:00
AKT99!
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.
2026-06-16 14:47:45 -05:00
Tejas Chopra
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>
2026-06-16 12:20:37 -07:00
felixboenkost-droid
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>
2026-06-16 12:19:22 -07:00
JD Davis
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 `46f556ee`.
- Observed result: diff check, Ruff, commitlint, and Rust check passed
locally.
- Not tested: no focused pytest was run because the branch has no
remaining code/workflow diff against upstream/main after update; full
GitHub CI is running on the new head.

## 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

## Additional Notes

This PR is now effectively redundant against current `main` after the
merge update; maintainers can close or merge according to project
preference once governance/CI is green.
2026-06-16 12:09:52 -07:00
Umi_Ma
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>
2026-06-16 12:07:10 -07:00
JD Davis
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
2026-06-16 12:36:39 -05:00
Zhenjia ZHOU
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>
2026-06-16 10:07:49 -05:00
Yasser Sheikh
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.
2026-06-16 09:45:24 -05:00
Dashsoap
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".
2026-06-16 09:43:08 -05:00
Focused Instability
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.
2026-06-15 23:30:23 -05:00
Shengbo_Wang
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.
2026-06-15 23:29:53 -05:00
Andrew Barnes
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.
2026-06-15 22:07:31 -05:00
Eyal Mizrachi
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>
2026-06-15 21:38:14 -05:00
RAPHAEL LUGO
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
2026-06-15 16:52:26 -05:00
Ashish
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>
2026-06-15 16:38:08 -05:00
Focused Instability
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`
(7c8c909c) `test_none_exemplars_early_return` fails with `assert
'semantic detector is not initialized' == 'exemplar embeddings not
initialized'`. After fix: full file 38 passed, full `tests/test_cache/`
198 passed.
- Observed result: model-present + exemplar-None now returns `(spans=[],
"exemplar embeddings not initialized")`; model-None still returns the
generic message; `np.dot` is never reached with a None matrix.
- Not tested: live model load / real embeddings — the guards are the
unavailable-state paths, exercised via the existing mock-based unit
tests.

## Review Readiness

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

## Additional Notes

This takes the **specific-message** direction because it matches #950's
newest test, the original (now-dead) specific guard string, and gives a
more actionable warning. The conservative **alternative** — keep the
generic unified message, delete the dead specific guard, and update
`test_none_exemplars_early_return` to assert the generic string — also
turns CI green with no production behavior change. Happy to switch to
that if you prefer; it's your call on the intended contract.
2026-06-15 16:29:52 -05:00
Tim Poppe
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 -->
2026-06-15 11:12:26 -05:00
Dashsoap
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.
2026-06-15 11:09:59 -05:00
Ashish
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>
2026-06-15 11:08:33 -05:00
Aniruddh Jha
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.
2026-06-15 11:07:43 -05:00
Focused Instability
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).
2026-06-15 11:06:28 -05:00
wangxiangyu7
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>
2026-06-15 11:04:29 -05:00
Ashish
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>
2026-06-15 10:26:57 -05:00
Ashish
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>
2026-06-15 10:25:57 -05:00
Ashish
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>
2026-06-15 10:24:21 -05:00
Ashish
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>
2026-06-15 10:23:26 -05:00
Ashish
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
`120696e5`) was superseded by an equivalent block the maintainer added
to `main`; the merge from main resolved to main's version. Codecov now
reports all modified lines covered.
- N/A checklist items: no docs or CHANGELOG entry — this is an internal
correctness fix to an existing flag.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-15 10:22:27 -05:00