Commit graph

152 commits

Author SHA1 Message Date
JD Davis
cbfa267c5f
fix(deps): enforce transformers security floor (#2201)
## Description

Raise the production `transformers` dependency floor so the security
workflow cannot resolve the CVE-2026-5241 vulnerable range reported by
`pip-audit`, and refresh the small current-main test fixtures needed for
the PR matrix to run green.

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

- Raised direct optional `transformers` declarations for `proxy`, `ml`,
and `voice` extras to `>=5.5.0,<6.0`.
- Refreshed `uv.lock` metadata so `uv export --extra all` resolves a
patched `transformers` version for the production audit set.
- Kept the current-main test fixture fixes for the ZCode setup printer
and deferred compression fallback metrics.

## 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
$ uv lock --check
Resolved 238 packages in 1ms

$ uv export --frozen --no-dev --no-emit-project --no-hashes --extra all --format requirements-txt | rg "^transformers==|^huggingface-hub=="
huggingface-hub==1.16.1
transformers==5.13.1

$ uv export --frozen --no-dev --no-emit-project --no-hashes --extra all --format requirements-txt > requirements-prod.txt
$ uvx pip-audit -r requirements-prod.txt
No known vulnerabilities found

$ uv run --with pytest --with pytest-asyncio --with fastapi --with httpx --with numpy pytest tests/test_cli/test_wrap_zcode.py::test_wrap_prints_proxy_urls tests/test_cold_start_fast_pass.py::test_fast_pass_failure_falls_back_to_full_deferral -q
2 passed

$ uvx ruff==0.15.17 check headroom/cli/wrap.py headroom/proxy/handlers/anthropic.py --output-format concise
All checks passed!

$ git diff --check
passed
```

## Real Behavior Proof

- Environment: Windows checkout plus the same frozen production
dependency export shape used by the GitHub Actions security workflow.
- Exact command / steps: raised the `transformers` floor, refreshed
`uv.lock`, exported `--extra all` production requirements, ran
`pip-audit`, then reproduced the focused ZCode and deferred-compression
tests.
- Observed result: the export resolves `transformers==5.13.1`;
`pip-audit` reported no known vulnerabilities; the focused tests pass
locally; CI is rerunning on the updated head.
- Not tested: full GitHub Actions matrix locally; CI is running the
complete suite on this PR.

## 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
- [ ] 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 - dependency metadata and CI fixture fix.

## Additional Notes

This PR is intentionally scoped to clearing the current red mainline
security gate while keeping the small fixture updates needed by the
branch test matrix.
2026-07-15 11:50:53 -05:00
JD Davis
ea3d5a86b7
fix(deps): clear Dependabot lockfile alerts (#2175)
## Description

Clears the current dependency/security-audit blockers that are making
unrelated PRs red:

- `transformers 5.3.0` / `CVE-2026-5241`, fixed by requiring
`transformers>=5.5.0` in the locked optional dependency set.
- `sqlitedict <=2.1.0` via the optional `benchmark` extra's
`lm-eval[api]` dependency. There is no patched `sqlitedict` release, so
this PR removes the published/locked `benchmark` extra instead of
shipping a known-vulnerable transitive dependency.
- `esbuild >=0.27.3,<0.28.1` in the OpenCode plugin lockfile, fixed by
forcing `esbuild@0.28.1` through the OpenCode npm override and
regenerated lockfile.

The benchmark code still invokes `python -m lm_eval`; researchers who
need that harness should install `lm-eval[api]` in their benchmark
environment until its transitive vulnerability has a patched release.

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

- `pyproject.toml`: remove the `benchmark` optional extra, document
external `lm-eval[api]` installation guidance, and require
`transformers>=5.5.0`.
- `uv.lock`: regenerate without the `benchmark` extra, removing
`lm-eval` and `sqlitedict` lock entries and locking the patched
transformers floor.
- `plugins/opencode/package.json`: add an `overrides` entry for
`esbuild@0.28.1`.
- `plugins/opencode/package-lock.json`: regenerate the OpenCode lockfile
with `esbuild@0.28.1`.

## 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
uv lock --check
rg -n -F 'sqlitedict' uv.lock        # no matches
rg -n -F 'name = "lm-eval"' uv.lock  # no matches
rg -n -F "extra == 'benchmark'" uv.lock # no matches
rg -n -F '0.27.7' plugins/opencode/package-lock.json plugins/opencode/package.json # no matches
npm ls esbuild --package-lock-only
npm audit --package-lock-only        # found 0 vulnerabilities
git diff --check
```

Previous GitHub checks were green. After merging current `main`, fresh
GitHub checks are running again; local targeted validation still passes.

## Real Behavior Proof

- Environment: Windows 11, Python 3.13.3, uv, npm in `plugins/opencode`,
Dependabot/pip-audit alert metadata from the failing PR jobs.
- Exact command / steps: inspected the regenerated Python and npm
lockfiles with `rg`, checked the uv lock with `uv lock --check`, checked
OpenCode's dependency tree with `npm ls esbuild --package-lock-only`,
and ran `npm audit --package-lock-only`.
- Observed result: `uv.lock` no longer contains `sqlitedict`, `lm-eval`,
or a `benchmark` extra marker; `transformers` resolves at the patched
`>=5.5.0` floor; OpenCode's lock resolves `esbuild@0.28.1`; `npm audit
--package-lock-only` reports 0 vulnerabilities; GitHub `Dependency audit
(pip-audit)` passes.
- Not tested: running the external `lm-eval` harness after installing it
separately.

## 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
- [x] I have updated the CHANGELOG.md if applicable

## Screenshots (if applicable)

N/A - dependency and lockfile security fix.

## Additional Notes

The `benchmark` extra can be restored once the upstream `lm-eval[api]`
dependency chain stops pulling a vulnerable `sqlitedict` release.

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-14 20:40:28 -07:00
Rod Boev
4ea96a417c
feat(mcp): add streamable HTTP MCP transport (#1773)
## Description

`headroom mcp serve` only exposed stdio, which blocked MCP clients that
require a Streamable HTTP endpoint. This PR adds an explicit HTTP
transport mode around the existing Headroom MCP server while keeping
stdio as the default and keeping tool registration single-sourced.

Closes #1346.

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

- Add `headroom mcp serve --transport http` with host, port, and path
options.
- Serve `headroom_compress`, `headroom_retrieve`, and `headroom_stats`
through the same MCP server instance used by stdio.
- Keep `headroom mcp serve` defaulting to stdio for current Claude Code
and local MCP host configs.
- Update MCP docs for stdio and HTTP setup without implying the proxy
automatically owns `/mcp`.
- Keep the scope clean, rebased, and covered by focused tests.

## Testing

- [x] Unit tests pass (`uv run pytest tests/test_ccr_mcp_http.py
tests/test_cli/test_mcp.py -q`)
- [x] Linting passes (`uv run ruff check headroom/cli/mcp.py
headroom/ccr/mcp_server.py headroom/ccr/mcp_http.py
tests/test_ccr_mcp_http.py tests/test_cli/test_mcp.py`)
- [x] Type checking passes (`uv run mypy headroom
--ignore-missing-imports`)
- [x] New tests added for new functionality when applicable
- [x] Manual testing performed

### Test Output

```text
uv run pytest tests/test_ccr_mcp_http.py tests/test_cli/test_mcp.py -q
20 passed in 0.53s

uv run ruff check headroom/cli/mcp.py headroom/ccr/mcp_server.py headroom/ccr/mcp_http.py tests/test_ccr_mcp_http.py tests/test_cli/test_mcp.py
All checks passed!

uv run ruff format headroom/cli/mcp.py headroom/ccr/mcp_server.py headroom/ccr/mcp_http.py tests/test_ccr_mcp_http.py tests/test_cli/test_mcp.py --check
5 files already formatted

uv run mypy headroom --ignore-missing-imports
Success: no issues found in 407 source files
```

## Real Behavior Proof

- Environment: Local Python environment with Headroom dev dependencies
and MCP extra installed.
- Exact command / steps: Start `headroom mcp serve --transport http
--host 127.0.0.1 --port <test-port> --path /mcp`, then perform an MCP
SDK Streamable HTTP initialize/list-tools exchange.
- Observed result: The HTTP transport initializes and lists the existing
Headroom MCP tools; `headroom mcp serve` without `--transport` still
selects stdio, and mixed-case `--transport HTTP` routes to the HTTP
transport.
- Not tested: live validation against external MCP hosts

## Review Readiness

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

## Additional Notes

`CHANGELOG.md` is not edited because this repository generates changelog
entries from conventional commits. Full-suite validation is left to CI.
2026-07-14 13:25:45 -04:00
JD Davis
fce93bf39a
fix(ci/deps): clear audit and release smoke failures (#2190)
## Summary
- add a uv constraint floor for `setuptools>=83.0.0` to address
`PYSEC-2026-3447`
- refresh `uv.lock` so the production audit export resolves with
`setuptools 83.0.0`
- harden the Release wheel smoke-import gate by retrying
Ubuntu-container `apt-get` operations and using `--fix-missing`
- keep the generated `requirements-prod.txt` uncommitted; it is produced
by the security workflow

## Why
This clears the new dependency audit alert that made PRs red:

- `setuptools 80.10.2`
- `PYSEC-2026-3447`
- fixed in `83.0.0`

While validating the queue, the same PR class also hit a Release
smoke-import failure in the Ubuntu 22.04 ARM container due apt mirror
skew:

`E: Failed to fetch ... python3-httplib2_0.20.2-2ubuntu0.1_all.deb 404
Not Found`

The smoke gate should still fail for broken wheels, but transient apt
mirror skew should not make unrelated PRs red.

## Lockfile impact
- `setuptools 80.10.2 -> 83.0.0`
- `torch 2.12.1 -> 2.13.0`, required for pip resolver compatibility with
`setuptools 83.0.0` in the exported audit set
- `cuda-toolkit 13.0.2 -> 13.0.3.0`, pulled by the torch lock refresh
- uv also refreshed the existing project metadata for the sandbox extra
so `uv lock --check` passes

## Validation
- `uv lock --check`
- `uv export --frozen --no-dev --no-emit-project --no-hashes --extra all
--format requirements-txt > requirements-prod.txt`
- confirmed generated `requirements-prod.txt` contains
`setuptools==83.0.0`, `torch==2.13.0`, `cuda-toolkit==13.0.3.0`
- `uvx pip-audit -r requirements-prod.txt` -> No known vulnerabilities
found
- `python -m pytest tests/test_release_workflows.py -q` -> 32 passed
- `uvx ruff@0.15.17 check tests/test_release_workflows.py` -> All checks
passed
- `git diff --check`
2026-07-14 11:43:23 -04:00
Tejas Chopra
b6eb7a7613
feat(kompress): optional remote compression endpoint (HEADROOM_KOMPRESS_ENDPOINT) (#2171)
## Description

Adds an **opt-in remote Kompress backend** so the proxy can offload
Kompress ML inference to a hosted `/compress` endpoint instead of
loading the ONNX model in-process.

This lets Headroom run as a lean proxy in a sandbox installed with only
`[proxy]` deps while the model runs elsewhere. The feature is purely
additive: with `HEADROOM_KOMPRESS_ENDPOINT` unset, behavior remains the
existing in-process Kompress path.

## Type of Change

- [x] New feature (non-breaking change that adds functionality)

## Changes Made

- `headroom/transforms/kompress_remote.py`: adds
`RemoteKompressCompressor`, a `KompressCompressor`-compatible HTTP
client that posts to `/compress`, sends optional bearer auth, skips
network for tiny inputs, and fails open on
HTTP/network/malformed-response errors.
- `headroom/transforms/kompress_compressor.py`: extracts
`store_kompress_in_ccr()` so the remote client reuses the same
proxy-local CCR marker/storage policy without importing the ML model.
- `headroom/transforms/content_router.py`: selects the remote compressor
when `HEADROOM_KOMPRESS_ENDPOINT` is set, while `"disabled"` still wins
and the unset path remains local Kompress.
- `tests/test_transforms/test_kompress_remote.py`: covers mocked remote
success, auth/header/request behavior, tiny-input no-call behavior, HTTP
fail-open, malformed-success fail-open, and router env selection.

## Testing

- [x] Unit tests pass (`uv run --extra dev pytest
tests/test_transforms/test_kompress_remote.py -q`)
- [x] Linting passes (`uvx ruff@0.15.17 check
headroom/transforms/kompress_remote.py
headroom/transforms/kompress_compressor.py
headroom/transforms/content_router.py
tests/test_transforms/test_kompress_remote.py`)
- [x] Formatting passes (`uvx ruff@0.15.17 format --check
headroom/transforms/kompress_remote.py
headroom/transforms/kompress_compressor.py
headroom/transforms/content_router.py
tests/test_transforms/test_kompress_remote.py`)
- [x] Type checking passes (`uv run --extra dev mypy
headroom/transforms/kompress_remote.py
headroom/transforms/kompress_compressor.py
headroom/transforms/content_router.py`)
- [x] New tests added for new functionality
- [x] Manual testing performed by the author against a live endpoint

## Real Behavior Proof

- Environment: Windows 11 review worktree, Python 3.13.3 for mocked
tests; author also manually tested against a Modal deployment of
`chopratejas/kompress-v2-base`.
- Exact command / steps: ran the focused mocked endpoint test file plus
lint/format/mypy on the changed modules.
- Observed result: remote success maps endpoint response into
`KompressResult`; short inputs do not call the network; 503 responses
and malformed 200 responses return the original content; router selects
the remote compressor only when the env var is set.
- Not tested: full `pytest` suite; production concurrency/latency under
load; endpoints other than the author's Modal reference deployment.

## 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 — follow-up
README flag section
- [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 endpoint/deploy artifact (`modal_serve.py`) lives in the separate
`kompress` repo; this PR is only the client-side flag.
- The endpoint is intentionally stateless for CCR. Original-content
storage and retrieval markers remain proxy-local.
- Design note: this capability is intentionally in OSS as an opt-in
flag. The same flag serves self-hosted endpoints and, later, a hosted
endpoint.

---------

Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-13 22:01:11 -07:00
Rod Boev
e9000863fc
fix(kompress): fail-open wall-clock guard on single-cache-miss compression (#2114)
## Description

The single-cache-miss branch in `ContentRouter` ran compression inline
on the request path without its own wall-clock guard, so a cooperative
stall waited for the full call even when
`HEADROOM_COMPRESSION_DEADLINE_MS` was meant to fail open. This change
adds a branch-level watchdog that returns `PASSTHROUGH` after the
deadline, scoped only to the one-pending-task path and not the native
GIL-hold root cause.

Closes #2046

## 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 `_compression_deadline_seconds()` and a watchdog around the
single-cache-miss inline compression branch in `ContentRouter`.
- Returned the original content with `PASSTHROUGH` and logged a
fail-open warning after the configured deadline, while preserving
under-deadline and deadline-disabled behavior.
- Added focused regressions for timeout, under-deadline output, and
disabled-deadline behavior, then kept the wider deadline suite green.
- Raised the locked production floors for `click` and `pillow` to clear
the current `pip-audit` findings that now fail external PR merge
snapshots.

## Testing

- [x] Unit tests pass (`uv run pytest
tests/test_content_router_single_item_deadline.py
tests/test_transforms/test_kompress_deadline.py -q`)
- [x] Linting passes (`uv run ruff check
headroom/transforms/content_router.py
tests/test_content_router_single_item_deadline.py
tests/test_transforms/test_kompress_deadline.py`)
- [ ] Type checking passes (`uv run mypy headroom`)
- [x] New tests added for new functionality
- [x] Manual testing performed

### Test Output

```text
uv run pytest tests/test_content_router_single_item_deadline.py tests/test_transforms/test_kompress_deadline.py -q

============================= test session starts =============================
platform win32 -- Python 3.12.13, pytest-9.0.3, pluggy-1.6.0
rootdir: D:\Repos\headroom-pr-2046-compression-freeze
configfile: pyproject.toml
plugins: anyio-4.12.1, langsmith-0.9.3, asyncio-1.3.0, cov-7.0.0
asyncio: mode=Mode.AUTO, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function
collected 5 items

tests\test_content_router_single_item_deadline.py ...                    [ 60%]
tests\test_transforms\test_kompress_deadline.py ..                       [100%]

============================== 5 passed in 0.42s ==============================

uv run ruff check headroom/transforms/content_router.py tests/test_content_router_single_item_deadline.py tests/test_transforms/test_kompress_deadline.py
All checks passed!

uv run ruff format headroom/transforms/content_router.py tests/test_content_router_single_item_deadline.py tests/test_transforms/test_kompress_deadline.py --check
3 files already formatted
```

## Real Behavior Proof

- Environment: Windows, Python 3.12.13, router-level harness with a
cooperative slow-compression stub
- Exact command / steps: `uv run pytest
tests/test_content_router_single_item_deadline.py
tests/test_transforms/test_kompress_deadline.py -q`, which forces one
frozen prefix and one cache miss, then sleeps past a 10 ms deadline
- Observed result: the guarded branch returns the original content
through `PASSTHROUGH` at the deadline, while under-deadline and
deadline-disabled behavior stay unchanged
- Not tested: native GIL-holding freeze

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

Not applicable.

## Additional Notes

- Security CI currently flags `click 8.3.1` and `pillow 12.2.0`, so this
PR carries the narrow floor bump to `click>=8.3.3` and `pillow>=12.3.0`
as a supply-chain unblock for the same final merge snapshot.
- `CHANGELOG.md` remains untouched because Headroom generates release
notes from conventional commits.
- This PR is a Python-side mitigation for the single-cache-miss branch
only; the native GIL-hold root cause remains a separate owner.

---------

Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-13 23:50:49 -04:00
JD Davis
09be107d06
fix(deps): raise transformers security floor
Raise the production transformers floor to a version fixed for CVE-2026-5241 and refresh uv.lock so pip-audit passes.
2026-07-14 02:14:13 +00:00
牧濑红莉栖(BOT)
09d1ef45be
fix(proxy): compress Hermes scoped coding-agent passthrough (#1815)
## Description

Compress Hermes Studio scoped coding-agent passthrough requests in the
generic OpenAI passthrough handler. Hermes can route scoped Claude Code
and Codex traffic through Headroom while preserving its own proxy paths;
this PR keeps Hermes responsible for scoped proxy
authentication/provider adaptation while still applying Headroom
compression to supported chat payloads before forwarding.

The compression remains narrow-scoped:
- Only chat messages with `user` or `assistant` roles are compressed.
- Tool, function, reasoning, and system items are preserved byte-stable.
- Non-dict items in the Responses `input` array are preserved and
spliced back.

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

- Detect `/api/codex-proxy/.../v1/responses` paths and compress
supported Responses `input` chat items before forwarding.
- Detect `/api/claude-code-proxy/.../v1/messages` paths and compress
supported Anthropic `messages` payloads before forwarding.
- Preserve bypass, malformed payload, missing-model, tool/function,
reasoning/system, and non-dict passthrough behavior.
- Add regression coverage in
`tests/test_hermes_passthrough_compression.py`.

## Testing

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

### Test Output

```text
$ python -m pytest tests/test_hermes_passthrough_compression.py -v
test_codex_proxy_preserves_tool_and_function_items PASSED
test_codex_proxy_preserves_nondict_items PASSED
test_codex_proxy_bypass_header_skips_compression PASSED
test_codex_proxy_malformed_input_preserved PASSED
test_codex_proxy_compression_applies_to_chat_messages PASSED
test_claude_proxy_preserves_tool_use_items PASSED
test_claude_proxy_bypass_header_skips_compression PASSED
test_claude_proxy_no_model_forwarded_unchanged PASSED
test_claude_proxy_compression_applies_to_chat_messages PASSED
test_non_hermes_routes_not_affected PASSED
```

## Real Behavior Proof

- Environment: Author-reported local test environment for
`headroom/proxy/handlers/openai.py` and
`tests/test_hermes_passthrough_compression.py`.
- Exact command / steps: `python -m pytest
tests/test_hermes_passthrough_compression.py -v`.
- Observed result: The 10 Hermes passthrough regression tests passed,
covering Codex and Claude scoped proxy routes plus preservation/bypass
cases.
- Not tested: End-to-end Hermes Studio traffic against a live upstream
service is not covered by this PR body evidence.

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

Generated with Claude Code. The unchecked checklist items are not
required for this narrow proxy-handler test change.

---------

Co-authored-by: x1051445024 <你的GitHub注册邮箱>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-13 17:22:46 -05:00
石岳峰
4f3d5ab341
fix(install): add orjson to [proxy] extra for LiteLLM provider backends (#2074)
## Description
Add `orjson` to the `[proxy]` extra so `uv tool install
"headroom-ai[all]"` installs a runtime dependency required by LiteLLM
provider backends (e.g. OpenRouter).

## Type of Change
- [x] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature
- [ ] Breaking change
- [ ] Documentation update

## Changes Made
- Add `orjson>=3.9.14; platform_python_implementation != 'PyPy'` to
`[proxy]`.
- Regression test in `tests/test_optional_dependencies.py`.
- Minimal `uv.lock` update (proxy/all optional-deps + metadata only).

## Motivation
Fixes #2056. `headroom-ai[all]` installs `litellm` (core dep) but not
`orjson`. LiteLLM provider backends import `orjson` at runtime; LiteLLM
only declares it under `litellm[proxy]`, not base deps. Headroom does
not depend on `litellm[proxy]` (would pull the full LiteLLM proxy server
stack).

## Testing
- `uv run --extra dev python -m pytest
tests/test_optional_dependencies.py -q` — 2 passed
- `uvx --from ruff==0.15.17 ruff check
tests/test_optional_dependencies.py`
- `uvx --from ruff==0.15.17 ruff format --check
tests/test_optional_dependencies.py`

## Real Behavior Proof
- **Setup:** Ubuntu, Python 3.12.3
- **Verified:** dependency graph test confirms `orjson` is selected for
`[proxy]` and `[all]` extras after this patch.
- **Not tested locally:** full `uv tool install` end-to-end (local sdist
build requires Rust/C++ toolchain unavailable in this environment).
Reporter workaround `uv tool install ... --with orjson` confirms the
missing transitive dep diagnosis.

## Review Readiness
- [x] I have performed a self-review of my code
- [x] This PR is ready for human review

## Notes
- Reporter used Python 3.14.4; `litellm` is intentionally skipped on
3.14 (GH #956). This PR fixes the missing `orjson` install path for
supported Python versions using `[all]`.

Co-authored-by: syf2211 <syf2211@users.noreply.github.com>
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
Co-authored-by: Tejas Chopra <chopratejas@gmail.com>
2026-07-13 09:37:28 -04:00
dependabot[bot]
ce3c959eae
deps: update tree-sitter requirement from <0.26,>=0.25.2 to >=0.25.2,<0.27 (#1681)
Updates the requirements on
[tree-sitter](https://github.com/tree-sitter/py-tree-sitter) to permit
the latest version.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/tree-sitter/py-tree-sitter/releases">tree-sitter's
releases</a>.</em></p>
<blockquote>
<h2>v0.26.0</h2>
<h2>What's Changed</h2>
<ul>
<li>ci: use windows-2025 &amp; macos-15-intel runners by <a
href="https://github.com/ObserverOfTime"><code>@​ObserverOfTime</code></a>
in <a
href="https://redirect.github.com/tree-sitter/py-tree-sitter/pull/422">tree-sitter/py-tree-sitter#422</a></li>
<li>ci: bump pypa/cibuildwheel from 3.1 to 3.2 in the actions group by
<a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/tree-sitter/py-tree-sitter/pull/418">tree-sitter/py-tree-sitter#418</a></li>
<li>ci: bump the actions group with 2 updates by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/tree-sitter/py-tree-sitter/pull/424">tree-sitter/py-tree-sitter#424</a></li>
<li>ci: bump pypa/cibuildwheel from 3.2 to 3.3 in the actions group by
<a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/tree-sitter/py-tree-sitter/pull/426">tree-sitter/py-tree-sitter#426</a></li>
<li>ci: bump the actions group across 1 directory with 3 updates by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/tree-sitter/py-tree-sitter/pull/430">tree-sitter/py-tree-sitter#430</a></li>
<li>feat!: update API for tree-sitter 0.26 by <a
href="https://github.com/ObserverOfTime"><code>@​ObserverOfTime</code></a>
in <a
href="https://redirect.github.com/tree-sitter/py-tree-sitter/pull/431">tree-sitter/py-tree-sitter#431</a></li>
<li>Add Python 3.14 to CI workflow matrix by <a
href="https://github.com/cclauss"><code>@​cclauss</code></a> in <a
href="https://redirect.github.com/tree-sitter/py-tree-sitter/pull/434">tree-sitter/py-tree-sitter#434</a></li>
<li>ci: add riscv64 wheels to PyPI release workflow by <a
href="https://github.com/gounthar"><code>@​gounthar</code></a> in <a
href="https://redirect.github.com/tree-sitter/py-tree-sitter/pull/443">tree-sitter/py-tree-sitter#443</a></li>
<li>ci: bump the actions group across 1 directory with 5 updates by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/tree-sitter/py-tree-sitter/pull/446">tree-sitter/py-tree-sitter#446</a></li>
<li>fix type hints for Query properties by <a
href="https://github.com/unawarez"><code>@​unawarez</code></a> in <a
href="https://redirect.github.com/tree-sitter/py-tree-sitter/pull/439">tree-sitter/py-tree-sitter#439</a></li>
<li>build: bump tree_sitter/core from <code>cd4b6e2</code> to
<code>6f2e8a6</code> by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/tree-sitter/py-tree-sitter/pull/447">tree-sitter/py-tree-sitter#447</a></li>
<li>build: bump tree_sitter/core from <code>6f2e8a6</code> to
<code>cd5b087</code> by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/tree-sitter/py-tree-sitter/pull/449">tree-sitter/py-tree-sitter#449</a></li>
<li>build: bump tree-sitter-rust from 0.24.0 to 0.24.1 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/tree-sitter/py-tree-sitter/pull/444">tree-sitter/py-tree-sitter#444</a></li>
<li>ci: bump actions/upload-pages-artifact from 4 to 5 in the actions
group by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/tree-sitter/py-tree-sitter/pull/452">tree-sitter/py-tree-sitter#452</a></li>
<li>build: bump tree_sitter/core from <code>cd5b087</code> to
<code>7f53486</code> by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/tree-sitter/py-tree-sitter/pull/459">tree-sitter/py-tree-sitter#459</a></li>
<li>ci: bump the actions group across 1 directory with 2 updates by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/tree-sitter/py-tree-sitter/pull/463">tree-sitter/py-tree-sitter#463</a></li>
<li>build: bump tree-sitter-rust from 0.24.1 to 0.24.2 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/tree-sitter/py-tree-sitter/pull/453">tree-sitter/py-tree-sitter#453</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/cclauss"><code>@​cclauss</code></a> made
their first contribution in <a
href="https://redirect.github.com/tree-sitter/py-tree-sitter/pull/434">tree-sitter/py-tree-sitter#434</a></li>
<li><a href="https://github.com/gounthar"><code>@​gounthar</code></a>
made their first contribution in <a
href="https://redirect.github.com/tree-sitter/py-tree-sitter/pull/443">tree-sitter/py-tree-sitter#443</a></li>
<li><a href="https://github.com/unawarez"><code>@​unawarez</code></a>
made their first contribution in <a
href="https://redirect.github.com/tree-sitter/py-tree-sitter/pull/439">tree-sitter/py-tree-sitter#439</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/tree-sitter/py-tree-sitter/compare/v0.25.2...v0.26.0">https://github.com/tree-sitter/py-tree-sitter/compare/v0.25.2...v0.26.0</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="a9e753ef67"><code>a9e753e</code></a>
ci(pypi): skip riscv64 tests properly</li>
<li><a
href="eeababc529"><code>eeababc</code></a>
chore: release 0.26.0</li>
<li><a
href="dac834eca3"><code>dac834e</code></a>
fix(node): fix reference leak</li>
<li><a
href="bdddb6180d"><code>bdddb61</code></a>
build: bump tree-sitter-rust from 0.24.1 to 0.24.2</li>
<li><a
href="baa5fd8e27"><code>baa5fd8</code></a>
ci: bump the actions group across 1 directory with 2 updates</li>
<li><a
href="c680e3b513"><code>c680e3b</code></a>
build: bump tree_sitter/core from <code>cd5b087</code> to
<code>7f53486</code></li>
<li><a
href="2d3fb3a2a7"><code>2d3fb3a</code></a>
ci: bump actions/upload-pages-artifact from 4 to 5 in the actions
group</li>
<li><a
href="bae0829cea"><code>bae0829</code></a>
build: bump tree-sitter-rust from 0.24.0 to 0.24.1</li>
<li><a
href="d99f79601f"><code>d99f796</code></a>
build: bump tree_sitter/core from <code>6f2e8a6</code> to
<code>cd5b087</code></li>
<li><a
href="a9282df035"><code>a9282df</code></a>
build: bump tree_sitter/core from <code>cd4b6e2</code> to
<code>6f2e8a6</code></li>
<li>Additional commits viewable in <a
href="https://github.com/tree-sitter/py-tree-sitter/compare/v0.25.2...v0.26.0">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 this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-11 10:38:04 -05:00
Ben Younes
fd0d29c92d
fix(packaging): guard torch extras on intel macos (#2011)
## Description

Closes #1931

Guard the `ml` and `voice` `torch` optional dependencies on macOS x86_64
so `headroom-ai[all]` remains resolvable on Intel Macs where PyTorch
does not publish compatible wheels for this version floor. The lockfile
metadata is updated with the same markers.

## Type of Change

- [x] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to not work as expected)
- [ ] Documentation update
- [ ] Refactoring
- [ ] Performance improvement
- [ ] Test update
- [ ] Other

## Changes Made

- Added macOS x86_64 environment markers to `torch` in the `ml` and
`voice` extras.
- Updated `uv.lock` optional dependency metadata to match the guarded
extras.
- Added a packaging regression test that checks `[all]` keeps `ml` and
`voice` while guarding `torch` on macOS x86_64.

## Testing

- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check`)
- [x] Formatting verified (`ruff format --check`)
- [ ] Manual testing performed

### Test Output

```text
$ python3 -m pytest tests/test_optional_dependencies.py -q
collected 1 item

tests/test_optional_dependencies.py .                                    [100%]

============================== 1 passed in 0.26s ===============================

$ .venv/bin/ruff check tests/test_optional_dependencies.py
All checks passed!

$ .venv/bin/ruff format --check tests/test_optional_dependencies.py pyproject.toml
1 file already formatted
```

## Test verification (RED -> GREEN)

RED, with the `torch` markers temporarily removed from `pyproject.toml`:

```text
tests/test_optional_dependencies.py F                                    [100%]
FAILED tests/test_optional_dependencies.py::test_all_extra_does_not_require_torch_on_macos_x86_64
E   assert False
```

GREEN, with this patch applied:

```text
tests/test_optional_dependencies.py .                                    [100%]
============================== 1 passed in 0.26s ===============================
```

## Real Behavior Proof

- Environment: Linux, Python 3.12.3, pytest 9.1.1, ruff 0.14.14.
- Exact command / steps: Removed the environment markers from `torch`,
ran the new packaging test, restored the markers, and reran the test
plus targeted ruff checks.
- Observed result: The test fails without the macOS x86_64 guard and
passes once the `ml` and `voice` `torch` requirements are guarded.
- Not tested: Full `uv run pytest`, full-project `uv run ruff check .`,
full-project `uv run ruff format --check .`, and `uv run mypy headroom`
were not run locally for this targeted packaging change.

## Review Readiness

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

## Checklist

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have added tests that prove my fix is effective
- [x] New and existing targeted tests pass locally with my changes
- [x] Any dependent changes have been merged and published in downstream
modules

## Screenshots (if applicable)

N/A

## Additional Notes

No new dependency is added; this only narrows when the existing `torch`
optional dependency is selected.
2026-07-11 10:20:33 -05:00
OrbisAI Security
28ca61fc9d
fix: patch nltk vulnerability (CVE-2026-54293) (#1929)
## Description

Updates the locked `nltk` package from 3.9.4 to 3.10.0 to address
CVE-2026-54293, reported by OrbisAI Security as an information
disclosure/path traversal issue in `nltk.data.load()`.

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

- Updated the `nltk` lockfile entry from 3.9.4 to 3.10.0.
- Added the new locked `defusedxml` dependency required by `nltk`
3.10.0.
- Added an explicit `nltk>=3.10.0` uv constraint so future lock
refreshes cannot regress below the fixed version.
- Updated the benchmark-extra comment now that the nltk CVE has an
upstream fixed release.

## 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
uv lock --locked
Resolved 257 packages in 1ms

uv run --extra benchmark python -c "import importlib.metadata as md; print('lm-eval', md.version('lm-eval')); print('rouge-score', md.version('rouge-score')); print('nltk', md.version('nltk'))"
lm-eval 0.4.10
rouge-score 0.1.2
nltk 3.10.0
```

## Real Behavior Proof

- Environment: GitHub pull request diff for
headroomlabs-ai/headroom#1929.
- Exact command / steps: Reviewed the PR diff and ran the focused uv
lock/import checks listed above.
- Observed result: The lockfile now points at nltk 3.10.0 artifacts,
includes the new defusedxml dependency, and records the nltk>=3.10.0
resolver constraint.
- Not tested: Full local test suite was not run for this lockfile-only
security update.

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

Original automated security context from OrbisAI Security:

- CVE: CVE-2026-54293
- Severity: HIGH
- Scanner: trivy
- Rule: `CVE-2026-54293`
- File: `uv.lock`
- Assessment: Likely exploitable
- Description: nltk information disclosure via path traversal in
`nltk.data.load()`.

---------

Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-09 21:20:13 -07:00
github-actions[bot]
a9515155c7
chore: release main (#1918)
🤖 I have created a release *beep* *boop*
---


<details><summary>0.31.0</summary>

##
[0.31.0](https://github.com/headroomlabs-ai/headroom/compare/v0.30.0...v0.31.0)
(2026-07-09)


### Features

* **cache:** provider-agnostic cache-mode delta + cc-agnostic prefix
comparison
([#1868](https://github.com/headroomlabs-ai/headroom/issues/1868))
([7c2f0ea](7c2f0ea079))
* **ccr:** wire retrieve-tool interception into OpenAI Responses handler
([#1898](https://github.com/headroomlabs-ai/headroom/issues/1898))
([62cd307](62cd3072a2))
* **compression:** add audit-safe mode with protected pattern matching
([#1899](https://github.com/headroomlabs-ai/headroom/issues/1899))
([bb112dd](bb112dd176))
* **content-router:** accept any real compression (remove min-savings
floor)
([#1771](https://github.com/headroomlabs-ai/headroom/issues/1771))
([6c31db9](6c31db97fb))
* **content-router:** lossless-first dispatch, cross-turn dedup, and A7
lossy-after-fold
([#1818](https://github.com/headroomlabs-ai/headroom/issues/1818))
([60af15f](60af15f96f))
* **proxy:** add provider-only HTTP proxy
([#1807](https://github.com/headroomlabs-ai/headroom/issues/1807))
([ebe0a3b](ebe0a3bd7b))
* **proxy:** add turn-hook extension point for buffered model turns
([#1891](https://github.com/headroomlabs-ai/headroom/issues/1891))
([ec950f7](ec950f7ef1))


### Bug Fixes

* **build:** enable Intel macOS pip installs via ort-load-dynamic
([#1538](https://github.com/headroomlabs-ai/headroom/issues/1538))
([32ce99e](32ce99e4b4))
* **cache:** avoid fallback session collisions
([#1827](https://github.com/headroomlabs-ai/headroom/issues/1827))
([0f606b6](0f606b6281))
* **ccr:** make expired retrieve misses terminal
([#1781](https://github.com/headroomlabs-ai/headroom/issues/1781))
([9cbdba4](9cbdba4dc1))
* **ccr:** preserve Anthropic re-stream shape
([#1854](https://github.com/headroomlabs-ai/headroom/issues/1854))
([f663894](f663894f60))
* **ccr:** preserve thinking blocks in buffered stream re-synthesis
([#1897](https://github.com/headroomlabs-ai/headroom/issues/1897))
([ede085c](ede085cc11))
* **cli/proxy:** preserve explicit HEADROOM_MIN_TOKENS=0 / MAX_ITEMS=0
([#1886](https://github.com/headroomlabs-ai/headroom/issues/1886))
([3a33af1](3a33af1af3))
* **code-compressor:** CJK-aware relevance-query symbol matching
([#1747](https://github.com/headroomlabs-ai/headroom/issues/1747))
([b38315c](b38315cf72))
* **codex:** discover updated Codex state stores
([#1889](https://github.com/headroomlabs-ai/headroom/issues/1889))
([9d42eba](9d42ebaa1a))
* **codex:** OpenCode Zen telemetry attribution
([#1648](https://github.com/headroomlabs-ai/headroom/issues/1648))
([f18c6bd](f18c6bd896))
* **content-detector:** detect and compress space-separated JSON objects
([#1742](https://github.com/headroomlabs-ai/headroom/issues/1742))
([5194bdc](5194bdc5a6))
* **content-router:** token-measure lossless folds at the acceptance
gate ([#1772](https://github.com/headroomlabs-ai/headroom/issues/1772))
([c5493ea](c5493ea93b))
* **copilot:** normalize subscription routing host
([#1836](https://github.com/headroomlabs-ai/headroom/issues/1836))
([afd9cbd](afd9cbdfaf))
* **copilot:** route mixed-model requests per model
([#1785](https://github.com/headroomlabs-ai/headroom/issues/1785))
([5af5e22](5af5e22862))
* **dashboard:** deduplicate repeated savings metrics
([#1804](https://github.com/headroomlabs-ai/headroom/issues/1804))
([88f935a](88f935a1eb))
* **dashboard:** distinguish unavailable RTK from zero stats in Docker
([#1900](https://github.com/headroomlabs-ai/headroom/issues/1900))
([87f6e93](87f6e93c14))
* **dashboard:** distinguish unavailable RTK from zero stats in Docker
([#1901](https://github.com/headroomlabs-ai/headroom/issues/1901))
([361adcd](361adcd1a0))
* **dashboard:** price proxy savings without litellm
([#1728](https://github.com/headroomlabs-ai/headroom/issues/1728))
([188e382](188e382b44))
* detect and clear stale ANTHROPIC_BASE_URL from crashed wrap sessions
([#1768](https://github.com/headroomlabs-ai/headroom/issues/1768))
([#1837](https://github.com/headroomlabs-ai/headroom/issues/1837))
([84509a4](84509a4b89))
* **docker:** persist headroom workspace in compose
([#1839](https://github.com/headroomlabs-ai/headroom/issues/1839))
([5e29c06](5e29c06aaf))
* **docker:** report source build version
([#1862](https://github.com/headroomlabs-ai/headroom/issues/1862))
([3807488](38074888ac))
* **evals:** default unparseable judge scores below pass threshold
([#1892](https://github.com/headroomlabs-ai/headroom/issues/1892))
([42ebbc6](42ebbc6cce))
* **install:** pass sc.exe create as raw command line so binPath=
quoting survives
([#1654](https://github.com/headroomlabs-ai/headroom/issues/1654))
([#1702](https://github.com/headroomlabs-ai/headroom/issues/1702))
([d6e0710](d6e0710228))
* **install:** persist --no-http2 override through install apply
([#1676](https://github.com/headroomlabs-ai/headroom/issues/1676))
([6fb5f3b](6fb5f3bc3d))
* **mcp:** isolate ClaudeRegistrar CLI config env
([#1888](https://github.com/headroomlabs-ai/headroom/issues/1888))
([1c947b1](1c947b1103))
* **mcp:** surface dead proxy state
([#1786](https://github.com/headroomlabs-ai/headroom/issues/1786))
([931eed8](931eed879d))
* **memory:** resolve Trae cwd metadata from user reminders
([#1737](https://github.com/headroomlabs-ai/headroom/issues/1737))
([#1887](https://github.com/headroomlabs-ai/headroom/issues/1887))
([3e85eb1](3e85eb1880))
* **opencode:** use local MCP config
([#1383](https://github.com/headroomlabs-ai/headroom/issues/1383))
([4bd3ddf](4bd3ddfaa5))
* **proxy/openai:** thread savings-profile kwargs into chat completions
([#1606](https://github.com/headroomlabs-ai/headroom/issues/1606))
([7ff842d](7ff842da17))
* **proxy/openai:** translate max_tokens -&gt; max_completion_tokens on
chat path
([#1774](https://github.com/headroomlabs-ai/headroom/issues/1774))
([285808b](285808b90e))
* **proxy:** bound Codex WS compression fallback latency
([#1802](https://github.com/headroomlabs-ai/headroom/issues/1802))
([d24a3f8](d24a3f8425))
* **proxy:** bound HF tokenizer load and offload token counting off
event loop
([#1738](https://github.com/headroomlabs-ai/headroom/issues/1738))
([46d5d68](46d5d685d9))
* **proxy:** cancel retry backoff on shutdown
([#1834](https://github.com/headroomlabs-ai/headroom/issues/1834))
([da2d8dc](da2d8dc9db))
* **proxy:** compress Anthropic user text blocks when enabled
([#1875](https://github.com/headroomlabs-ai/headroom/issues/1875))
([e36439a](e36439a941))
* **proxy:** freeze must forward cached (compressed) prefix
byte-identical — stop token-mode cache busting
([#1850](https://github.com/headroomlabs-ai/headroom/issues/1850))
([248ae0f](248ae0f3e0))
* **proxy:** fsync savings dir after atomic rename
([#1764](https://github.com/headroomlabs-ai/headroom/issues/1764))
([7de2c1e](7de2c1e4c2))
* **proxy:** keep cache_control bounded + stable so the freeze overlay
stops busting
([#1852](https://github.com/headroomlabs-ai/headroom/issues/1852))
([4820134](48201345be))
* **proxy:** persist lifetime cache-read savings across restarts
([#1665](https://github.com/headroomlabs-ai/headroom/issues/1665))
([908997e](908997ef61))
* **proxy:** preserve streaming passthrough beta headers
([#1783](https://github.com/headroomlabs-ai/headroom/issues/1783))
([0f553a8](0f553a8ebb))
* **proxy:** release _active_streams session lock on setup-phase errors
([#1864](https://github.com/headroomlabs-ai/headroom/issues/1864))
([2ccd831](2ccd831032))
* **proxy:** retry HTTP/2 stream resets instead of 502ing
([#1645](https://github.com/headroomlabs-ai/headroom/issues/1645))
([2ce19c2](2ce19c2c55))
* **proxy:** retry passthrough on transient upstream connection close
([#1513](https://github.com/headroomlabs-ai/headroom/issues/1513))
([5d14080](5d14080c94))
* **proxy:** route Foundry Anthropic messages
([#1878](https://github.com/headroomlabs-ai/headroom/issues/1878))
([739f654](739f654bbd))
* **proxy:** serve /favicon.ico locally instead of tunneling upstream
([#1787](https://github.com/headroomlabs-ai/headroom/issues/1787))
([#1847](https://github.com/headroomlabs-ai/headroom/issues/1847))
([3076e32](3076e32172))
* **proxy:** stop rtk stat failures from corrupting session baseline
([#1693](https://github.com/headroomlabs-ai/headroom/issues/1693))
([681b9a8](681b9a8c1a))
* **proxy:** strip 1m model suffix before upstream forwarding
([#1840](https://github.com/headroomlabs-ai/headroom/issues/1840))
([e22d745](e22d7453d4))
* **proxy:** subtract cache write premiums from net savings
([#1800](https://github.com/headroomlabs-ai/headroom/issues/1800))
([53a465b](53a465b121))
* **router:** honor MCP aliases in excluded tools
([#1822](https://github.com/headroomlabs-ai/headroom/issues/1822))
([#1863](https://github.com/headroomlabs-ai/headroom/issues/1863))
([140d6e4](140d6e4f96))
* **rtk:** link managed rtk onto PATH instead of mutating the hook
([#1698](https://github.com/headroomlabs-ai/headroom/issues/1698))
([140cb05](140cb05fbc))
* **streaming:** preserve server_tool_use sse blocks
([#1826](https://github.com/headroomlabs-ai/headroom/issues/1826))
([4ac5493](4ac54934cb))
* **toin:** publish skip compression recommendations
([#1782](https://github.com/headroomlabs-ai/headroom/issues/1782))
([be51008](be51008c70))
* **transforms:** normalize diff compressor context
([#1801](https://github.com/headroomlabs-ai/headroom/issues/1801))
([838c523](838c5234a8))
* **transforms:** pass through ragged tables instead of misaligning
columns
([#1713](https://github.com/headroomlabs-ai/headroom/issues/1713))
([c7665ca](c7665ca088))
* use rtk native Cursor hook instead of injecting .cursorrules
([#756](https://github.com/headroomlabs-ai/headroom/issues/756))
([#1846](https://github.com/headroomlabs-ai/headroom/issues/1846))
([1573f1f](1573f1fd07))
* **wrap:** replace stale-proxy detection with Vite-style port fallback
([#1406](https://github.com/headroomlabs-ai/headroom/issues/1406))
([b4205c6](b4205c68e6))


### Performance Improvements

* **proxy:** cap compression workers to CPU count
([#1803](https://github.com/headroomlabs-ai/headroom/issues/1803))
([0a3851b](0a3851b240))
* **savings:** batch tracker persistence off the request hot path
([#1817](https://github.com/headroomlabs-ai/headroom/issues/1817))
([451b9f0](451b9f0867))


### Dependencies

* bump the cargo-minor-patch group across 1 directory with 7 updates
([#1909](https://github.com/headroomlabs-ai/headroom/issues/1909))
([45601d9](45601d93bc))
* bump the npm-minor-patch group across 4 directories with 18 updates
([#1907](https://github.com/headroomlabs-ai/headroom/issues/1907))
([8872bbc](8872bbc6a2))
</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-07-09 07:47:54 -07:00
Tejas Chopra
2990b7457d
chore: sync version state to released 0.30.0 to unblock release-please (#1916)
## Description

**Fixes the Release Please pipeline**, which stopped opening the release
PR, leaving pip / Docker / npm out of sync.

### Root cause
Recent releases (0.28 → 0.30) were cut **out-of-band** (manual tag +
release), so Release Please's state drifted from reality:
- `.release-please-manifest.json` was frozen at **0.29.0**, and the
in-repo version files at **0.29.0** — even the `v0.30.0` tag commit has
`pyproject.toml = 0.29.0`. The plugin/marketplace manifests were frozen
even further back, at **0.22.3**.
- `v0.30.0` was tagged and published to PyPI (CI stamps the version from
the tag at build time via `version-sync.py`, which is why PyPI got
0.30.0 despite the committed 0.29.0).
- With the manifest at 0.29.0, RP kept computing the next version as
**0.30.0**, saw that tag already exists, and produced **no PR** — so
nothing new could ship, and Docker/npm fell behind.

### Fix
Realign the repo with the last real release (0.30.0) so RP can drive the
next one:
- `.release-please-manifest.json` → `0.30.0`
- `pyproject.toml`, `sdk/typescript/package.json`,
`plugins/openclaw/package.json`, both `plugin.json`, both
`marketplace.json` → `0.30.0` (via `scripts/version-sync.py --version
0.30.0`, which also fixes the 0.22.3 drift).

### What happens after merge
1. Release Please runs on `main`, sees manifest = 0.30.0 + 69 releasable
commits since `v0.30.0`, and opens a clean **`chore: release 0.31.0`**
PR (bumping every version file).
2. Merging that PR tags `v0.31.0` and fires `release: published`, which
publishes **PyPI + npm (SDK + openclaw) + Docker** at 0.31.0 in one shot
— bringing all three registries back in sync.

No behavior/code change — versions only.

## Type of Change

- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature
- [ ] Breaking change
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)

## Changes Made

- Advance the Release Please manifest to the last released version
(0.30.0).
- Sync all 7 version-tracked files to 0.30.0 with the repo's own
`version-sync.py`.

## Testing

- [x] Linting passes (`ruff check .`)
- [x] Manual testing performed (`scripts/verify-versions.py`)

### Test Output

```text
$ python scripts/version-sync.py --version 0.30.0
Version synchronized to 0.30.0

$ python scripts/verify-versions.py
All versions aligned at 0.30.0
Packages: pyproject.toml, plugins/openclaw/package.json, sdk/typescript/package.json,
  plugins/headroom-agent-hooks/.claude-plugin/plugin.json,
  plugins/headroom-agent-hooks/.github/plugin/plugin.json,
  .claude-plugin/marketplace.json, .github/plugin/marketplace.json
```

## Real Behavior Proof

- Environment: local macOS, project `.venv` (Python 3.12.6).
- Exact command / steps: ran `scripts/version-sync.py --version 0.30.0`,
set the RP manifest to 0.30.0, then `scripts/verify-versions.py`.
- Observed result: `verify-versions.py` reports all seven version
locations aligned at 0.30.0; `git diff` shows version-field changes only
(no code). Confirmed PyPI latest is 0.30.0 and a `v0.30.0` tag/release
exists, while the manifest was 0.29.0 — the drift this PR corrects.
- Not tested: the downstream release itself (that runs when the
follow-up `chore: release 0.31.0` PR is merged); npm registry state
could not be read from this environment (network), but the `publish-npm`
job in `release.yml` publishes both npm packages on release.

## 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] New and existing unit tests pass locally with my changes
2026-07-09 07:08:06 -07:00
Tejas Chopra
0ba5065d40
Tejas/tool search deferral (#1885)
## Description

<!-- Briefly explain the change and why it is needed. -->

Closes #

## 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
- [ ] Code refactoring (no functional changes)

## Changes Made

- 

## 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
- [ ] Manual testing performed

### Test Output

```text
# Paste relevant command output or artifact links here
```

## Real Behavior Proof

- Environment:
- Exact command / steps:
- Observed result:
- Not tested:

## Review Readiness

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

## Checklist

- [ ] My code follows the project's style guidelines
- [ ] 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
- [ ] 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)

Add screenshots to help explain your changes.

## Additional Notes

<!-- Mention any N/A checklist items, tradeoffs, follow-ups, or
maintainer context. -->

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 14:03:45 -07:00
github-actions[bot]
660fa8cfb6
chore: release main (#1574)
🤖 I have created a release *beep* *boop*
---


<details><summary>0.29.0</summary>

##
[0.29.0](https://github.com/headroomlabs-ai/headroom/compare/v0.28.0...v0.29.0)
(2026-07-03)


### Features

* **proxy:** add --lossless no-CCR mode with format-native compaction
([#1721](https://github.com/headroomlabs-ai/headroom/issues/1721))
([c75ebde](c75ebdee6d))
* **stats:** surface Codex WS compression counters in /stats summary
([#1680](https://github.com/headroomlabs-ai/headroom/issues/1680))
([2fe19c3](2fe19c39e4))
* **transforms:** adaptive Otsu KEEP/DROP threshold (+ land relevance
split on main)
([#1726](https://github.com/headroomlabs-ai/headroom/issues/1726))
([eea667a](eea667a720))


### Bug Fixes

* **bedrock:** fail fast when session-token auth lacks botocore
([#1553](https://github.com/headroomlabs-ai/headroom/issues/1553))
([54cfa36](54cfa361d3))
* **bedrock:** route ARNs via converse, named AWS profiles, and au. re…
([#1456](https://github.com/headroomlabs-ai/headroom/issues/1456))
([7d87aa2](7d87aa2f1c))
* **ccr:** honor workspace dir for sqlite store
([#1564](https://github.com/headroomlabs-ai/headroom/issues/1564))
([96e1dfe](96e1dfe395))
* **claude:** surface Remote Control proxy incompatibility
([#1610](https://github.com/headroomlabs-ai/headroom/issues/1610))
([4bf7f92](4bf7f92417))
* **cli:** stop advertising unwired compression tuning env vars in
banner
([#1634](https://github.com/headroomlabs-ai/headroom/issues/1634))
([d5bf98d](d5bf98df31))
* **codex:** avoid duplicate headroom provider config
([#1431](https://github.com/headroomlabs-ai/headroom/issues/1431))
([ddd4adf](ddd4adf911))
* **compression:** reject lossy unmarked tool output in unit router path
([#1479](https://github.com/headroomlabs-ai/headroom/issues/1479))
([de24cd5](de24cd5fc0))
* **cortex-code:** migrate to current Cortex REST API endpoints + add
e2e benchmarks
([#1474](https://github.com/headroomlabs-ai/headroom/issues/1474))
([f00ace6](f00ace6da5))
* **dashboard:** align token savings headline denominator
([#1653](https://github.com/headroomlabs-ai/headroom/issues/1653))
([646e705](646e705514))
* **dashboard:** derive per-project setup URL from live origin
([#1511](https://github.com/headroomlabs-ai/headroom/issues/1511))
([e035aef](e035aefce2))
* **detection:** contain unidiff panic on orphaned +++ target line
([#1548](https://github.com/headroomlabs-ai/headroom/issues/1548))
([e386c09](e386c097d6))
* **evals:** CJK-aware F1 tokenization + token estimation
([#1527](https://github.com/headroomlabs-ai/headroom/issues/1527))
([99a8540](99a8540e65))
* **install:** close parent log fd in start_detached_agent
([#1576](https://github.com/headroomlabs-ai/headroom/issues/1576))
([816cb85](816cb85fa8))
* **install:** use Windows-safe PID liveness probe in runtime_status
([#1544](https://github.com/headroomlabs-ai/headroom/issues/1544))
([#1560](https://github.com/headroomlabs-ai/headroom/issues/1560))
([6b227b9](6b227b9c90))
* **learn:** aggregate verbosity baselines across projects instead of
overwriting
([#1288](https://github.com/headroomlabs-ai/headroom/issues/1288))
([27a5468](27a5468349))
* **mcp:** show lifetime totals and label rolling session scope in
headroom_stats
([#1428](https://github.com/headroomlabs-ai/headroom/issues/1428))
([1c0e152](1c0e15243e))
* **memory:** cap local embedder CPU thread oversubscription
([#198](https://github.com/headroomlabs-ai/headroom/issues/198))
([#1559](https://github.com/headroomlabs-ai/headroom/issues/1559))
([b84afbf](b84afbfb83))
* **memory:** singleflight LocalBackend init to stop cold-start races
([#1691](https://github.com/headroomlabs-ai/headroom/issues/1691))
([bec47a1](bec47a1898))
* **openclaw:** detect uv-installed headroom binary in ~/.local/bin
([#1459](https://github.com/headroomlabs-ai/headroom/issues/1459))
([adaeb88](adaeb88a4d))
* **opencode:** preserve custom OpenAI gateway paths
([#1596](https://github.com/headroomlabs-ai/headroom/issues/1596))
([c19347c](c19347c310))
* **opencode:** route native providers + load transport plugin, fix
Serena context
([#1573](https://github.com/headroomlabs-ai/headroom/issues/1573))
([ad0034f](ad0034f981))
* preserve anthropic passthrough tool order
([#1427](https://github.com/headroomlabs-ai/headroom/issues/1427))
([a932247](a9322477e3))
* **proxy/auth:** match real Anthropic OAuth token prefix (sk-ant-oat)
([#1672](https://github.com/headroomlabs-ai/headroom/issues/1672))
([8cddf9b](8cddf9b58e))
* **proxy:** expose persistent savings metrics
([#1647](https://github.com/headroomlabs-ai/headroom/issues/1647))
([5fe4e7b](5fe4e7b195))
* **proxy:** fail open when kompress saturation would exhaust
pre-upstream budget
([#1430](https://github.com/headroomlabs-ai/headroom/issues/1430))
([15ac650](15ac650d40))
* **proxy:** handle streaming CCR retrieval
([#1451](https://github.com/headroomlabs-ai/headroom/issues/1451))
([d337e3b](d337e3b828))
* **proxy:** include system/tools/sampling in cache key
([#1473](https://github.com/headroomlabs-ai/headroom/issues/1473))
([312129a](312129a8e7))
* **proxy:** preserve Responses passthrough bytes
([#1598](https://github.com/headroomlabs-ai/headroom/issues/1598))
([2a34a82](2a34a822f2))
* **proxy:** strip Codex lite header on the HTTP /responses path
([#1663](https://github.com/headroomlabs-ai/headroom/issues/1663))
([9fbd47b](9fbd47ba6b))
* **proxy:** wire --compression-max-workers /
HEADROOM_COMPRESSION_MAX_WORKERS
([#1632](https://github.com/headroomlabs-ai/headroom/issues/1632))
([814ffa3](814ffa36a4))
* **savings:** count cache-read tokens in input cost estimate
([#1429](https://github.com/headroomlabs-ai/headroom/issues/1429))
([72ade37](72ade37112))
* skip Magika backend on x86 CPUs without AVX2
([#1162](https://github.com/headroomlabs-ai/headroom/issues/1162))
([64783d8](64783d8824))
* **transforms/content-router:** route grep/log output away from HTML
extractor
([#1719](https://github.com/headroomlabs-ai/headroom/issues/1719))
([0d18ef2](0d18ef26f4))
* **transforms:** bound native content detection with a Windows watchdog
([#575](https://github.com/headroomlabs-ai/headroom/issues/575))
([#1563](https://github.com/headroomlabs-ai/headroom/issues/1563))
([95abca3](95abca3abd))
* Vertex AI support for Claude Code with ANTHROPIC_VERTEX_BASE_URL
([#1393](https://github.com/headroomlabs-ai/headroom/issues/1393))
([cff7247](cff7247efd))
* **wrap:** detach the shared proxy on Windows so it survives an
ungraceful agent close
([#1464](https://github.com/headroomlabs-ai/headroom/issues/1464))
([6cba441](6cba4419d0))
* **wrap:** preserve custom Vertex base URL
([#1477](https://github.com/headroomlabs-ai/headroom/issues/1477))
([75427bb](75427bbd4a))
* **wrap:** remove rtk instructions from Codex AGENTS.md on unwrap
([#1604](https://github.com/headroomlabs-ai/headroom/issues/1604))
([c9d717c](c9d717c13c))
</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-07-02 22:54:04 -07:00
github-actions[bot]
aea3c35177
chore: release main (#1441)
🤖 I have created a release *beep* *boop*
---


<details><summary>0.28.0</summary>

##
[0.28.0](https://github.com/headroomlabs-ai/headroom/compare/v0.27.0...v0.28.0)
(2026-06-29)


### Features

* add --disable-kompress-fallback to restore legacy PASSTHROUGH fallback
([#1185](https://github.com/headroomlabs-ai/headroom/issues/1185))
([f309244](f309244a77))
* add first-class OpenCode support (wrap, learn, mcp install)
([#559](https://github.com/headroomlabs-ai/headroom/issues/559))
([91cd210](91cd2102d7))
* add HEADROOM_KEEPALIVE_EXPIRY to keep upstream connections warm
([#1124](https://github.com/headroomlabs-ai/headroom/issues/1124))
([85786b3](85786b33a3))
* **azure-foundry:** derive upstream URL from ANTHROPIC_FOUNDRY_RESOURCE
([#1138](https://github.com/headroomlabs-ai/headroom/issues/1138))
([e5031b0](e5031b0121))
* **cache:** attribute prompt-cache misses to TTL lapse vs prefix change
([#1313](https://github.com/headroomlabs-ai/headroom/issues/1313))
([#1343](https://github.com/headroomlabs-ai/headroom/issues/1343))
([4658721](4658721ea0))
* **code:** add Perl support to code-aware compressor
([#1125](https://github.com/headroomlabs-ai/headroom/issues/1125))
([f39858c](f39858c233))
* headroom wrap opencode / unwrap opencode CLI
([#1105](https://github.com/headroomlabs-ai/headroom/issues/1105))
([b4571cc](b4571cc346))
* **learn:** weight loops in Headroom Learn + RTK-loop eval
([#1160](https://github.com/headroomlabs-ai/headroom/issues/1160))
([14e8dc4](14e8dc4c84))
* **learn:** write per-project learnings to CLAUDE.local.md by default
([#1115](https://github.com/headroomlabs-ai/headroom/issues/1115))
([ced75e4](ced75e4718))
* **proxy:** add request timeout config
([#738](https://github.com/headroomlabs-ai/headroom/issues/738))
([c0745d4](c0745d4161))
* **proxy:** pilot hardening — inbound auth, security headers, audit
log, air-gap switch
([#1537](https://github.com/headroomlabs-ai/headroom/issues/1537))
([546ab55](546ab553dc))
* **proxy:** support glob patterns in exclude_tools
([#870](https://github.com/headroomlabs-ai/headroom/issues/870))
([#1259](https://github.com/headroomlabs-ai/headroom/issues/1259))
([a2159c0](a2159c0b66))
* **read-maturation:** activity-based hold-back Read maturation
(Mechanism B)
([#1068](https://github.com/headroomlabs-ai/headroom/issues/1068))
([723b80c](723b80c091))
* **savings:** durable savings ledger + headroom savings command
([#1127](https://github.com/headroomlabs-ai/headroom/issues/1127))
([978ffa0](978ffa0a6a))
* **wrap:** add --1m to preserve the 1M context window on wrap claude
([#1158](https://github.com/headroomlabs-ai/headroom/issues/1158))
([#1351](https://github.com/headroomlabs-ai/headroom/issues/1351))
([b50d9c1](b50d9c17ce))
* **wrap:** make tokensave the primary coding-task compressor, Serena
the backup
([#1230](https://github.com/headroomlabs-ai/headroom/issues/1230))
([dca9853](dca9853ed9))


### Bug Fixes

* **agent-evals:** Phase 0 — coding-agent accuracy A/B framework
([#1037](https://github.com/headroomlabs-ai/headroom/issues/1037))
([84f9871](84f9871e30))
* **agno:** tolerate streaming tool-call SDK objects in parser
([#1312](https://github.com/headroomlabs-ai/headroom/issues/1312))
([#1336](https://github.com/headroomlabs-ai/headroom/issues/1336))
([5986c22](5986c2260f))
* **bedrock:** add boto3 1.41 + CRT for aws login credentials
([#1486](https://github.com/headroomlabs-ai/headroom/issues/1486))
([4db3bc9](4db3bc91d9))
* bump codebase-memory-mcp to v0.8.1
([#1284](https://github.com/headroomlabs-ai/headroom/issues/1284))
([530318b](530318b425))
* **ccr:** make headroom_retrieve a hash-only full-content lookup
([#1532](https://github.com/headroomlabs-ai/headroom/issues/1532))
([c2fc4d3](c2fc4d3753))
* **ccr:** propagate --no-ccr-marker flag to all compressors
([#1022](https://github.com/headroomlabs-ai/headroom/issues/1022))
([#1197](https://github.com/headroomlabs-ai/headroom/issues/1197))
([0c9b42a](0c9b42a919))
* **ccr:** skip Anthropic marker emission when tool injection is
deferred
([#1273](https://github.com/headroomlabs-ai/headroom/issues/1273))
([2cae13d](2cae13dd79))
* **ci:** extend gitleaks allowlist to cover test fixtures + verified
examples
([#1539](https://github.com/headroomlabs-ai/headroom/issues/1539))
([d2565a6](d2565a6983))
* **ci:** guarantee model present in test shards to end cache-miss
flakiness
([#1399](https://github.com/headroomlabs-ai/headroom/issues/1399))
([2e29c72](2e29c7223f))
* **ci:** normalize Windows CRLF line endings in PR governance script
([#1012](https://github.com/headroomlabs-ai/headroom/issues/1012))
([5194388](5194388b66))
* **cli:** add explicit UTF-8 encoding to file I/O in wrap commands
([#1126](https://github.com/headroomlabs-ai/headroom/issues/1126))
([#1164](https://github.com/headroomlabs-ai/headroom/issues/1164))
([a0cb798](a0cb7982e3))
* **cli:** fall back gracefully when embedding-server sidecar is absent
([#1206](https://github.com/headroomlabs-ai/headroom/issues/1206))
([38f1404](38f1404432))
* **cli:** harden all CLI surfaces + fix docs accuracy
([#1491](https://github.com/headroomlabs-ai/headroom/issues/1491))
([bd76235](bd76235f5c))
* **cli:** wire --http2/--no-http2 (HEADROOM_HTTP2) into proxy command
([#1373](https://github.com/headroomlabs-ai/headroom/issues/1373))
([e06b616](e06b61671f))
* **cli:** wire --rpm/--tpm and HEADROOM_RPM/HEADROOM_TPM to the Click
proxy command
([#1375](https://github.com/headroomlabs-ai/headroom/issues/1375))
([8aab8f2](8aab8f22cb))
* **code:** slice tree-sitter byte offsets as UTF-8
([#1332](https://github.com/headroomlabs-ai/headroom/issues/1332))
([8238402](82384022bd))
* **code:** validate Python compressed syntax
([#1302](https://github.com/headroomlabs-ai/headroom/issues/1302))
([cbd361d](cbd361de2a))
* **code:** verify a real parse in tree-sitter availability check
([#1231](https://github.com/headroomlabs-ai/headroom/issues/1231))
([#1299](https://github.com/headroomlabs-ai/headroom/issues/1299))
([5e0bb69](5e0bb69725))
* **codex:** retag threads on init so Codex Desktop history stays
visible ([#961](https://github.com/headroomlabs-ai/headroom/issues/961))
([#1349](https://github.com/headroomlabs-ai/headroom/issues/1349))
([e6bbc40](e6bbc40b11))
* **codex:** stop pinning Codex memory MCP to one project db
([#1269](https://github.com/headroomlabs-ai/headroom/issues/1269))
([ad7993b](ad7993bf15))
* **dashboard:** include RTK stats in the historical tab
([#1324](https://github.com/headroomlabs-ai/headroom/issues/1324))
([35939c3](35939c3536))
* **deps:** remediate dependency CVEs and publish SBOM
([#1509](https://github.com/headroomlabs-ai/headroom/issues/1509))
([5771a80](5771a8020e))
* **docker:** persist session history across container revisions
([#1118](https://github.com/headroomlabs-ai/headroom/issues/1118))
([5912d65](5912d65674))
* **gemini:** offload compression to the executor
([#1382](https://github.com/headroomlabs-ai/headroom/issues/1382))
([615848e](615848eba4))
* **gemini:** resolve Google model capabilities through ModelRegistry
([#1276](https://github.com/headroomlabs-ai/headroom/issues/1276))
([17ecad9](17ecad9d89))
* **install:** guard install_agent_ensure against duplicate runtime
spawns
([#1301](https://github.com/headroomlabs-ai/headroom/issues/1301))
([8da0b4e](8da0b4e565))
* **install:** repair macOS launchd restart/start lifecycle
([#1290](https://github.com/headroomlabs-ai/headroom/issues/1290))
([da1a397](da1a3973ed))
* **install:** stop duplicating ENTRYPOINT in persistent-docker runtime
command ([#833](https://github.com/headroomlabs-ai/headroom/issues/833))
([#1348](https://github.com/headroomlabs-ai/headroom/issues/1348))
([feedead](feedead077))
* **io:** use UTF-8 with locale fallback and preserve line endings on
config/text I/O
([#1498](https://github.com/headroomlabs-ai/headroom/issues/1498))
([1baa04e](1baa04ef65))
* **kompress:** hard override keeps must-keep tokens regardless of model
score ([#1400](https://github.com/headroomlabs-ai/headroom/issues/1400))
([42612c8](42612c86df))
* **langchain:** disable streaming on wrapped model during ainvoke()
([#1287](https://github.com/headroomlabs-ai/headroom/issues/1287))
([3590046](359004646b))
* **mcp:** register managed installs with a resolvable headroom command
([#1386](https://github.com/headroomlabs-ai/headroom/issues/1386))
([22def93](22def93177))
* **mcp:** report correct savings_percent in headroom_compress
([#1106](https://github.com/headroomlabs-ai/headroom/issues/1106))
([f216e43](f216e43055))
* **opencode:** write local MCP config
([#1381](https://github.com/headroomlabs-ai/headroom/issues/1381))
([6c83790](6c83790680))
* **packaging:** move hnswlib to optional [vector] extra so [all] needs
no C++ toolchain
([#1499](https://github.com/headroomlabs-ai/headroom/issues/1499))
([80fa086](80fa086660))
* patch rtk hook script to use absolute path after register_claude_hooks
([#571](https://github.com/headroomlabs-ai/headroom/issues/571))
([b618d2d](b618d2d11a))
* **perf:** surface RTK/CLI context-tool savings in perf and the session
card ([#1433](https://github.com/headroomlabs-ai/headroom/issues/1433))
([9362747](93627471b7))
* **proxy:** add --protect-tool-results to prevent lossy compression of
exact-output Bash results
([#1374](https://github.com/headroomlabs-ai/headroom/issues/1374))
([51d4bcf](51d4bcfc11))
* **proxy:** add an Anthropic buffered read-timeout override
([#1331](https://github.com/headroomlabs-ai/headroom/issues/1331))
([3be2526](3be2526b76))
* **proxy:** add versionless Vertex AI routes for Claude Code
compatibility
([#1321](https://github.com/headroomlabs-ai/headroom/issues/1321))
([bb3e040](bb3e040a46))
* **proxy:** bind before eager preload so a hung compressor load can't
block startup
([#1500](https://github.com/headroomlabs-ai/headroom/issues/1500))
([d5ac07f](d5ac07fc45))
* **proxy:** build SSL contexts for custom CA bundles
([#1134](https://github.com/headroomlabs-ai/headroom/issues/1134))
([561ba17](561ba17ec2))
* **proxy:** forward request-id headers on the streaming path
([#1100](https://github.com/headroomlabs-ai/headroom/issues/1100))
([#1258](https://github.com/headroomlabs-ai/headroom/issues/1258))
([3d59df7](3d59df7be8))
* **proxy:** gate CCR retrieve/compress endpoints to loopback
([#1338](https://github.com/headroomlabs-ai/headroom/issues/1338))
([acafb2d](acafb2d0f6))
* **proxy:** honor force_kompress routing profile
([#996](https://github.com/headroomlabs-ai/headroom/issues/996))
([b4682d6](b4682d6f91))
* **proxy:** keep large compression results on the critical path
([#296](https://github.com/headroomlabs-ai/headroom/issues/296))
([#1352](https://github.com/headroomlabs-ai/headroom/issues/1352))
([90734b6](90734b691a))
* **proxy:** offload /v1/compress to the compression executor to stop
blocking the loop
([#1501](https://github.com/headroomlabs-ai/headroom/issues/1501))
([27e010e](27e010e38f))
* **proxy:** preserve Responses memory continuations with store=false
([#1103](https://github.com/headroomlabs-ai/headroom/issues/1103))
([cdfeeac](cdfeeacc63))
* **proxy:** queue mid-turn user messages on non-Bedrock streaming path
([#1377](https://github.com/headroomlabs-ai/headroom/issues/1377))
([b09f027](b09f027062))
* **proxy:** register interceptor in explicit transforms list when
HEADROOM_INTERCEPT_ENABLED
([#1376](https://github.com/headroomlabs-ai/headroom/issues/1376))
([55c700c](55c700c686))
* **proxy:** report real input tokens on streaming message_start
([#1132](https://github.com/headroomlabs-ai/headroom/issues/1132))
([#1305](https://github.com/headroomlabs-ai/headroom/issues/1305))
([70cc96a](70cc96a386))
* **proxy:** retry upstream 429 with Retry-After on both forwarders
([#1329](https://github.com/headroomlabs-ai/headroom/issues/1329))
([90bee89](90bee89243))
* **proxy:** retry upstream 529 overloaded like 429 on both forwarders
([#1495](https://github.com/headroomlabs-ai/headroom/issues/1495))
([547b15d](547b15dab2))
* **proxy:** stop re-compressing headroom_retrieve output and emitting
unredeemable markers
([#1323](https://github.com/headroomlabs-ai/headroom/issues/1323))
([43494ff](43494ff526))
* **proxy:** strip Codex lite header from OpenAI WebSockets
([#1543](https://github.com/headroomlabs-ai/headroom/issues/1543))
([5d3803a](5d3803a21c))
* **read-lifecycle:** persist STALE Read originals in the CCR store
([#1488](https://github.com/headroomlabs-ai/headroom/issues/1488))
([9157173](9157173018))
* recover persistent proxy feature checks and reject non-Copilot
exchange URL
([#1465](https://github.com/headroomlabs-ai/headroom/issues/1465))
([16c638b](16c638bc21))
* remove agents.md
([#1540](https://github.com/headroomlabs-ai/headroom/issues/1540))
([a7d3360](a7d3360a05))
* respect COPILOT_PROVIDER_TYPE env var when provider_type is auto
([#549](https://github.com/headroomlabs-ai/headroom/issues/549))
([24cf256](24cf256e50))
* restore token-mode compression on frozen prefixes
([#1489](https://github.com/headroomlabs-ai/headroom/issues/1489))
([8e0dadf](8e0dadfe02))
* **router:** degrade to pure-Python detection on native panic
([#1123](https://github.com/headroomlabs-ai/headroom/issues/1123))
([#1260](https://github.com/headroomlabs-ai/headroom/issues/1260))
([a00fb67](a00fb6761e))
* **rtk:** stop hook registration timing out on a forked daemon
([#1314](https://github.com/headroomlabs-ai/headroom/issues/1314))
([9758817](9758817979))
* **smart-crusher:** honor enable_ccr_marker on the opaque-blob path
([#1130](https://github.com/headroomlabs-ai/headroom/issues/1130))
([27d6f8e](27d6f8e2a7))
* **subscription:** only reset 5h contribution on real rollover, not API
jitter
([#1255](https://github.com/headroomlabs-ai/headroom/issues/1255))
([8d6c175](8d6c175d60))
* **subscription:** run transcript token scan off the event loop
([#1263](https://github.com/headroomlabs-ai/headroom/issues/1263))
([f03021f](f03021f1b6))
* surface output reduction without a restart, and explain $0.00 savings
on Python 3.14
([#1296](https://github.com/headroomlabs-ai/headroom/issues/1296))
([c30ec4c](c30ec4cda8))
* **tests:** reset whole headroom logger subtree so caplog stays
deterministic
([#1117](https://github.com/headroomlabs-ai/headroom/issues/1117))
([fda4670](fda4670ef8))
* **tls:** add HEADROOM_TLS_STRICT=0 toggle for corporate SSL inspection
([#1308](https://github.com/headroomlabs-ai/headroom/issues/1308))
([#1341](https://github.com/headroomlabs-ai/headroom/issues/1341))
([52068dd](52068dd650))
* **tokenizers:** price CJK/Kana/Hangul at ~1 token per char in
EstimatingTokenCounter
([#1093](https://github.com/headroomlabs-ai/headroom/issues/1093))
([a35fe86](a35fe86e87))
* **transforms:** gate tool string output from lossy compression
([#1307](https://github.com/headroomlabs-ai/headroom/issues/1307))
([#1387](https://github.com/headroomlabs-ai/headroom/issues/1387))
([c6c921a](c6c921a7c1))
* **websocket:** harden responses websocket origin handling
([#1481](https://github.com/headroomlabs-ai/headroom/issues/1481))
([c632023](c632023cc1))
* **windows:** pin UTF-8 encoding on text-mode subprocess calls
([#1311](https://github.com/headroomlabs-ai/headroom/issues/1311))
([d633e81](d633e8172c))
* **wrap:** add Copilot unwrap command
([#1251](https://github.com/headroomlabs-ai/headroom/issues/1251))
([b4fde0c](b4fde0c3a4))
* **wrap:** isolate proxy stdio from proxy.log on Windows
([#1191](https://github.com/headroomlabs-ai/headroom/issues/1191))
([959ab0d](959ab0de47))
* **wrap:** keep agent savings opt-in
([#1294](https://github.com/headroomlabs-ai/headroom/issues/1294))
([b829ceb](b829ceba84))
* **wrap:** show the dashboard URL when the proxy is already running
([#1313](https://github.com/headroomlabs-ai/headroom/issues/1313))
([b0146c4](b0146c4ccd))


### Performance Improvements

* **compression:** take large cold-start contexts off the synchronous
kompress path
([#1171](https://github.com/headroomlabs-ai/headroom/issues/1171))
([#1298](https://github.com/headroomlabs-ai/headroom/issues/1298))
([6c68ff4](6c68ff4e9f))
</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-29 12:53:17 -07:00
Kenneth Wong
4db3bc91d9
fix(bedrock): add boto3 1.41 + CRT for aws login credentials (#1486)
## Description

`pip install headroom-ai[bedrock]` cannot serve users who authenticate
with `aws login` (IAM Identity Provider / console-login, DPoP).
Resolving those credentials requires the AWS Common Runtime (CRT);
without `awscrt`, botocore raises `MissingDependencyException`.

The AWS docs state the requirement as: **"Boto3 version 1.41.0 or later
with AWS Common Runtime (CRT)"** — i.e. both a modern boto3 floor and
CRT (installed via the `[crt]` extra).

## Type of Change

- [x] Bug fix (non-breaking)

## Changes Made

- `pyproject.toml` `bedrock` extra: bump `boto3>=1.28.0` →
`boto3>=1.41.0`, add `botocore[crt]>=1.41.0` (installs `awscrt`).
- `uv.lock`: regenerated — adds `awscrt`, resolves `boto3` to 1.42.x.

No code changes — the bedrock backend already passes `aws_profile_name`
through to the LiteLLM calls (via #1456); this just makes the installed
dependencies actually able to resolve `aws login` credentials.

## Impact

- **`aws login` (IAM Identity Provider / DPoP):** now works — awscrt
present.
- **`aws sso login` (classic Identity Center):** unaffected (already
worked).
- **static keys (`~/.aws/credentials`):** unaffected.
- Bumping the boto3 floor only affects the optional `[bedrock]` extra;
bedrock users benefit from a current boto3 regardless.

## Testing

Dependency-only change. `uv lock` resolves cleanly (257 packages, awscrt
0.29.2, boto3 1.42.38). No runtime code path altered, so existing
bedrock tests are unaffected.

## Checklist

- [x] Self-review performed
- [x] No new warnings
- [x] Linting passes

## Additional Notes

Focused on the dependency gap only. ARN routing / named-profile wiring /
docs are handled in #1456; pricing in #1485.
2026-06-28 14:51:52 -07:00
Parideboy
80fa086660
fix(packaging): move hnswlib to optional [vector] extra so [all] needs no C++ toolchain (#1499)
## Description

`pip install "headroom-ai[all]"` aborts on any machine without a C++
toolchain.
`[all]` pulls `[memory]`, which was the only extra carrying
`hnswlib>=0.8.0`. hnswlib
compiles from source where no wheel matches the target, and that build
failure rolls
back the **entire** `[all]` install.

hnswlib is already fully optional at runtime: `MemoryConfig` defaults to
`VectorBackend.AUTO` → **sqlite-vec** (pure Python, no compiler), and
only falls back
to HNSW. So `[memory]` does not need hnswlib to function. This moves
hnswlib into a
dedicated optional `[vector]` extra, exactly like `[pytorch-mps]` is
already kept out
of `[all]`.

Closes #1368

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

- `pyproject.toml`:
  - Removed `hnswlib>=0.8.0` from `[memory]` (keeps `sqlite-vec` +
`sentence-transformers`; the default sqlite-vec backend still works).
- Added `vector = ["hnswlib>=0.8.0"]` for users who opt into the HNSW
backend.
- `[all]` still references `[memory]` (now hnswlib-free) and does
**not** add
    `[vector]`, so it resolves with no compiler.
- `[dev]` keeps `hnswlib`, so CI still installs and exercises the HNSW
backend tests.
- Docs: documented the new `[vector]` extra in `installation.mdx` and
the README, and
noted it is excluded from `[all]`; fixed the `[memory]` row that claimed
to bundle
  hnswlib.

No application code changed.

## Testing

- [x] Linting passes (`ruff check .`)
- [x] Manual testing performed (TOML resolution check — see proof)
- [ ] Unit tests pass (`pytest`) — no app code changed; existing
memory/HNSW tests are
unaffected (the HNSW backend dependency moved extras but `[dev]`/CI
still install it).

### Test Output

```text
$ python - <<'PY'  # resolve [all] transitively and check hnswlib placement
memory has hnswlib: False
vector has hnswlib: True
dev has hnswlib:    True
[all] resolved has hnswlib: False
[all] has sqlite-vec: True
[all] has sentence-transformers: True
PY

$ ruff check headroom/ tests/
All checks passed!
```

## Real Behavior Proof

- Environment: Windows 11, Python 3.13.11; `tomllib` + a small
transitive-extra
  resolver over the edited `pyproject.toml`.
- Exact command / steps: parse `pyproject.toml`, expand
`headroom-ai[...]`
self-references in `[all]` recursively, then check which extras carry
`hnswlib`.
- Observed result: the resolved `[all]` set contains no hnswlib while
`[vector]` and `[dev]` do. Full output:
  ```text
  memory has hnswlib: False
  vector has hnswlib: True
  dev has hnswlib:    True
  [all] resolved has hnswlib: False
  [all] has sqlite-vec: True
  [all] has sentence-transformers: True
  ```
`[all]` now resolves with **no** hnswlib (so no compiler needed), while
the HNSW
  backend stays installable via `[vector]` and still tested via `[dev]`.
- Not tested: a real `pip install` on a compiler-less host (the failure
is a build-time
rollback that the resolver check captures deterministically); the
native-wrapper e2e
jobs that this `pyproject.toml` change triggers run `wrap` e2e, not the
memory HNSW
  path, so dropping hnswlib from `[all]` does not affect them.

## Review Readiness

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

## Additional Notes

- Editing `pyproject.toml` trips the `e2e` path filter, so the
Windows/macOS/Docker
native-wrapper jobs also run on this PR. They install + run the `wrap`
e2e flow (not
  the memory HNSW backend), so the extras change is safe for them.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 13:16:55 -07:00
Tejas Chopra
5771a8020e
fix(deps): remediate dependency CVEs and publish SBOM (#1509)
## Description

Supply-chain hardening: takes the **shipped** dependency surface from
**26 known CVEs to 0**. `pip install headroom-ai[all]` now resolves with
no known vulnerabilities (verified with Anchore syft + grype). Also
publishes a checked-in SBOM package (`sbom/`) so any user — especially
pilots running their own security review — can verify what's inside and
that we track it.

This addresses the Dependabot alerts on `main` (9 high / 4 moderate / 7
low at time of writing).

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

**Rust**
- `pyo3` 0.24 → 0.29 (GHSA-36hh-v3qg-5jq4 High, GHSA-chgr-c6px-7xpp
Med). Migrated `Python::allow_threads` → `Python::detach` (10 sites) and
added `from_py_object` to the `Clone`-deriving `#[pyclass]` types (both
required by the 0.25+ API).
- `pyo3-log` 0.12 → 0.13; `lru` 0.12 → 0.18 (GHSA-rhfx-m35p-ff5j).

**Python**
- `torch` → 2.12.1, `mem0ai` → 2.x.
- Floor-pinned transitive CVE deps via `[tool.uv]
constraint-dependencies`: `pygments>=2.20.0`,
`pydantic-settings>=2.14.2`, `gitpython>=3.1.50`, `langsmith>=0.9.0`.
- **Removed `benchmark` from the `[all]` aggregate** so the default
install is CVE-free. `lm-eval` is invoked as an external subprocess
(`python -m lm_eval`) and never imported, so it is not a true runtime
dep — it remains available via the opt-in `[benchmark]` extra. See
[Accepted Risks](#additional-notes).

**npm (build/test tooling — never shipped in the
wheel/container/published SDK)**
- `esbuild` override `>=0.28.1` in `sdk/typescript` + `plugins/openclaw`
(GHSA-g7r4-m6w7-qqqr).
- `docs/`: `@anthropic-ai/sdk` → `^0.106.0` (GHSA-p7fg-763f-g4gf),
`postcss` override to force Next.js's bundled copy ≥8.5.10
(GHSA-qx2v-qp2m-jg93); regenerated a stale `bun.lock` that carried a
**Critical** vitest/vite.

**CI**
- Pinned `pypa/gh-action-pypi-publish` `@release/v1` → `@v1.13.0`
(GHSA-vxmw-7h4f-hqxh) in `release.yml` + `publish.yml`.

**SBOM**
- New `sbom/` directory: CycloneDX 1.7 + SPDX 2.3 SBOMs, grype scan
evidence, 330-package license inventory, and a regeneration guide.

## Testing

- [ ] Unit tests pass (`pytest`) — N/A, no Python source changed
(deps/config only)
- [x] Linting passes — `cargo fmt --check` + `cargo clippy` clean on the
changed crate; 0 `.py` files changed so `ruff`/`mypy` scope is
unaffected
- [x] Type checking passes — `cargo check --workspace` (0 errors)
- [ ] New tests added — N/A (dependency bumps; covered by existing
suites)
- [x] Manual testing performed — see Real Behavior Proof

### Test Output

```text
# headroom-ai[all] product surface — the number that matters
$ grype sbom:sbom/headroom-sbom-all-extra.cdx.json
No vulnerabilities found

# full repo scan (universal lock incl. opt-in [benchmark] + dev)
$ grype sbom:sbom/headroom-sbom.cdx.json
NAME        INSTALLED  TYPE    VULNERABILITY        SEVERITY
sqlitedict  2.1.0      python  GHSA-g4r7-86gm-pgqc  High      # [benchmark]-only, unpatchable, accepted
nltk        3.9.4      python  GHSA-p4gq-832x-fm9v  High      # [benchmark]-only, unpatchable, accepted

# pyo3 0.29 migration — extension builds + imports + runs
$ cargo check --workspace
    Finished `dev` profile [unoptimized + debuginfo] target(s)
$ maturin develop && python -c "from headroom._core import DiffCompressor, SmartCrusher; ..."
extension OK — detach + from_py_object paths exercised

# lru 0.18 — eviction path
$ cargo test -p headroom-proxy --lib drift
14 passed, 213 filtered out

# per-ecosystem npm audits
$ (cd sdk/typescript && npm audit)         -> found 0 vulnerabilities
$ (cd plugins/openclaw && npm audit)       -> found 0 vulnerabilities
$ (cd docs && npm audit && bun audit)      -> found 0 vulnerabilities / No vulnerabilities found
```

## Real Behavior Proof

- Environment: macOS (darwin 25.4.0, arm64), Python 3.12 `.venv`, Rust
1.95 toolchain, syft 1.46.0, grype 0.115.0, bun 1.3.14, maturin 1.13.3.
- Exact command / steps: (1) `uv export --extra all --no-dev
--no-emit-project | syft → grype` for the product surface; (2) `cargo
check --workspace` + `maturin develop` + extension import/compress smoke
test; (3) `cargo test -p headroom-proxy --lib drift`; (4) `cargo fmt
--check` + `cargo clippy -p headroom-py`; (5) `npm audit` in
sdk/openclaw/docs + `bun audit` in docs.
- Observed result: `headroom-ai[all]` resolution scans clean — "No
vulnerabilities found" (179 pkgs); full/prod SBOM shows only the 2
documented accepted CVEs; pyo3 0.29 extension imports and runs (detach +
from_py_object paths exercised); drift tests 14/14 pass; cargo fmt +
clippy clean; all npm/bun audits report 0.
- Not tested: full `pytest` suite (no Python source changed);
release-profile wheel build (used dev-profile `maturin develop` for the
import proof — the extension is semantically identical).

## 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
(`sbom/README.md`)
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective — N/A
(dependency bumps; existing suites + scans cover it)
- [x] New and existing unit tests pass locally with my changes
- [ ] I have updated the CHANGELOG.md — N/A (Release Please
auto-generates from the conventional commit)

## Additional Notes

**Accepted risks (the 2 residual CVEs).** Both originate solely from the
EleutherAI `lm-evaluation-harness` under the **opt-in `[benchmark]`
extra**, which Headroom invokes as a subprocess (never imports):
- `sqlitedict` CVE-2024-35515 (High) — pickle deserialization; package
abandoned (last release 2021), **no upstream fix exists**.
- `nltk` CVE-2026-54293 (High) — path traversal in `nltk.data.load()`;
affects ≤3.9.4 (current latest), **no patched release**.

Neither is in `[all]`, the published wheel, or the container. They are
documented in `sbom/README.md` and will be picked up automatically once
upstream ships fixes.

**Release/CHANGELOG:** N/A items above are because this is a
dependency/security PR with no Python source changes; CHANGELOG is
Release-Please-managed via the conventional commit message.
2026-06-27 15:28:12 -07:00
github-actions[bot]
95b2333ee5
chore: release main (#1274)
🤖 I have created a release *beep* *boop*
---


<details><summary>0.27.0</summary>

##
[0.27.0](https://github.com/chopratejas/headroom/compare/v0.26.0...v0.27.0)
(2026-06-22)


### Features

* **cli:** add headroom doctor setup diagnostics
([#926](https://github.com/chopratejas/headroom/issues/926))
([e45cf4e](e45cf4e061))
* **cli:** add headroom update command and release banner
([#1088](https://github.com/chopratejas/headroom/issues/1088))
([26be2c3](26be2c39cb))
* compression extraction — Rust knob exposure, CCR hardening, traffic
audits ([#818](https://github.com/chopratejas/headroom/issues/818))
([b7be381](b7be3814f1))
* measure and surface token throughput (tokens/sec) through the proxy
([#983](https://github.com/chopratejas/headroom/issues/983))
([0d89c67](0d89c674cd))
* output-token reduction — verbosity shaper, per-user learning,
counterfactual savings
([#965](https://github.com/chopratejas/headroom/issues/965))
([a99dc61](a99dc61424))
* **policy:** decay P_alive from idle time near cache TTL
([#856](https://github.com/chopratejas/headroom/issues/856) P3b)
([#1028](https://github.com/chopratejas/headroom/issues/1028))
([fe4f9ee](fe4f9ee478))
* **providers:** add Cortex Code (Snowflake CoCo) as a supported agent
([#1190](https://github.com/chopratejas/headroom/issues/1190))
([d9d0bf4](d9d0bf4b79))
* **proxy:** cc-switch reconciler — keep Headroom in the request path
alongside cc-switch
([#1030](https://github.com/chopratejas/headroom/issues/1030))
([e8fc8a0](e8fc8a0d18))
* **proxy:** hot-reload live env knobs so a reused proxy picks them up
without a restart
([#1090](https://github.com/chopratejas/headroom/issues/1090))
([6904d47](6904d47a01))
* **proxy:** make COMPRESSION_TIMEOUT_SECONDS configurable via env
([#946](https://github.com/chopratejas/headroom/issues/946))
([#991](https://github.com/chopratejas/headroom/issues/991))
([addebdb](addebdb29c))
* **transforms:** tabular + spreadsheet (.xlsx/.xls) compression
([#1128](https://github.com/chopratejas/headroom/issues/1128))
([d789a7c](d789a7c528))
* **vertex:** turnkey Claude Code + Vertex compression (+ fixes from the
Vertex review)
([#1113](https://github.com/chopratejas/headroom/issues/1113))
([0e05915](0e0591506c))


### Bug Fixes

* **ccr:** accept 12-char SmartCrusher hashes in tool injection
([#1095](https://github.com/chopratejas/headroom/issues/1095))
([#1141](https://github.com/chopratejas/headroom/issues/1141))
([9f7f3ad](9f7f3adfea))
* **ccr:** return stored content when headroom_retrieve query matches
nothing ([#1213](https://github.com/chopratejas/headroom/issues/1213))
([#1236](https://github.com/chopratejas/headroom/issues/1236))
([08fb845](08fb845fe3))
* **content-router:** honor target_ratio in compression cache + add
proxy --target-ratio flag
([#1108](https://github.com/chopratejas/headroom/issues/1108))
([8894ee0](8894ee0c18))
* **dashboard:** light-mode backgrounds + aligned savings tables
([#1064](https://github.com/chopratejas/headroom/issues/1064))
([5eae32b](5eae32ba47))
* **deps:** make litellm optional on Python 3.14
([#956](https://github.com/chopratejas/headroom/issues/956))
([#993](https://github.com/chopratejas/headroom/issues/993))
([b2f04e4](b2f04e4ef7))
* **e2e:** align Codex wrap e2e with global-only RTK guidance
([#1240](https://github.com/chopratejas/headroom/issues/1240))
([#1254](https://github.com/chopratejas/headroom/issues/1254))
([bc12ace](bc12acef59))
* **init:** set ENABLE_TOOL_SEARCH=true so Claude Code keeps deferring
tools ([#746](https://github.com/chopratejas/headroom/issues/746))
([#995](https://github.com/chopratejas/headroom/issues/995))
([500ec2b](500ec2b7fa))
* **kompress:** never block the request path on the cold-cache model
download ([#1161](https://github.com/chopratejas/headroom/issues/1161))
([3fc2a78](3fc2a78a5e))
* **memory:** use ONNX embedder for `wrap --memory` sync
([#1092](https://github.com/chopratejas/headroom/issues/1092))
([#1262](https://github.com/chopratejas/headroom/issues/1262))
([4f9feda](4f9fedaa7a))
* **openclaw:** wrap plugin export as {register} object for OpenClaw
2026.x compatibility
([#1218](https://github.com/chopratejas/headroom/issues/1218))
([2e6c442](2e6c442dc8))
* **providers:** update DeepSeek V3 context limit from 128K to 1M
([#1038](https://github.com/chopratejas/headroom/issues/1038))
([#1137](https://github.com/chopratejas/headroom/issues/1137))
([bcabc5c](bcabc5cb11))
* **proxy:** allow disabling periodic TOIN stats logging
([#1265](https://github.com/chopratejas/headroom/issues/1265))
([b5f63d8](b5f63d8fa9))
* **proxy:** honor HEADROOM_EXCLUDE_TOOLS for Codex /v1/responses tool
outputs ([#940](https://github.com/chopratejas/headroom/issues/940))
([#1053](https://github.com/chopratejas/headroom/issues/1053))
([f03e77b](f03e77bec0))
* **proxy:** preserve byte-faithful Anthropic tool forwarding
([#1222](https://github.com/chopratejas/headroom/issues/1222))
([1f18d59](1f18d59809))
* **proxy:** route Codex OAuth image requests
([#1215](https://github.com/chopratejas/headroom/issues/1215))
([381d771](381d771e46))
* **proxy:** scope CORS to loopback + gate operator/content endpoints
([#1226](https://github.com/chopratejas/headroom/issues/1226))
([bd55a42](bd55a426bc))
* **proxy:** stamp X-Client: codex on Responses endpoint for
unidentified callers
([#1036](https://github.com/chopratejas/headroom/issues/1036))
([b0cd032](b0cd0329c7))
* **proxy:** treat NODE_EXTRA_CA_CERTS as additive, not replacement
([#998](https://github.com/chopratejas/headroom/issues/998))
([#1031](https://github.com/chopratejas/headroom/issues/1031))
([c987283](c98728363a))
* **telemetry:** switch anonymous telemetry to opt-in (off by default)
([#1223](https://github.com/chopratejas/headroom/issues/1223))
([b998697](b99869778b))
* **tokenizers:** bound tiktoken vocab load so a stalled download cannot
hang requests
([#956](https://github.com/chopratejas/headroom/issues/956))
([#994](https://github.com/chopratejas/headroom/issues/994))
([7e86baf](7e86bafb90))
* **unwrap:** remove ANTHROPIC_BASE_URL + ENABLE_TOOL_SEARCH and init
hooks on unwrap
([#992](https://github.com/chopratejas/headroom/issues/992))
([5b84691](5b84691770))
* **wrap:** keep Codex RTK guidance global
([#1240](https://github.com/chopratejas/headroom/issues/1240))
([7c26a54](7c26a54d53))
* **wrap:** percent-encode non-ASCII cwd names in X-Headroom-Project
header ([#1071](https://github.com/chopratejas/headroom/issues/1071))
([9f712cc](9f712ccbd7))
* **wrap:** write env.ANTHROPIC_BASE_URL to settings.json so
daemon-spawned conversations inherit proxy
([#951](https://github.com/chopratejas/headroom/issues/951))
([#1078](https://github.com/chopratejas/headroom/issues/1078))
([a554c3a](a554c3a0e6))
</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-21 22:28:55 -07:00
Tufan ALIN
f11a271229
fix(code): pin tree-sitter-language-pack <1.0 so code compression works (#1234)
## Description

The `[code]` extra requires `tree-sitter-language-pack>=0.10.0` with no
upper bound, so it now resolves to the 1.x line.
tree-sitter-language-pack 1.0 (2026-03-21) is a breaking rewrite whose
`get_language()` / `get_parser()` return the pack's own binding types
instead of standalone `tree_sitter.Language` / `tree_sitter.Parser`. As
a result `headroom/transforms/code_compressor.py::_get_parser()` raises,
the exception is caught upstream, and AST code compression silently
falls back to passthrough (0% reduction, no error surfaced) on a fresh
`pip install headroom-ai[code]`. This caps the dependency below the
breaking rewrite and pins the matching tree-sitter range, which is the
line the existing code is written against.

Closes #1232

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

- Pin `tree-sitter-language-pack>=0.10.0,<1.0` in the `[code]` extra
(was `>=0.10.0`).
- Add an explicit `tree-sitter>=0.25.2,<0.26` pin to document the
supported range (0.13.0 already requires `tree-sitter>=0.25.2`).
- Add an inline comment explaining why the `<1.0` cap is required, to
prevent a future re-bump.

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

I did not run the full pytest / ruff / mypy suite for this change (it is
a dependency-constraint pin); I verified the actual runtime behavior the
pin restores. See Real Behavior Proof.

### Test Output

```text
# BEFORE (resolved tree-sitter-language-pack 1.9.1): code compression no-ops
CodeAwareCompressor().compress(<real .py>) -> compression_ratio = 1.0  (0% on every file sampled)

# AFTER (tree-sitter-language-pack 0.13.0 + tree-sitter 0.25.2), headroom code unchanged:
is_tree_sitter_available(): True
# 60 varied real Python files (headroom, litellm, pydantic, openai), default CodeCompressorConfig:
  compressed OK (valid + reduced): 31   (52%)
  rejected for invalid syntax:     17   (28%)  -> returns original, never serves broken code
  no reduction / too small:        12   (20%)
  reduction when it worked: min 4.4%  median 37.2%  max 88.8%
# All compressed outputs re-parsed clean with ast.parse().
```

## Real Behavior Proof

- Environment: Python 3.12, headroom-ai 0.26.0. Before:
tree-sitter-language-pack 1.9.1 (what `[code]` resolves today). After:
tree-sitter-language-pack 0.13.0 + tree-sitter 0.25.2 (what this pin
resolves).
- Exact command / steps: `pip install "headroom-ai[code]"`; then run
`CodeAwareCompressor(CodeCompressorConfig()).compress(src)` over a
sample of real `.py` files and re-tokenize before/after with tiktoken
(cl100k_base), re-parsing each output with `ast.parse`.
- Observed result: with the unpinned (1.x) resolution, every sampled
file returned `compression_ratio == 1.0` (0%, silent passthrough). With
the pinned (0.x) resolution and no code changes,
`is_tree_sitter_available()` is True and 31/60 files compressed validly
at a ~37% median (up to ~89%); all compressed outputs re-parsed clean.
- Not tested: the full pytest / ruff / mypy suite; per-language rates
for JS/TS/Go/Rust/Java/C/C++ (they share the same `_get_parser()` path,
so the fix applies, but I measured Python specifically); the ~28%
invalid-syntax rejections are a separate pre-existing robustness issue
tracked in #1233, not addressed here.

## Review Readiness

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

## Checklist

- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [ ] 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 (dependency-constraint change).

## Additional Notes

- This is the minimal fix to restore functionality. The proper
longer-term fix is to migrate `_get_parser()` and the AST walker to the
tree-sitter-language-pack 1.x API, after which the `<1.0` cap can be
lifted; happy to follow up with that if preferred.
- Unchecked checklist items, with rationale: no docs change needed
(constraint-only); no new tests added (a corpus-based
compress-and-reparse regression test would be valuable but belongs with
the robustness work in #1233); I did not run the full local unit-test
suite for a dependency pin; CHANGELOG appears to be release-please
managed, so I left it untouched.
- I am not a maintainer; this came out of an independent evaluation of
the `[code]` path. Pinning `<1.0` parks the project on the
now-superseded 0.x pack, which is the tradeoff for a one-line fix today.

Co-authored-by: mitralone <5514599+mitralone@users.noreply.github.com>
2026-06-21 10:06:59 -07:00
Ashish
d789a7c528
feat(transforms): tabular + spreadsheet (.xlsx/.xls) compression (#1128)
## Description

Adds a content-type-aware path for **tabular data** — CSV/TSV, markdown
tables, fixed-width text, and binary `.xlsx`/`.xls` spreadsheets — by
routing them through the existing, battle-tested `SmartCrusher` instead
of letting them fall through to `PLAIN_TEXT → Kompress`.

The pipeline already compressed tables losslessly when handed a JSON
array of records. This wires up the missing front door: detect tabular
text (and ingest binary spreadsheets), convert to JSON records, and
reuse `SmartCrusher.crush()`. No new compression algorithm.

Closes #

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

- **Detection** (`content_detector.py`): new `ContentType.TABULAR` +
`_try_detect_tabular()` for CSV/TSV, markdown tables, and fixed-width
columns. Ordered after search/log (which also look "delimited") and
before code, with a prose-rejection guard so it never steals
`file:line:content` search output, `key: value` logs, or sentences with
incidental commas. Rust backend returns `plain_text` for unknown types
and the router already falls back to the Python detector, so **no Rust
change**.
- **Bridge** (`tabular_ingest.py`): stdlib parsers + `to_records()` + a
`TabularCompressor` that parses → JSON records → `SmartCrusher`
(lossless `csv-schema` first; lossy row-drop with reversible
`<<ccr:HASH>>` markers stays SmartCrusher's built-in fallback). Only
adopts a result when it actually saves bytes.
- **Spreadsheets** (`spreadsheet_ingest.py`): `.xlsx`/`.xls` → per-sheet
CSV text at the SDK boundary. Optional deps (`pip install
headroom-ai[spreadsheet]`) fail loudly with an install hint, never
silently degrade.
- **Routing** (`content_router.py`): `CompressionStrategy.TABULAR`,
`enable_tabular_compressor` flag, lazy getter, apply branch, strategy
maps, Kompress fallback eligibility.
- **SDK** (`compress.py`): `compress_spreadsheet(path, ...)` helper (one
message per sheet).
- **Packaging** (`pyproject.toml`): new `[spreadsheet]` extra;
`openpyxl` added to `[dev]` so the xlsx path is exercised in CI.
- **Docs/demo**: `examples/tabular_compression_demo.py` + README entry.

### Design note: lossless-only

Compact, all-unique tables with no query yield ~0 savings — this is
correct, not a bug. SmartCrusher returns
`skip:unique_entities_no_signal` and won't drop unique rows without a
duplicate/relevance signal. Real wins come from verbose/redundant tables
and query-driven selection. A pressure-driven lossy row sampler was
considered and intentionally not added.

## Testing

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

### Test Output

```text
$ python -m pytest tests/test_transforms_tabular.py -q
collected 20 items
tests/test_transforms_tabular.py ....................                    [100%]
============================== 20 passed in 7.15s ==============================

$ ruff check headroom/transforms/tabular_ingest.py headroom/transforms/spreadsheet_ingest.py
All checks passed!

$ mypy headroom/transforms/tabular_ingest.py headroom/transforms/spreadsheet_ingest.py
Success: no issues found in 2 source files
```

`tests/test_transforms_tabular.py` (20 tests): detection true positives
+ no-misroute negatives (search/log/JSON/prose), parser units (incl.
fixed-width), the CSV→SmartCrusher bridge, router routing + disable
flag, and `.xlsx` ingestion (skipif openpyxl missing) + error paths.
`spreadsheet_ingest` 100% / `tabular_ingest` 90% line coverage.

## Real Behavior Proof

- **Environment:** local checkout of `feat/tabular-compression`, Python
3.x, `pip install -e ".[dev]"`.
- **Exact command / steps:** `python
examples/tabular_compression_demo.py` (no API key required).
- **Observed result:**
  ```text
  === Raw tabular text (ContentRouter, char-level) ===
compact unique CSV strat=tabular chars 1306 -> 1072 ( 17.9% saved)
redundant CSV strat=tabular chars 2661 -> 1350 ( 49.3% saved)
verbose markdown strat=tabular chars 2019 -> 1580 ( 21.7% saved)

  === Full pipeline (real tokenizer) ===
  redundant CSV            tokens       768 ->    394  ( 48.7% saved)

  === Binary spreadsheet (.xlsx) ===
  2-sheet workbook         tokens      1092 ->    683  ( 37.5% saved)
  ```
- **Not tested:** legacy `.xls` binary path (needs optional `xlrd` +
binary fixture; `# pragma: no cover`); base64-embedded `.xlsx` inside
multimodal blocks (out of scope, noted as a follow-up).

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

- CHANGELOG/version are intentionally untouched: this repo uses
**release-please**, which bumps the version and CHANGELOG via automated
`chore: release main` PRs, not per-feature PRs.
- The `.xls` path is `# pragma: no cover` (legacy, needs optional `xlrd`
+ a binary fixture).
- Follow-up (out of scope): base64-embedded `.xlsx` inside
tool-result/multimodal blocks; porting tabular parsers into the Rust
core for parity.

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 11:30:20 -05: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
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
github-actions[bot]
8a53c8ec3b
chore: release main (#891)
🤖 I have created a release *beep* *boop*
---


<details><summary>0.25.0</summary>

##
[0.25.0](https://github.com/chopratejas/headroom/compare/v0.24.0...v0.25.0)
(2026-06-12)


### Features

* add differential network capture harness
([#761](https://github.com/chopratejas/headroom/issues/761))
([11ab5f8](11ab5f83a1))
* add light mode for dashboard
([#834](https://github.com/chopratejas/headroom/issues/834))
([c425893](c425893d12))
* add OAuth2 client-credentials upstream-auth proxy extension
([#778](https://github.com/chopratejas/headroom/issues/778))
([#784](https://github.com/chopratejas/headroom/issues/784))
([eb2e50f](eb2e50feb2))
* add Vertex AI proxy routing
([#793](https://github.com/chopratejas/headroom/issues/793))
([3c77e52](3c77e52ce4))
* **cli:** comprehensive help text, validation, and exception handling
improvements
([#640](https://github.com/chopratejas/headroom/issues/640))
([028efab](028efabb4e))
* compression safety rails — error-output protection, pipeline circuit
breaker, library inflation guard
([#851](https://github.com/chopratejas/headroom/issues/851))
([c0cadcc](c0cadccff9))
* **dashboard:** per-model savings breakdown and expected-vs-actual cost
on historical charts
([#807](https://github.com/chopratejas/headroom/issues/807))
([34dafe6](34dafe69d9))
* detect re-served tool results as over-compression waste signal
([#854](https://github.com/chopratejas/headroom/issues/854))
([5f1d88a](5f1d88ad27))
* **evals:** add zero-cost tool schema compaction integrity eval
([#817](https://github.com/chopratejas/headroom/issues/817))
([53a08c6](53a08c63bf))
* gated Markdown-KV compaction formatter (serialization-aware output)
([#859](https://github.com/chopratejas/headroom/issues/859))
([06b2625](06b2625b17))
* **kompress:** warn on unrecognized HEADROOM_KOMPRESS_BACKEND +
document backend selection
([#204](https://github.com/chopratejas/headroom/issues/204))
([6367d0b](6367d0b722))
* **memory:** add opt-in Apple-GPU (MPS) embedding runtime
([#766](https://github.com/chopratejas/headroom/issues/766))
([c71592d](c71592d421))
* net-cost cache mutation formula on CompressionPolicy
([#856](https://github.com/chopratejas/headroom/issues/856) P1)
([#857](https://github.com/chopratejas/headroom/issues/857))
([d5f5802](d5f58026e2))
* **plugins:** Hermes agent headroom_retrieve plugin
([#824](https://github.com/chopratejas/headroom/issues/824))
([058bced](058bcedab8))
* probe-based retention scoring of recorded compression events
([#862](https://github.com/chopratejas/headroom/issues/862))
([c2106cb](c2106cbdab))
* **proxy:** add CLI opt-outs for CCR injection (compression-only mode)
([#823](https://github.com/chopratejas/headroom/issues/823))
([693d9d2](693d9d20e2))
* **proxy:** attribute savings history rollups per provider
([#791](https://github.com/chopratejas/headroom/issues/791))
([0b8b8d9](0b8b8d92de))
* **proxy:** log compressed messages alongside original request
([#261](https://github.com/chopratejas/headroom/issues/261))
([2269e40](2269e40bde))
* **proxy:** per-project savings breakdown on the dashboard (claude,
codex, aider, copilot, cursor)
([#803](https://github.com/chopratejas/headroom/issues/803))
([914a60a](914a60a2b0))
* support Python 3.14+ via pyo3 abi3 stable ABI
([#516](https://github.com/chopratejas/headroom/issues/516))
([19eac8e](19eac8e00d))
* switch Kompress default to kompress-v2-base with weight-only int8 ONNX
([#799](https://github.com/chopratejas/headroom/issues/799))
([74392b2](74392b238e))
* **transforms:** attribute read_lifecycle + smart_crush tags
([#249](https://github.com/chopratejas/headroom/issues/249))
([8f37426](8f374263d3))


### Bug Fixes

* **anthropic:** CCR exception must re-raise, not silently swallow
([#838](https://github.com/chopratejas/headroom/issues/838))
([8db5efc](8db5efc6f9))
* **ccr:** key Rust search/diff/log markers with explicit_hash
([#852](https://github.com/chopratejas/headroom/issues/852))
([bfcb07d](bfcb07d78e))
* **ccr:** make retrieval TTL configurable
([#715](https://github.com/chopratejas/headroom/issues/715))
([2533f77](2533f7703e))
* **ccr:** skip CCR when model calls headroom_retrieve alongside user
tools ([#839](https://github.com/chopratejas/headroom/issues/839))
([30078f8](30078f8465))
* **ccr:** use shared compression store
([#875](https://github.com/chopratejas/headroom/issues/875))
([249af6c](249af6cc7b))
* **ci:** correct comments, timeouts, and pip reliability in native e2e
workflows ([#878](https://github.com/chopratejas/headroom/issues/878))
([b716c8c](b716c8c2ee))
* **ci:** pin cosign-installer to v3 (v4 does not exist)
([#774](https://github.com/chopratejas/headroom/issues/774))
([199d693](199d693f98))
* **codex:** respect CODEX_HOME for wrap config
([#731](https://github.com/chopratejas/headroom/issues/731))
([96abf38](96abf38b09))
* **content_router:** guard against empty compression output causing
Anthropic 400
([#771](https://github.com/chopratejas/headroom/issues/771))
([2f9ff07](2f9ff07e6c))
* **copilot:** use responses API for subscription reasoning models
([#647](https://github.com/chopratejas/headroom/issues/647))
([84ac332](84ac332d14))
* correct preserved-entry index mapping in Gemini content round-trip
([#836](https://github.com/chopratejas/headroom/issues/836))
([0ffe2b6](0ffe2b6ea4))
* **dashboard:** stable 'Proxy $ Saved' hero tile under --workers &gt; 1
([#481](https://github.com/chopratejas/headroom/issues/481))
([fd73b88](fd73b88368))
* don't inject empty tools:[] when client omitted the tools field
([#772](https://github.com/chopratejas/headroom/issues/772))
([574bbae](574bbae2cb))
* harden Copilot API auth token handling
([#557](https://github.com/chopratejas/headroom/issues/557))
([6b0c09f](6b0c09ffd5))
* **health:** readyz verifies upstream connectivity, not just process
liveness ([#744](https://github.com/chopratejas/headroom/issues/744))
([5dfb446](5dfb446da1))
* **init:** guard persistent task startup
([#616](https://github.com/chopratejas/headroom/issues/616))
([9252d85](9252d852c5))
* **init:** normalize Windows hook paths to forward slashes
([#788](https://github.com/chopratejas/headroom/issues/788))
([6ea6e31](6ea6e31f09))
* **init:** suppress hook recovery output
([#760](https://github.com/chopratejas/headroom/issues/760))
([b439599](b4395993ae))
* **learn:** claude-cli streams output with idle timeout
([#373](https://github.com/chopratejas/headroom/issues/373))
([9bff575](9bff5752bb))
* make headroom wrap readiness probe timeout configurable for slow ML
imports ([#581](https://github.com/chopratejas/headroom/issues/581))
([163677b](163677b405))
* **parser:** detect waste signals in Anthropic tool_result content
blocks ([#815](https://github.com/chopratejas/headroom/issues/815))
([929698a](929698af10))
* **proxy:** F4 — trust X-Forwarded-* only behind allow-listed gateway
([d10bd5f](d10bd5f59c))
* **proxy:** lazy-import server to avoid fastapi crash
([#442](https://github.com/chopratejas/headroom/issues/442))
([93c6937](93c69372e6))
* **proxy:** make CCR multi-worker warning conditional on backend
([#770](https://github.com/chopratejas/headroom/issues/770))
([d76a729](d76a7296df))
* **proxy:** make Kompress eager preload cache-only so a cold cache
can't block startup
([#783](https://github.com/chopratejas/headroom/issues/783))
([841663d](841663da16))
* **proxy:** restore Codex usage headers on WS and streaming SSE
transports ([#577](https://github.com/chopratejas/headroom/issues/577))
([#794](https://github.com/chopratejas/headroom/issues/794))
([0ce68de](0ce68dedd7))
* schema compaction must not drop property names that match DROP_KEYS
([#785](https://github.com/chopratejas/headroom/issues/785))
([ae2122f](ae2122fda8))
* **security:** block DNS-rebinding on /debug/* and /stats/reset via
Host-header allowlist
([#605](https://github.com/chopratejas/headroom/issues/605))
([b4b5025](b4b50253f1))
* **ssl:** upstream httpx client inherits SSL_CERT_FILE,
REQUESTS_CA_BUNDLE, NODE_EXTRA_CA_CERTS
([#745](https://github.com/chopratejas/headroom/issues/745))
([e50fbb3](e50fbb3e0d))
* suppress LiteLLM provider banner before import
([#874](https://github.com/chopratejas/headroom/issues/874))
([f9384ef](f9384ef4b7))
* **transforms:** use thread-local tree-sitter parsers to prevent pyo3
Unsendable panic
([#604](https://github.com/chopratejas/headroom/issues/604))
([2ad300a](2ad300aff8))
* **wrap:** track shared proxy clients with markers
([#877](https://github.com/chopratejas/headroom/issues/877))
([05bd56b](05bd56bcb6))


### Code Refactoring

* extract litellm model resolution to shared utility
([ec7d006](ec7d0065cc))
</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-11 22:18:46 -08:00
Logan Kang
c71592d421
feat(memory): add opt-in Apple-GPU (MPS) embedding runtime (#766)
## Description

On Apple-Silicon Macs — especially fanless models like the MacBook Air
(M5) — running the proxy with memory context injection can pin the CPU
while embedding. The embedding work runs an uncapped session on the CPU,
saturating multiple cores, which starves the proxy's asyncio loop and
leads to request timeouts.

This PR adds an **opt-in** runtime that offloads the memory embedder to
the Apple GPU (MPS). Setting `HEADROOM_EMBEDDER_RUNTIME=pytorch_mps`
routes embedding through the torch `sentence-transformers` backend on
MPS instead of the default ONNX CPU embedder, moving the work off the
CPU and keeping the proxy responsive.
The default behavior is unchanged — the feature is strictly opt-in,
env-var only, and falls through to the existing default embedder
selection (with a warning) whenever MPS or the torch dependencies are
unavailable.

Fixes: N/A — no tracking issue (surfaced while running codex auto-review
through
the proxy on a fanless MacBook Air (M5)).

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

- **Runtime selection** (`headroom/proxy/memory_handler.py`): read
`HEADROOM_EMBEDDER_RUNTIME`; when set to `pytorch_mps` **and** MPS is
actually available, route the memory embedder to the torch
`sentence-transformers` backend (Apple GPU).
- If MPS is unavailable or torch/sentence-transformers is not installed,
log a warning and fall through to the existing default embedder
selection (ONNX when available, else the pre-existing local
sentence-transformers fallback) — no crash. The default (env var unset)
is unchanged. Env-var only
- **MPS serialization** (`headroom/memory/adapters/embedders.py`):
`LocalEmbedder` now funnels every `encode()` through a dedicated
single-worker `ThreadPoolExecutor` when the resolved device is MPS.
torch-MPS is not thread-safe, and the existing `run_in_executor(None,
...)` dispatch would otherwise let concurrent proxy requests call MPS
from multiple threads. CPU/CUDA keep the shared default executor
(behavior unchanged). `close()` also drops the cached model so re-use
after close re-initializes cleanly.
- **Packaging** (`pyproject.toml`): new `pytorch-mps` extra (`torch` +
`sentence-transformers`), **platform-gated to macOS** (`; sys_platform
== 'darwin'`) since MPS is Apple-Silicon-only. Deliberately left out of
`[all]` (its deps already arrive via `[ml]`/`[memory]`).
- **Tests** (`tests/test_memory/test_embedder_mps_serialization.py`):
regression coverage for the serialized executor, concurrency safety (no
SIGABRT), CPU-path default behavior, and close/re-use re-initialization.
- **Docs**: `wiki/{configuration,memory,macos-deployment}.md`,
`docs/content/docs/{configuration,installation,memory}.mdx`,
`README.md`, `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 (CPU-offload + concurrency profiling on
Apple Silicon)

## Test Output

```
$ pytest -v tests/test_memory/test_embedder_mps_serialization.py
collected 4 items
tests/test_memory/test_embedder_mps_serialization.py::test_cpu_uses_shared_executor PASSED            [ 25%]
tests/test_memory/test_embedder_mps_serialization.py::test_mps_creates_single_worker_executor PASSED  [ 50%]
tests/test_memory/test_embedder_mps_serialization.py::test_mps_concurrent_embeds_do_not_crash PASSED  [ 75%]
tests/test_memory/test_embedder_mps_serialization.py::test_mps_reembed_after_close_recreates_executor PASSED [100%]
============================== 4 passed in 6.92s ===============================

$ ruff check headroom/memory/adapters/embedders.py headroom/proxy/memory_handler.py tests/test_memory/test_embedder_mps_serialization.py
All checks passed!

$ mypy headroom/memory/adapters/embedders.py headroom/proxy/memory_handler.py
Success: no issues found in 2 source files

$ pytest -q tests/test_memory/ tests/test_memory_handler_concurrent_init.py tests/test_memory_handler_native_ops.py
553 passed, 1 skipped in 13.51s
```

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

**Why MPS (and not CoreML or a thread cap):** measured on an
Apple-Silicon Mac, the default uncapped CPU embedding session saturates
the cores; the same model on MPS runs at a fraction of the CPU (≈8x
lower sustained CPU utilization in profiling) while producing
**byte-identical embeddings** (cosine distance ≈ 0 across runtimes), so
relevance/ranking is unchanged. A CoreML execution-provider path was
evaluated and rejected: the default optimized ONNX model uses fused ops
that fall back to CPU under CoreML (no offload), and a full-precision
re-export was impractical (very low throughput + multi-GB memory). MPS
via `sentence-transformers` was the only practical GPU offload.

**Why serialization is mandatory:** torch-MPS is not thread-safe —
concurrent encode calls from a multi-worker executor abort with
`-[IOGPUMetalCommandBuffer validate]: failed assertion 'commit an
already committed command buffer'` (reproduced deterministically; a
single-worker executor resolves it).
Under concurrent load the serialized single-GPU-stream throughput meets
or exceeds the parallel CPU path while using a fraction of the cores.

**Scope / boundary:** this targets the Python **memory** embedder, which
is live on the proxy request path (memory context injection). The
Rust-backed SmartCrusher compression path is unaffected and remains
non-configurable from Python by design.

**Safety:** default behavior is unchanged (ONNX, no torch). The feature
is opt-in, env-var only, macOS-gated at the packaging layer, and
degrades gracefully (warn + the existing default embedder selection)
when MPS or the dependencies are unavailable.
2026-06-11 12:59:20 -05:00
Patrick A
2ad300aff8
fix(transforms): use thread-local tree-sitter parsers to prevent pyo3 Unsendable panic (#604)
## Problem

pyo3 marks `_native::Parser` as `#[pyclass(unsendable)]`, which causes a
hard thread-assertion panic when a parser created on one thread is
accessed from another:

```
thread '<unnamed>' panicked at pyo3-0.28.3/src/impl_/pyclass.rs:1055:9:
assertion `left == right` failed: _native::Parser is unsendable, but sent to another thread
  left: ThreadId(2)
 right: ThreadId(1)
```

The prior implementation stored parsers in a module-level `dict[str,
Any]` (`_tree_sitter_languages`). When `_run_compression_in_executor`
dispatched compression work to a `ThreadPoolExecutor`, pool workers
grabbed parsers from that shared dict that were originally created on
the main asyncio thread and panicked.

This produces a 500 on every request where code compression is attempted
via a pool thread.

## Fix

Replace the global dict with `threading.local()` so each thread creates
and owns its own parser instances. No cross-thread parser access is
possible.

```python
# before
_tree_sitter_languages: dict[str, Any] = {}  # shared — crosses threads

# after
_tree_sitter_local = threading.local()  # per-thread — isolated
```

`is_tree_sitter_loaded()` and `unload_tree_sitter()` updated to operate
on the current thread's local cache (semantics unchanged for
single-threaded callers).

## Tests

9 regression tests added in
`tests/test_transforms/test_tree_sitter_thread_safety.py`:

- Thread isolation: two threads get distinct parser instances
- Within-thread reuse: same thread gets the same cached instance
- Thread pool: parsers usable from `ThreadPoolExecutor` workers without
panic
- Concurrent workers: each distinct pool thread owns a unique parser
- `is_tree_sitter_loaded` / `unload_tree_sitter` lifecycle

Also adds a `filterwarnings` entry for
`PytestUnraisableExceptionWarning`: pyo3 emits this when short-lived
test threads drop parsers at teardown; it does not occur in production
where pool threads are long-lived.

## Relation to #564

PR #564 proposes the same `threading.local()` approach but was blocked
on missing tests (`CHANGES_REQUESTED`). This PR includes the full test
suite.
2026-06-10 18:30:00 -05:00
Frank Borkin
19eac8e00d
feat: support Python 3.14+ via pyo3 abi3 stable ABI (#516)
## Description

Sets pyo3 params to support python above 3.13

Fixes #(408

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

- Updated Cargo.toml

## Testing

Describe the tests you ran to verify your changes:

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

```
Compiles
```

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

Co-authored-by: Tejas Chopra <chopratejas@gmail.com>
2026-06-09 06:33:16 -08:00
Patrick A
9579567b7d
chore(deps): loosen over-pinned constraints and add upper bounds (#538)
## What

Loosen over-pinned Python dependency constraints and add missing upper
bounds in `pyproject.toml`. Also bump the neo4j Docker image and uv
builder version.

## Why

Several dependencies had constraints that either blocked security
patches or allowed silent major-version jumps:

- `litellm==1.82.3` was an exact pin — every security patch release
requires a manual lockfile bump
- `transformers`, `sentence-transformers` had no upper bound and have
already crossed major version boundaries without a constraint gate
- `neo4j>=5.20.0` had no upper cap; the driver has already reached 6.x
in the wild
- `mem0ai>=0.1.100` had a pre-1.0 floor while the locked version is
already 1.0.11
- `langchain-core`, `langchain-openai`, `qdrant-client`, `uvicorn` had
no upper bound on a range with active major-version churn
- `docker-compose.yml` pinned neo4j at `5.15.0`, which is 11 patch
releases behind the current 5.x LTS
- `Dockerfile` pinned uv at `0.11.16`; latest stable is `0.11.18`

## How

Constraint changes only — no code changes, no `uv lock --upgrade`. The
existing locked versions all satisfy the new bounds (we added caps, not
floors). `uv` re-resolved the lockfile to format revision 3 (adds
`upload-time` metadata fields) and cleaned up the defunct `llmlingua`
extra entries.

| Dependency | Before | After |
|---|---|---|
| `litellm` | `==1.82.3` | `>=1.82.3,<2.0` |
| `transformers` | `>=4.30.0` | `>=4.30.0,<6.0` |
| `sentence-transformers` | `>=2.2.0` | `>=2.2.0,<6.0` |
| `neo4j` | `>=5.20.0` | `>=5.20.0,<7.0` |
| `mem0ai` | `>=0.1.100` | `>=1.0.0,<2.0` |
| `langchain-core` | `>=0.2.0` | `>=0.2.0,<4.0` |
| `langchain-openai` | `>=0.1.0` | `>=0.1.0,<2.0` |
| `qdrant-client` | `>=1.9.0` | `>=1.9.0,<2.0` |
| `uvicorn` | `>=0.23.0` | `>=0.23.0,<1.0` |
| neo4j Docker image | `5.15.0` | `5.26` |
| uv (Dockerfile ARG) | `0.11.16` | `0.11.18` |

## Breaking changes

None. All currently installed versions fall within the new ranges.
Installers that previously resolved `litellm` to an older exact pin may
now resolve newer patch releases — which is the desired behavior.

---------

Co-authored-by: Tejas Chopra <chopratejas@gmail.com>
2026-06-08 22:06:24 -08:00
github-actions[bot]
01762b1ec7
chore: release main (#607)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-08 11:21:23 -07:00
Patrick A
fa558c5647
fix(deps): move gunicorn to [proxy-prod] extra, add Windows guard (#537)
* fix(deps): add missing runtime deps to [code] and [proxy] extras

- Add gunicorn>=21.0.0 to the [proxy] extra

The proxy docs (docs/content/docs/proxy.mdx and wiki/proxy.md) show
gunicorn as the recommended production deployment server:

  pip install gunicorn
  gunicorn headroom.proxy.server:app --worker-class uvicorn.workers.UvicornWorker

Users installing headroom-ai[proxy] for production get uvicorn (already
declared) but had to discover and install gunicorn manually. Adding it
to [proxy] removes that friction.

Investigation notes:
- [code] only needs tree-sitter-language-pack (already declared).
  code_compressor.py has zero numpy imports. The kompress fallback
  inside code_compressor.py is guarded by ImportError and requires [ml].
- numpy is correctly declared in [relevance] (numpy>=1.24.0) and pulled
  transitively by sentence-transformers in [memory]. It is NOT needed
  under [code].
- tree-sitter is a transitive dep of tree-sitter-language-pack (requires
  tree-sitter>=0.25.2) so it does not need an explicit entry.

* docs(changelog): add entry for gunicorn proxy dep fix

style(tests): ruff format test_provider_proxy_routes.py (blank lines after docstrings)

* fix(deps): move gunicorn to [proxy-prod] extra, add Windows guard

- Remove gunicorn from [proxy] so dev, CI, and Windows users are not
  forced to install a Unix-only package that does nothing on Windows
- Add new [proxy-prod] extra that includes [proxy] + gunicorn with a
  sys_platform != 'win32' environment marker
- Production users: pip install 'headroom-ai[proxy,proxy-prod]'
- Update CHANGELOG to reflect the new extra name

* fix(devcontainer): bump uv floor to >=0.11.0 for lockfile compatibility

uv 0.6.17 (previously pinned) cannot parse lockfiles generated by
uv >= 0.11.x. The validate CI job (triggered by pyproject.toml
changes) was failing with 'Failed to parse uv.lock'. Loosening the
pin to >=0.11.0 picks up the matching format parser while keeping the
Docker layer cacheable with a range rather than an exact pin.

* fix(devcontainer): skip gitpython wheel filename check in uv sync

gitpython 3.1.47 on PyPI has wheel gitpython-3.1.46-py3-none-any.whl
(wrong filename). uv >=0.11.19 strict filename validation rejects this
lockfile entry. UV_SKIP_WHEEL_FILENAME_CHECK=1 bypasses the check until
the upstream lockfile is regenerated with a corrected entry.

* fix(deps): correct gitpython version in uv.lock to match actual wheel

gitpython 3.1.47 on PyPI was uploaded with sdist/wheel files named
gitpython-3.1.46.*. The version field in uv.lock said 3.1.47 but all
download URLs reference 3.1.46 files, causing uv >=0.11.19 to refuse
to parse the lockfile with a version-mismatch error.

Change the version field to 3.1.46 so the entry is internally
consistent. Also revert the now-unnecessary UV_SKIP_WHEEL_FILENAME_CHECK
workaround from post-create.sh.

---------

Co-authored-by: Tejas Chopra <chopratejas@gmail.com>
2026-06-07 23:49:14 -07:00
github-actions[bot]
f7c2552264
chore: release main 2026-06-04 14:05:56 +00:00
github-actions[bot]
cc71e07d01
chore: release main 2026-05-26 03:54:25 +00:00
chopratejas
8e4ab187e9 ci(release): align manifest + pyproject + package.json to 0.22.3
The repo had drifted: pyproject.toml said 0.9.1 but PyPI's latest
published headroom-ai was 0.22.3. release_version.py papered over
this by taking max(canonical, latest_tag) at release time;
release-please does NOT do that — it trusts the manifest verbatim.

Left as-is, release-please would propose 0.9.2 on the next merge
and PyPI would reject it ("400 Cannot publish version lower than
latest"), looping the bot forever.

Fix: align every version-bearing file to 0.22.3 (the truth on
PyPI). Done via `scripts/version-sync.py --version 0.22.3`:

- .release-please-manifest.json
- pyproject.toml
- sdk/typescript/package.json
- plugins/openclaw/package.json (+ headroom-ai dep range -> ^0.22.3)
- .claude-plugin/marketplace.json
- .github/plugin/marketplace.json
- plugins/headroom-agent-hooks/.claude-plugin/plugin.json
- plugins/headroom-agent-hooks/.github/plugin/plugin.json

After this lands, the bot's next release PR will propose 0.22.4
(patch) or 0.23.0 (minor) depending on conventional-commit traffic
since v0.22.3.
2026-05-25 18:41:38 -07:00
chopratejas
20dc1f28f3 fix(proxy): Strands MCP bundle + backend path fixes + Codex fail-closed protection
Three logically-related sets of proxy changes ship in this branch:

1. Strands integration on the Bedrock path (HeadroomBundle + 4 OpenAI
   handler fixes + LiteLLM cache stats + dep pin)
2. /stats MCP aggregation (cross-process events log → proxy summary)
3. Codex compression-failure fail-closed (WS + HTTP /v1/responses)

== 1. Strands integration on the Bedrock path ==

* HeadroomBundle (headroom/integrations/strands/bundle.py): single-helper
  MCP wiring for a Strands Agent — Headroom MCP server (headroom_compress
  / headroom_retrieve / headroom_stats) plus optional Serena MCP and
  optional in-process compression hook. Constructor builds unstarted
  MCPClient instances per server; Strands' Agent owns the subprocess
  lifecycle. Default config: MCP enabled, Serena enabled, hook OFF
  (proxy is the single source of truth for compression). User-side
  integration is two lines in any Strands app.

* headroom/proxy/handlers/openai.py — backend path now:
  - calls PrefixCacheTracker.update_from_response (was direct-OpenAI only)
  - intercepts CCR headroom_retrieve tool_calls server-side, mirroring
    the Anthropic handler pattern; NO silent fallback, re-raises on
    CCR errors (per feedback_no_silent_fallbacks)
  - works for both non-streaming and streaming paths

* headroom/proxy/handlers/streaming.py: _stream_openai_via_backend now
  accepts prefix_tracker + optimized_messages, parses cache stats from
  the SSE final-usage frame (cache_creation_input_tokens added to the
  state machine), records CCR retrieve feedback via a new
  _record_ccr_feedback_from_openai_sse helper. Streaming CCR intercept
  is intentionally out of scope (mirrors Anthropic streaming behaviour).

* headroom/backends/litellm.py: send_openai_message response usage block
  now carries cache_read_input_tokens / cache_creation_input_tokens
  (Anthropic/Bedrock dialect) and prompt_tokens_details.cached_tokens
  (OpenAI dialect). Backwards-compatible — cold-start callers see the
  same 3-key shape; cache keys appear only when the underlying provider
  returns them. Pinned by test_no_cache_fields_means_no_cache_keys.

* headroom/proxy/auth_mode.py: ("strands-agents/", "strands") added to
  CLIENT_UA_MAP. Production callers should also set X-Client: strands
  since the default openai-python UA carries no Strands signal.

* pyproject.toml: huggingface-hub>=1.5.0,<2.0 pinned in [ml] so a sibling
  install (e.g. strands-agents) can't drag the version below the floor
  transformers 5.x requires (otherwise Kompress silently goes
  "unavailable").

== 2. /stats MCP aggregation ==

* headroom/proxy/cost.py: _aggregate_mcp_events() reads the cross-process
  shared events file the Headroom MCP server already writes to and
  surfaces summary.mcp with three new keys:
    - compressions       (count of headroom_compress invocations)
    - tokens_removed     (sum of input - output across those)
    - retrievals         (count of headroom_retrieve — the load-bearing
                          over-compression alarm; if it grows linearly
                          with turn count, lossy compressors are
                          dropping info the model actually needs)
  Defensive on every axis — missing MCP SDK, missing file, malformed
  events, read errors — never blocks /stats.

* examples/strands_bundle_demo.py: stats panel prints the new fields so
  the demo shows the full proxy-HTTP + MCP-tool story in one view.

== 3. Codex compression-failure fail-closed protection ==

Reported by Camille (2026-05-21): Codex threads were locking with
"ran out of room in the model's context window" after Headroom's
compression timed out on an oversized response.create frame and
forwarded the original ~1.7 MB frame to the upstream, which then
rejected it. Codex's auto-compact heuristic gates on the upstream-
reported total_usage_tokens (which Headroom had been shrinking on
earlier turns), so its compaction never fired and the thread locked.

Validated against open Codex issues (CLI + Desktop share codex-rs/core):
* #16068 — confirms compaction gates on total_usage_tokens,
  estimated_token_count is computed but only logged
* #19806 — confirms image token estimator unbounded, contributes to
  the same ContextManager.get_total_token_usage → auto-compaction chain

* headroom/proxy/helpers.py: decide_compression_failure_action() with a
  unit-tested decision matrix:
    - asyncio.TimeoutError                              → refuse, always
    - non-timeout failure + frame > 256 KiB (configurable) → refuse
    - non-timeout failure + small frame                 → forward (legacy)
  Operator escape hatches:
    - HEADROOM_WS_FAIL_OPEN_ON_COMPRESSION_FAILURE=1 restores legacy
    - HEADROOM_WS_COMPRESSION_FAIL_THRESHOLD_BYTES tunes the threshold

* headroom/proxy/handlers/openai.py (WS /v1/responses): consults the
  helper after compression failure. On refuse: close client websocket
  code 1009 with "headroom: compression <reason> — please compact
  context and retry" reason; set termination_cause for the outer
  lifecycle finally; return.

* headroom/proxy/handlers/openai.py (HTTP /v1/responses): same helper.
  On refuse: raise HTTPException(413) with a structured error body so
  FastAPI's HTTPException handler emits a clean 413. The existing
  `except HTTPException: raise` guard in this handler already ensures
  the 413 propagates without being swallowed by the 502 catch-all.

Anthropic /v1/messages NOT changed in this branch: no equivalent bug
report on Anthropic-protocol clients, Claude Code (Anthropic-owned)
handles context overflow via its own cache_control/ephemeral
primitives, and Cursor/Aider don't maintain the local-Y estimate the
Codex bug requires. Deferred until a real report lands; the patch is
a one-liner reusing the same helper.

== Tests + verification ==

* tests/test_backends/test_litellm_cache_stats.py — 3 tests pinning
  cache-stat surfacing across Anthropic/OpenAI dialects + backwards-
  compat for no-cache responses.
* tests/test_proxy/test_openai_backend_path.py — 5 tests (Bedrock cache
  fields, OpenAI fallback shape, CCR intercept with provider="openai",
  CCR re-raise on exception, streaming signature contract).
* tests/test_proxy/test_mcp_stats_aggregation.py — 5 tests pinning the
  aggregator across compress+retrieve mixes, empty events, unknown event
  types, missing token fields, and read failures.
* tests/test_proxy/test_compression_failure_action.py — 12 tests pinning
  the fail-closed decision matrix (timeout always refuses, small
  transient passes through, oversize refuses, env override variants,
  custom threshold, invalid threshold falls back, 0/negative ignored).

* examples/strands_bedrock_demo.py — model_id bumped from deprecated
  Claude 3 Haiku to Sonnet 4.5 (the deprecated model now errors on
  account access).
* examples/strands_via_proxy_demo.py — proxy + Bedrock cache + streaming
  smoke test.
* examples/strands_mcp_dispatch_test.py — pure MCP round-trip probe.
* examples/strands_bundle_demo.py — full Strands + HeadroomBundle E2E
  demo (this is the shape a real Strands user copies into their app).

Full pytest: 5327 passed, 178 skipped. The previously-failing
test_core_operations.py::TestAddBatch::test_add_batch_basic passes now
that the huggingface-hub pin in pyproject.toml unblocks transformers
imports.

E2E verified live against AWS Bedrock (Sonnet 4.5):
* cache_write=10,438 on turn A → cache_read=10,438 on turn B
* streaming SSE final usage frame carries cache_read_input_tokens
* 78.7% reduction on a 50 KB JSON tool_result via SmartCrusher (
  dispatched per-content-type by ContentRouter)
* Strands Agent + HeadroomBundle: model autonomously called
  headroom_compress + headroom_retrieve via MCP; CompressionStore
  round-trip succeeded; final answer correct.
2026-05-21 11:00:14 -07:00
chopratejas
c1d2eec588 docs: improve discoverability for AI agents and search crawlers
Several signals AI agents and search engines use to discover and
install a project were misaligned or missing:

* ``docs/app/layout.tsx`` set ``metadataBase`` to
  ``https://chopratejas.github.io/headroom/`` while the live docs run
  on Vercel — every page's ``og:url`` and ``twitter:url`` resolved to
  a URL that returns 404 for ``/llms.txt``. Now points at the live
  Vercel host (overridable via ``NEXT_PUBLIC_SITE_URL`` for a future
  custom domain). Adds explicit ``openGraph`` and ``twitter`` metadata
  so social shares render a card with the project's pitch.
* No ``llms.txt`` at the GitHub repo root. AI agents crawling
  ``github.com/chopratejas/headroom/`` saw only the README. The new
  ``llms.txt`` follows the llmstxt.org convention: 1-line pitch,
  canonical docs links, copy-paste install commands (pip / npm /
  Docker / proxy / ``headroom wrap``), and entry points for the
  library, proxy, MCP server, and SDK integrations. Points at the
  Fumadocs-generated ``/llms.txt`` and ``/llms-full.txt`` for the
  full picture.
* ``pyproject.toml`` ``Documentation`` URL pointed at the GitHub
  README anchor. Updated to point at the docs site so PyPI visitors
  land on searchable docs, and adds an ``AI / LLM Index`` URL
  pointing at the Fumadocs ``/llms.txt``.
* No explicit AI-bot allow list. Added ``docs/app/robots.ts`` (Next
  13+ App Router convention) with explicit allows for GPTBot,
  ClaudeBot, PerplexityBot, Google-Extended, OAI-SearchBot,
  ChatGPT-User, Cohere-AI, CCBot, and Applebot-Extended. Wildcard
  allow as the catch-all. Advertises the sitemap.
* No ``sitemap.xml`` route. Added ``docs/app/sitemap.ts`` that pulls
  every Fumadocs page out of ``source`` (same source backing
  ``/llms.txt``, search, and OG images) so search and AI crawlers
  can enumerate doc pages without scraping HTML.
* README didn't tell AI agents where to look. Added a 2-line
  pointer near the top nav row: read ``/llms.txt`` here, or fetch
  the live index / full docs blob.

Also tightened the GitHub repo description and added five topics
(``claude-code``, ``cursor``, ``tokens``, ``prompt-engineering``,
``typescript``) via ``gh repo edit`` — that's already live on the
repo, not part of this commit.

No Python or Rust code changes; ``make ci-precheck`` was run to
confirm the test slice still passes.
2026-05-13 17:36:06 -07:00
JerrettDavis
2fd48260f6 ci: skip live evals without credentials 2026-05-11 13:35:24 -05:00
Tejas Chopra
9ff28e9803
Merge pull request #422 from chopratejas/fix-user-experience-and-feature-clarity
fix(cli): proxy/perf/wrap UX cleanup + perf --hours correctness
2026-05-07 16:46:55 -07:00
chopratejas
265554d4ad fix(cli): proxy/perf/wrap UX cleanup + perf --hours correctness
Address user-reported UX gaps across the CLI surface:

- code-aware: add --code-aware/--no-code-aware (+ HEADROOM_CODE_AWARE_ENABLED env)
  to the Click CLI. PR #411 had added these only to the orphaned argparse main;
  the user-facing CLI couldn't reach the flag. Banner status text "remove
  --no-code-aware to enable" referenced a flag that didn't exist — fix to point
  at the actual flag/env. Surface code-aware in the click banner and add
  print_banner=False plumbing to run_server so the click path doesn't print
  two banners back-to-back.

- --mode: hide alias clutter via metavar=[token|cache] and rewrite help to
  lead with the two real modes. Legacy aliases (token_mode/token_savings/...)
  still validate.

- perf --hours: was documented but ignored. Records are now actually filtered,
  the report shows the actual time-range covered, and the count of records
  filtered out (so users can tell when raising --hours helps).

- perf TOIN: replace the hash-keyed pattern dump with a strategy-distribution
  view + recommendation-eligibility from the live store — actionable signal
  rather than opaque rows.

- code-graph: clarify in --help that it indexes cwd / project root.

- wrap: spell out supported tools, wrap-vs-proxy distinction, and that
  `headroom wrap opencode` isn't a thing (use `proxy` directly for opencode;
  openclaw is not opencode).

- mcp: note that mcp__headroom__headroom_retrieve is correct MCP namespacing,
  not a doubled-prefix bug. Renaming would break the proxy's tool injection.

- LLMLingua cleanup: remove [llmlingua] extra from pyproject (no live code
  uses it). Delete wiki/llmlingua.md and clean retired flag/class references
  in 6 other wiki pages. Point at [ml] (Kompress) where ML compression is
  documented.

- init -g openclaw: strip mcpServers from existing plugin entries before
  re-writing — newer openclaw schemas reject it, leaving stale entries from
  older installs unhealable. Pinned with regression test.

Tests: mock_run_server signatures in two existing tests accept **kwargs
(needed for the new print_banner plumbing). New test for the openclaw
mcpServers strip. Full suite: 4847 passed, 262 skipped, 0 failed.
2026-05-07 16:43:35 -07:00
chopratejas
183d51c8a8 fix(ci): include NOTICE in sdist + assert License-File metadata matches tarball
Every release since v0.20.16 has uploaded 12 wheels but no sdist. The
underlying failure is a 400 from PyPI:

    400 License-File NOTICE does not exist in distribution file
    headroom_ai-X.Y.Z.tar.gz at headroom_ai-X.Y.Z/NOTICE

Two-part regression:

1. The hatch -> maturin migration in 2a91cbb (single-wheel maturin build
   backend, May 4) replaced `[tool.hatch.build.targets.sdist].include`,
   which listed both `LICENSE` and `NOTICE`, with maturin's own include
   directive that only carried `LICENSE` over. Maturin's PEP 639 license
   auto-discovery still emits `License-File: NOTICE` into the sdist's
   PKG-INFO (because NOTICE exists at the project root and matches the
   default glob), so the sdist tarball declares a license file it
   doesn't physically contain. PyPI's PEP 639 validator rejects with
   400. Wheels were unaffected because maturin auto-injects both files
   into `*.dist-info/licenses/`.

2. CI showed "publish-pypi" green for ~22 releases despite this break
   because twine was bailing earlier with `400 File already exists` on
   the wheels (the version detector kept computing the same v0.21.5).
   PR #412 added `skip-existing: true` (May 6) to make wheel re-uploads
   idempotent. With wheels now silently skipping, twine proceeded to
   upload the sdist for the first time in three weeks - and the
   dormant License-File error surfaced as a hard 400.

Fix:

- Add `NOTICE` alongside `LICENSE` in `[tool.maturin].include` for the
  `sdist` format. Both files now ship in the tarball, matching what
  PEP 639 already declares in PKG-INFO.
- Replace the existing "verify sdist contains LICENSE" check with a
  generic "every License-File entry in PKG-INFO resolves to a real
  tarball member" check. This catches the same bug class for any
  future addition (COPYING, AUTHORS, etc.) without another bespoke
  literal.

Verified locally:

    $ maturin sdist --out dist
    Including license file `LICENSE`
    Including license file `NOTICE`
    Including files matching "LICENSE"
    Including files matching "NOTICE"
    Built source distribution to dist/headroom_ai-0.9.1.tar.gz

    $ tar -tzf dist/headroom_ai-0.9.1.tar.gz | grep -E '(LICENSE|NOTICE)$'
    headroom_ai-0.9.1/LICENSE
    headroom_ai-0.9.1/NOTICE

    $ twine check dist/headroom_ai-0.9.1.tar.gz
    Checking dist/headroom_ai-0.9.1.tar.gz: PASSED
2026-05-07 16:29:05 -07:00
Daniel Munoz
9492386398 fix: harden release gating and clarify pipx compatibility 2026-05-06 12:41:17 +02:00
chopratejas
b154e17853 fix: PR #372 — restore [image] extra on Python 3.13 via rapidocr 3.x adapter
Root cause: `headroom-ai[all]==0.20.16` fails to install on Python 3.13
because `rapidocr-onnxruntime` 1.4.0–1.4.4 wheels declare
`requires-python: <3.13,>=3.6`. After 1.4.x the rapidocr ecosystem
split: `rapidocr-onnxruntime` (bundled-ORT, capped at <3.13) vs
`rapidocr` 3.x (engine-agnostic core, supports 3.13+, returns
RapidOCROutput dataclass instead of v1's tuple).

Fix:

1. pyproject.toml — environment-marker hybrid in [image]:
   - rapidocr-onnxruntime>=1.4.0,<2; python_version<'3.13'
   - rapidocr>=3.0,<4; python_version>='3.13'
   - onnxruntime>=1.7,<2; python_version>='3.13'
   ORT remains the engine on every Python version; bundle and speed
   unchanged, just split into two packages on 3.13+.

2. headroom/image/compressor.py — runtime adapter:
   _resolve_rapidocr() tries v1 first, falls back to v3 when v1 is
   missing, returns (None, None) when neither installed. Cached at
   module scope. Detection at runtime (not Python-version-based) so
   users can install either package on any Python version.

   _ocr_extract branches on resolved api_version:
   - v1: (list[(box, text, score)], elapsed) tuple — unchanged
   - v3: RapidOCROutput dataclass with .txts / .scores / .boxes
     attrs (each may be None when nothing detected)

   Defensive None-handling, length-mismatch detection, structured
   log events for both branches.

Smoke test (real install verified before commit):
    pip install rapidocr onnxruntime pillow
    → result type: RapidOCROutput
    → fields: txts (None when empty), scores (None when empty), boxes
    Confirms the v3 None-coercion is necessary.

Tests: 11 new unit tests in tests/test_image_ocr_api_compat.py covering:
- Resolver: v1 preferred, v3 fallback, both missing
- v1 path: tuple parses, low-confidence None, empty result None
- v3 path: dataclass parses, low-confidence None, None attrs handled,
  mismatched lengths logged + None
- Backend missing: returns None gracefully

All 11 pass; `make ci-precheck` PASSED.

Closes #372.
2026-05-04 08:20:01 -07:00
chopratejas
e325d1b866 fix(ci): pin public PyPI in pyproject.toml + scrub Netflix URLs from uv.lock
Devcontainer validate jobs were failing on PR #360 with:

    × Failed to fetch:
      https://pypi.netflix.net/packages/.../nvidia_nvshmem_cu12-3.4.5-...whl
    ├─▶ Request failed after 3 retries
    ╰─▶ operation timed out

`pypi.netflix.net` is Netflix's internal PyPI mirror. It got into the
lockfile because my local `~/.config/uv/uv.toml` had:

    index-url = "https://pypi.netflix.net/simple"

Running `uv lock` from that machine baked Netflix-internal URLs into
uv.lock for every package. Public CI runners (and any external
contributor) can't resolve them.

Two fixes:

1. Add `[[tool.uv.index]]` block to pyproject.toml pinning public PyPI
   as the project's default index. uv now ignores user-level config when
   resolving for this project, regardless of who runs `uv lock`. This
   prevents the same contamination from any developer in the future.

2. Regenerate uv.lock against public PyPI. All package URLs now point
   at `https://files.pythonhosted.org/...` and `https://pypi.org/simple/`.
   Zero references to `pypi.netflix.net` remain in the lockfile.

Verified: `grep -c "pypi.netflix" uv.lock` returns 0.
2026-05-03 13:52:16 -07:00
chopratejas
2a91cbb4b4 refactor: single-wheel maturin build backend (fixes #355)
Eliminates the dual-package architecture that was the root cause of #355.
`pip install headroom-ai` now produces ONE wheel containing both the Python
source (headroom/*.py) and the compiled Rust extension (headroom/_core.so).
No more separate `headroom-core-py` package, no more chicken-and-egg with
PyPI publication, no more wheelhouse / PIP_FIND_LINKS / composite-action
plumbing in CI.

This is the canonical pattern used by cryptography, polars, ruff,
pydantic-core, and other Rust-as-core Python packages. Honors the
"Rust as core engine" direction.

## What changed

- pyproject.toml: `[build-system]` swapped from hatchling to maturin.
  `[tool.hatch.*]` deleted; `[tool.maturin]` added pointing at
  `crates/headroom-py/Cargo.toml` for the cdylib. `python-source = "."`
  picks up the root `headroom/` package directly (dashboard HTML
  templates and other non-Python files included automatically).
- crates/headroom-py/pyproject.toml: deleted. The crate is no longer a
  separate published package; its Cargo.toml stays as the cdylib build
  target invoked via `[tool.maturin] manifest-path`.
- crates/headroom-py/python/: deleted (placeholder layout for the old
  separate package).

## CI updates

- ci.yml: `test` / `test-extras` / `test-agno` jobs simplified — Rust
  toolchain set up before `pip install -e .` (which now invokes maturin
  via build-system). Removed the "build wheel + symlink .so" dance.
  `build` job swapped from `python -m build` (hatch) to
  `maturin build` + `maturin sdist`.
- release.yml: collapsed dual-package matrix into one. New `build-wheels`
  matrix produces cross-platform wheels for cp310/11/12/13 ×
  {linux x86_64, linux aarch64, macos x86_64, macos aarch64}. New
  `collect-dist` aggregator merges artifacts. publish-pypi consumes the
  merged dist.
- init-native-e2e.yml: dropped windows-latest from the matrix —
  upstream `esaxx-rs` (/MT) and `ort-sys` (/MD) link with conflicting
  MSVC C runtime libraries, so the Rust extension cannot build for
  win_amd64 today. Tracked as a follow-up; not a blocker for Linux+macOS.
- headroom-e2e-setup: composite action now sets up Rust toolchain +
  Swatinem/rust-cache before `pip install -e .[proxy]`.
- eval.yml, publish.yml, rust.yml: same pattern — rust toolchain before
  install. rust.yml's wheels job builds from root pyproject.toml (no
  more `-m crates/headroom-py/Cargo.toml`).
- e2e/init/Dockerfile, e2e/wrap/Dockerfile: install rust + maturin in
  the build stage; copy `crates/` + workspace `Cargo.toml/lock` so the
  install can build the extension. Dropped `HEADROOM_REQUIRE_RUST_CORE=false`
  from wrap-e2e — the image now ships the full Rust core.
- Dockerfile (main): simplified — no more Layer 2/3 dance with
  `headroom-core-py` install + symlink. Single `uv pip install` builds
  + installs everything.
- .devcontainer/Dockerfile: rust toolchain + libssl-dev + maturin
  added so `uv sync` builds the extension inside the devcontainer.

## Lockfile + script

- uv.lock: regenerated. No `headroom-core-py` entries remain.
- scripts/build_rust_extension.sh: simplified from a symlink-into-tree
  workaround to a thin wrapper around `pip install -e .`. The maturin
  build-backend handles placement automatically.

## Local validation (all green on macOS aarch64)

1. Clean venv `pip install -e .` → `from headroom._core import …` works.
2. `maturin build --release` → 13.8 MB wheel, 336 files including
   `headroom/_core.cpython-311-darwin.so` (32 MB cdylib) and
   `headroom/dashboard/templates/dashboard.html`.
3. `pip install <wheel>` in fresh venv → import works.
4. Wheel contents verified via `unzip -l`.
5. `pytest tests/test_transforms/test_diff_compressor.py` — 29 passed.
6. `pytest tests/test_relevance.py` — 30 passed.
7. `cargo build --workspace` + `cargo test --workspace` — all green.
8. `make ci-precheck` — 176 Python tests + Rust + commitlint green.

## Migration notes

Users on `pip install headroom-ai` get the Rust core automatically
(linux + macos wheels). sdist installs require rust toolchain available
locally — pip will build via maturin.

Closes #355
Supersedes #357 (workarounds-based fix abandoned in favor of
architectural fix)
2026-05-03 13:16:41 -07:00
chopratejas
dcbc921d63 fix: Wave 3 — multi-turn live integration tests for A+B realignment
Adds tests/test_realignment_live_multi_turn.py with 9 OPT-IN live tests
that validate the load-bearing claims of the Phase A+B megamerge against
real upstream APIs (Anthropic, OpenAI, Gemini). Each test maps to one or
more realignment PRs:

  1. test_anthropic_cache_hit_across_two_turns          — A2/A6/E
       Identical cache_control'd system+messages on two turns must
       eventually produce cache_read_input_tokens > 0. Guards the cache
       hot zone invariant (I2): proxy must not mutate frozen prefix bytes.
       Uses a bounded retry loop (max 4 attempts) to absorb Anthropic's
       eventually-consistent prompt-cache write latency without masking
       a real "proxy broke cache stability" regression.

  2. test_anthropic_cache_stable_when_live_zone_compresses — B2/B3
       Turn 2 mutates only the LATEST user content (8KB+ JSON tail);
       cache_read on turn 2 must still be > 0 AND the proxy must emit
       compression headers — proving the live-zone block dispatcher
       ran on the new tail without disturbing the cached prefix.

  3. test_anthropic_cache_control_passthrough_byte_faithful — A3/A4
       Wraps proxy._retry_request to snapshot the upstream-bound body
       and assert cache_control on system blocks survives verbatim,
       and user content is not flattened from list to string form.

  4. test_openai_chat_completions_multi_turn_through_proxy — A8/B
       Three-turn conversation through /v1/chat/completions; each
       turn returns valid content, prior assistant turns survive in
       the messages list (proxy doesn't drop them).

  5. test_openai_streaming_sse_chunks_arrive_in_order   — A8 (SSE wire)
       Streams /v1/chat/completions; asserts each event is
       'data: ...\\n\\n', terminator is 'data: [DONE]\\n\\n',
       reassembled content non-empty, no malformed events.

  6. test_gemini_multi_turn_through_proxy               — Gemini reach
       Two-turn conversation through native
       /v1beta/models/{model}:generateContent. Proves Gemini handler
       wiring stayed intact through the megamerge.

  7. test_ccr_marker_round_trip_live                    — B7 (CCR)
       Pre-populates compression_store with a fixture entry, embeds
       a CCR marker on a tool_result, verifies (a) headroom_retrieve
       tool is injected into the upstream tools array (PR-B7
       always-on), and (b) /v1/retrieve returns the original bytes
       by hash with all rows intact. Pre-populating the Python store
       (vs. driving SmartCrusher's internal Rust store) matches the
       established pattern in tests/test_proxy_ccr.py and exercises
       the surface served by /v1/retrieve.

  8. test_memory_tail_injection_does_not_modify_system_prompt_live — B6/A2
       Spins up a memory-enabled proxy with MemoryMode.AUTO_TAIL,
       seeds LocalBackend, captures upstream-bound body. Asserts:
       (a) system prompt byte-identical to input; (b) memory text
       lands on latest user message tail; (c) earlier messages
       untouched. Guards the live-zone-only injection contract.

  9. test_classify_auth_mode_routes_payg_vs_oauth       — Phase F-prep / B5
       NOT a live API call. Sends three header shapes through the
       proxy (x-api-key=..., Bearer sk-ant-oat01-..., Bearer
       sk-ant-api03-...), captures dispatcher headers via a wrap on
       _retry_request, and asserts the canonical auth-mode classifier
       maps each correctly. Codifies the Phase F contract.

Conventions:

  * file-level pytestmark = pytest.mark.live → excluded by default
    via 'pytest -m "not live"'. Adds a 'live' marker registration in
    pyproject.toml's [tool.pytest.ini_options].markers.
  * each test skipif's on the relevant API key — no silent fallbacks,
    no real-API runs against fake keys.
  * uses tests/_dotenv.py helpers (load_env_overrides + autouse_apply_env)
    rather than re-implementing env loading.
  * model IDs and thresholds live in a top-of-file LIVE_CONFIG dict
    (no hardcodes); Anthropic primary/fallback resolves at runtime per
    key entitlement.
  * assertions are direction-only (cache_read > 0, tokens_after <=
    tokens_before) — never tied to upstream pricing/tokenizer drift.
  * shared module-scoped TestClient fixture for performance; CCR and
    memory tests build dedicated proxies for their config-specific paths.

Verification:
  * pytest tests/test_realignment_live_multi_turn.py -v
      → 9 passed, 0 skipped, 0 failed in ~25s (with all keys set)
  * pytest -m "not live" --tb=short -q
      → 4694 passed, 265 skipped, 9 deselected — same baseline as today
  * make ci-precheck → green (rust + python + commitlint)

Per-realignment-plan: REALIGNMENT/04-phase-B-live-zone.md.
2026-05-02 17:39:19 -07:00
chopratejas
fb9be139a5 feat(python): switch relevance scorer to fastembed (BAAI/bge-small-en-v1.5)
Replace `sentence-transformers` (PyTorch-backed) with `fastembed` (ONNX-
backed) so Python and Rust call into the same library + same model for
relevance scoring. Both sides run BAAI/bge-small-en-v1.5 (33M params,
384 dims, ~30 MB int8-quantized ONNX) auto-downloaded from HF Hub.

Cross-language verification on ('authentication failed for user',
'login error'): Python=0.7505, Rust=0.7507, delta ~0.0002 — well below
the relevance_threshold (0.3) buffer SmartCrusher uses for keep/drop
decisions, so the two implementations agree on every observable
SmartCrusher output. (True byte-equal would require both to load the
identical ONNX weights file — Rust's `fastembed` crate and Python's
`fastembed` package can pick different upstream artifacts; deferred.)

Why fastembed:
- removes torch from the relevance/ path (Phase 6: drop torch from
  Python).
- ~2-3x faster than sentence-transformers' all-MiniLM-L6-v2 for the
  same input shape.
- bge-small-en-v1.5 outranks all-MiniLM-L6-v2 on MTEB by ~6 points.
- self-contained: no longer reads ML_MODEL_DEFAULTS.sentence_transformer
  from utils config.
2026-04-26 23:36:22 -07:00
chopratejas
7ed2b0ba34 Sync plugins to 0.9.2, pyproject canonical at 0.9.1 [skip ci] 2026-04-21 20:36:51 -07:00
JerrettDavis
56f6307665 fix: support py310 version sync scripts
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-21 20:42:56 -05:00
chopratejas
7cd67ef06d fix: bundle ast-grep/difftastic/scc + generic tool_result interceptor framework
What this does, in plain terms:

Headroom's proxy now ships with three CLI tools (ast-grep, difftastic,
scc) that it can use to shrink tool_result payloads before they reach
the model. The goal is simple: when Claude Code (or Codex, Aider, etc.)
asks the model to reason about a big file or diff, we swap the verbose
output for a compact, same-meaning version. Fewer tokens per turn, same
answers, lower bill.

Today a single interceptor is wired: ast-grep on Read. When an agent
reads a large code file, the proxy replaces the file body with an
outline of its top-level functions/classes plus docstrings. In live
tests that cut prompt tokens 74–76% on both OpenAI and Anthropic,
same answer either way.

How it works:
- `pip install headroom-ai` now installs ast-grep via a PyPI wheel
  (core dep). difftastic and scc are fetched once at proxy startup
  from pinned upstream GitHub releases and cached per-user.
- A generic registry (`headroom/proxy/interceptors/`) lets us add more
  tool-aware rewrites in one file each: declare `matches()` and
  `transform()`, call `register()`, done. No proxy or metrics plumbing
  per tool.
- Safety rails built in: pass-through when a Read specifies a line
  range; second Read of the same file in a conversation returns full
  content (progressive disclosure); any failing interceptor logs and
  skips, never crashes a request.

Opt-in for now:
- Off by default while this ships. Turn on with
  `headroom proxy --intercept-tool-results` or
  `HEADROOM_INTERCEPT_ENABLED=1`, so we can measure before flipping
  defaults.

What users see after turning it on:
- First `headroom wrap claude` boot is ~5s longer (binaries fetched).
  Every subsequent run is cache-only.
- Existing `transforms_applied` field in metrics gets entries like
  `interceptor:ast-grep`, so savings show up in current dashboards
  and HTML reports with no UI change.

Other housekeeping in this PR:
- uv.lock moved to .gitignore — regenerated locally per environment.
- 35 unit + integration tests, ruff + mypy clean.
- Dead-code audit done: removed `binaries.run()`, `needs_filesystem`
  plumbing, unused `_kind` tuple elements, unused `tool_output`
  parameter, and the never-set HEADROOM_SKIP_TOOLS_BOOTSTRAP env.
2026-04-20 17:36:44 -07:00