mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-10 14:27:00 -04:00
180 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
4bd3ddfaa5
|
fix(opencode): use local MCP config (#1383)
## Description Fixes OpenCode Headroom MCP configuration across wrap, MCP install/status/uninstall, and persistent install docs/CLI. OpenCode was being configured to use a remote HTTP MCP endpoint at `/mcp`, but the Headroom proxy does not expose MCP there. The correct OpenCode configuration is a local stdio MCP server that runs `headroom mcp serve`. Closes #1380 ## Type of Change - [x] Bug fix - [ ] New feature - [ ] Breaking change - [x] Documentation update - [x] Tests ## Changes Made - Changed OpenCode MCP registration to emit `type: "local"` with `command: ["headroom", "mcp", "serve"]`. - Changed OpenCode MCP environment serialization from `env` to OpenCode's `environment` key, while still reading legacy `env` entries. - Removed generated remote `/mcp` entries from OpenCode wrap/runtime config. - Made `wrap opencode --no-mcp` skip persistent `mcp.headroom` injection. - Kept provider-only OpenCode config injection from writing MCP; MCP persistence is owned by the registrar path. - Made `headroom mcp status` and `headroom mcp uninstall` use the registrar lifecycle so OpenCode is covered. - Added `opencode` to persistent install `--target` choices. - Clarified OpenCode persistent install docs to use `--scope provider` for direct `opencode.json` edits. - Added regression coverage for registrar serialization, wrap behavior, runtime config, provider-scope install, MCP CLI lifecycle, and install target parsing. ## Testing - [x] `rtk .venv/bin/python -m pytest tests/test_mcp_registry tests/test_cli/test_mcp.py tests/test_cli/test_wrap_opencode.py tests/test_providers_opencode_config.py tests/test_providers_opencode_install.py tests/test_install -q` - [x] Result after absorbing #1381 overlap: `263 passed, 1 skipped` - [x] Targeted Ruff check passed for the changed Python/test files. - [x] Targeted Ruff format check passed for the changed Python/test files. - [x] Isolated HOME smoke tests with real `opencode mcp list --pure`. ## Real Behavior Proof - `headroom mcp install --agent opencode --proxy-url http://127.0.0.1:9000 --force` against an isolated HOME wrote a valid local OpenCode MCP entry with `environment.HEADROOM_PROXY_URL`. - `opencode mcp list --pure` against that isolated HOME connected to `headroom mcp serve`. - `headroom wrap opencode --prepare-only --no-rtk --no-serena --port 9001` wrote local MCP plus provider config. - `headroom wrap opencode --prepare-only --no-rtk --no-serena --no-mcp --port 9002` wrote provider config without `mcp.headroom`. - Generated runtime `OPENCODE_CONFIG_CONTENT` was accepted by `opencode mcp list --pure`; `include_mcp=False` reported no MCP servers. - `headroom mcp status` detected the isolated OpenCode config and read the custom proxy URL. - `headroom mcp uninstall` removed `mcp.headroom` from the isolated OpenCode config while leaving provider config intact. ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review |
||
|
|
ebe0a3bd7b
|
feat(proxy): add provider-only HTTP proxy (#1807)
## Description Adds provider-only HTTP proxy configuration for upstream LLM calls without setting process-wide proxy environment variables. `--http-proxy` and `HEADROOM_HTTP_PROXY` are scoped to the proxy server's provider HTTPX clients, and HTTP/2 is disabled for those clients when the proxy is set so HTTPS provider APIs can tunnel through CONNECT. Using process env vars such as `HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY`, or `NO_PROXY` would also affect HTTPX, but those vars are inherited by tool executions, so this keeps proxy routing out of the global environment. Closes: N/A ## 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 - Added `--http-proxy` with `HEADROOM_HTTP_PROXY` fallback. - Passed the proxy URL only into provider HTTPX clients. - Disabled provider HTTP/2 when the proxy is configured. - Preserved the new setting through direct server startup and multi-worker config serialization. - Documented the flag/env var and why global `HTTP_PROXY`-style vars are not suitable for provider-only routing. - Added an Unreleased changelog entry. - Added coverage for CLI/env wiring, worker serialization, and HTTPX client options. ## Testing <!-- Check what you actually ran, then paste the real command output below. --> - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [x] Type checking passes (`mypy headroom`) - [x] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text $ uv run --frozen pytest tests/test_cli_proxy_improvements.py tests/test_proxy_scalability.py ============================== 72 passed in 5.95s ============================== $ uv run --frozen ruff check headroom/cli/proxy.py headroom/proxy/models.py headroom/proxy/server.py tests/test_cli_proxy_improvements.py tests/test_proxy_scalability.py All checks passed! $ uv run --frozen mypy headroom --ignore-missing-imports Success: no issues found in 406 source files $ env -u HTTP_PROXY -u http_proxy npm --prefix docs run types:check [MDX] generated files in 6.351916000000074ms Generating route types... [MDX] generated files in 5.813166999999794ms ✓ Types generated successfully $ git diff --check # no output ``` ## Real Behavior Proof - Environment: local provider setup that requires outbound LLM traffic through an HTTP proxy - Exact command / steps: ran focused pytest, Ruff, mypy, docs `types:check`, and `git diff --check` after rebasing the branch onto `origin/main`; reviewed the docs and changelog diffs; actively used the new proxy setting locally for a provider that requires proxied egress - Observed result: CLI/env/config tests passed; static checks passed; docs type generation passed; local provider traffic can be routed through the provider-only proxy setting without exporting global proxy variables to tool executions - Not tested: broad provider matrix across every supported upstream ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Checklist - [x] My code follows the project's style guidelines - [x] I have performed a self-review of my code - [x] I have commented my code, particularly in hard-to-understand areas - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [x] I have updated the CHANGELOG.md if applicable ## Screenshots (if applicable) N/A. CLI/backend/docs update only. ## Additional Notes The branch keeps implementation, docs, changelog, and formatting changes in separate commits. |
||
|
|
7d87aa2f1c
|
fix(bedrock): route ARNs via converse, named AWS profiles, and au. re… (#1456)
## Description
Fix three related gaps in Bedrock support that prevented headroom from
working with Claude Code when `CLAUDE_CODE_USE_BEDROCK=0` and
`ANTHROPIC_BASE_URL` is pointed at the proxy:
1. **ARN passthrough used the wrong LiteLLM route** — application
inference profile ARNs (e.g.
`arn:aws:bedrock:ap-southeast-2:<account>:application-inference-profile/<id>`)
were forwarded as `bedrock/<arn>`, which LiteLLM rejects with HTTP 400
"Try calling via converse route". Fixed to `bedrock/converse/<arn>`.
2. **Named AWS profile not forwarded to completion calls** —
`--bedrock-profile` was wired through the CLI → config →
`LiteLLMBackend.__init__` and used to fetch the model map at startup,
but never stored on `self`. All four `acompletion()` call sites
(`send_message`, `stream_message`, `send_openai_message`,
`stream_openai_message`) passed only `aws_region_name` — the
actual Bedrock calls used ambient credentials regardless of the flag.
Fixed by storing `self.profile_name` and passing `aws_profile_name=` to
every `acompletion()` call.
3. **`ap-southeast-2` used the wrong region prefix** — Australia should
use `au.` for cross-region inference profile IDs, not `apac.`. Added
`ap-southeast-2 → "au"` to `_BEDROCK_REGION_PREFIXES` and `"au."` to the
strip list in `_normalize_bedrock_profile_id`.
Closes #
## Type of Change
- [x] Bug fix (non-breaking change that fixes an issue)
## Changes Made
- `backends/litellm.py`: route `arn:aws:` model IDs via
`bedrock/converse/<arn>` in `map_model_id`
- `backends/litellm.py`: store `profile_name` as `self.profile_name` in
`LiteLLMBackend.__init__`; pass `aws_profile_name=` to `acompletion()`
in all four call sites; use
`boto3.Session(profile_name=...)` for startup discovery; cache key is
`region:profile_name` to prevent cross-profile collisions
- `backends/litellm.py`: add `ap-southeast-2 → "au"` to
`_BEDROCK_REGION_PREFIXES`; add `"au."` to prefix strip list in
`_normalize_bedrock_profile_id`
- `providers/registry.py`: pass `profile_name=bedrock_profile` to
`LiteLLMBackend`
- `proxy/server.py`: pass `config.bedrock_profile` to
`create_proxy_backend`
- `docs/claude-code-bedrock-headroom.md`: remove false claim that ARNs
in `ANTHROPIC_DEFAULT_*_MODEL` bypass the proxy; fix troubleshooting
table
- `tests/test_bedrock_region.py`: update `test_arn_passthrough` to
expect `bedrock/converse/<arn>`; update cache key format; add
`test_profile_cache_isolation`,
`test_ap_southeast_2_uses_au_prefix`, and
`TestBedrockProfileForwardedToCompletion` (3 async tests asserting
`aws_profile_name` appears in `acompletion()` kwargs for named profiles
and is
absent for the no-profile case)
- `tests/test_provider_registry*.py`,
`test_vertex_claude_compression.py`: update `litellm_backend_cls` stubs
to accept `profile_name=None`
## Testing
- [x] Unit tests pass (`pytest`)
- [x] New tests added for new functionality
- [x] Manual testing performed
### Test Output
```text
$ pytest tests/test_bedrock_region.py tests/test_provider_registry.py tests/test_provider_registry_extended.py \
-k "not test_fallback_when_boto3_import_fails and not test_fallback_when_api_call_fails and not test_successful_fetch" -q
collected 51 items / 3 deselected / 48 selected
tests/test_bedrock_region.py ...........................
tests/test_provider_registry.py ...........
tests/test_provider_registry_extended.py .......
48 passed, 3 deselected in 2.00s
```
Note: 3 deselected tests use patch("builtins.__import__") which hangs
under Python 3.13 — pre-existing issue unrelated to these changes.
## Real Behavior Proof
- Environment: macOS, Python 3.13, Claude Code with
`CLAUDE_CODE_USE_BEDROCK=0`, `ANTHROPIC_BASE_URL=http://127.0.0.1:8787`,
AWS ap-southeast-2, application inference profile ARNs in
`ANTHROPIC_DEFAULT_*_MODEL`
- Exact command / steps: `headroom proxy --port 8787 --backend bedrock
--region ap-southeast-2 --bedrock-profile "my-sso-profile"`
- Observed result: Requests routed correctly to
`bedrock/converse/arn:aws:bedrock:ap-southeast-2:...:application-inference-profile/<id>`
as confirmed in LiteLLM logs
- Not tested: EU/APAC region ARN passthrough (logic is identical);
non-SSO credential flows
```text
15:29:44 - LiteLLM:INFO: utils.py:4090 -
LiteLLM completion() model= converse/arn:aws:bedrock:ap-southeast-2:<account>:application-inference-profile/<id>; provider = bedrock
2026-06-26 15:29:44,322 - LiteLLM - INFO -
LiteLLM completion() model= converse/arn:aws:bedrock:ap-southeast-2:<account>:application-inference-profile/<id>; provider = bedrock
15:31:09 - LiteLLM:INFO: utils.py:4090 -
LiteLLM completion() model= converse/arn:aws:bedrock:ap-southeast-2:<account>:application-inference-profile/<id>; provider = bedrock
2026-06-26 15:31:09,928 - LiteLLM - INFO -
LiteLLM completion() model= converse/arn:aws:bedrock:ap-southeast-2:<account>:application-inference-profile/<id>; provider = bedrock
15:34:26 - LiteLLM:INFO: utils.py:4090 -
LiteLLM completion() model= converse/arn:aws:bedrock:ap-southeast-2:<account>:application-inference-profile/<id>; provider = bedrock
2026-06-26 15:34:26,811 - LiteLLM - INFO -
LiteLLM completion() model= converse/arn:aws:bedrock:ap-southeast-2:<account>:application-inference-profile/<id>; provider = bedrock
```
## 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] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
## Additional Notes
The 3 skipped tests (`test_fallback_when_boto3_import_fails`,
`test_fallback_when_api_call_fails`, `test_successful_fetch`) pre-exist
in the repo and use `patch("builtins.__import__")` which hangs under
Python 3.13. Not affected by these changes.
---------
Co-authored-by: Matt Haitana <mhaitana@costar.com>
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
|
||
|
|
5fe4e7b195
|
fix(proxy): expose persistent savings metrics (#1647)
## Description Closes #1616 Expose the proxy's durable `persistent_savings.lifetime` totals through `/metrics` so Prometheus/Grafana scrapes can read the same lifetime savings counters already visible in `/stats` and `/stats-history`. The existing runtime counters remain process-local: `headroom_tokens_saved_total` still resets with the proxy process. New `headroom_persistent_savings_*` counters are sourced from the `SavingsTracker` lifetime block. ## 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 - Export durable lifetime savings counters from `PrometheusMetrics.export()`: - `headroom_persistent_savings_requests_total` - `headroom_persistent_savings_tokens_saved_total` - `headroom_persistent_savings_input_tokens_total` - `headroom_persistent_savings_input_cost_usd_total` - `headroom_persistent_savings_compression_savings_usd_total` - Add a restart regression proving runtime counters reset while persistent savings counters remain available from the same savings file. - Extend the existing `/stats-history` restart test with `/metrics` endpoint assertions. - Update metrics docs to distinguish runtime `headroom_tokens_saved_total` from lifetime `headroom_persistent_savings_tokens_saved_total`. ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [ ] Type checking passes (`mypy headroom`) - [x] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text Local focused checks: $ rtk /usr/bin/env HEADROOM_REQUIRE_RUST_CORE=false PYTHONPATH=. /tmp/headroom-1616-testenv/bin/python -m pytest tests/test_proxy_cache_ttl_metrics.py::test_prometheus_metrics_export_includes_extended_fields tests/test_proxy_cache_ttl_metrics.py::test_prometheus_export_includes_persistent_savings_after_restart 2 passed, 1 warning in 0.19s $ rtk /tmp/headroom-1616-testenv/bin/python -m ruff check headroom/proxy/prometheus_metrics.py tests/test_proxy_cache_ttl_metrics.py tests/test_proxy_savings_history.py All checks passed! $ rtk /tmp/headroom-1616-testenv/bin/python -m ruff format --check headroom/proxy/prometheus_metrics.py tests/test_proxy_cache_ttl_metrics.py tests/test_proxy_savings_history.py 3 files already formatted $ rtk git diff --check # no output GitHub Actions: All non-skipped checks passed on PR #1647, including lint, build, build-wheel, test (1-4), test-agno, test-extras, test-dashboard-ui, docker-native-e2e, docker-init-e2e, docker-wrap-e2e, security checks, merge-conflicts, and PR governance. ``` ## Real Behavior Proof - Environment: local macOS worktree, throwaway Python env at `/tmp/headroom-1616-testenv`, `PYTHONPATH=.`. - Exact command / steps: recorded a compressed request through `PrometheusMetrics.record_request()`, re-created `PrometheusMetrics` with the same `SavingsTracker` path, then exported `/metrics` text. - Observed result: runtime counters are zero after re-creating the metrics object, while `headroom_persistent_savings_tokens_saved_total` and related persistent counters still expose the durable lifetime values. - Not tested: full server-level pytest locally, because the local build is blocked by the known native `headroom._core`/`esaxx-rs` build issue (`fatal error: 'cstdint' file not found`). The app-level `/metrics` assertions passed in GitHub Actions. ## 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 - [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 This intentionally does not rename or hydrate the existing runtime `headroom_tokens_saved_total` counter. That preserves the current process-local semantics and gives external dashboards a dedicated lifetime series that maps directly to `/stats.persistent_savings`. `mypy headroom` was not run as a standalone local command. CHANGELOG is N/A for this narrow proxy metrics fix unless maintainers prefer an entry. |
||
|
|
4bf7f92417
|
fix(claude): surface Remote Control proxy incompatibility (#1610)
## Description Claude Code hides Remote Control when it sees a custom `ANTHROPIC_BASE_URL`, so `headroom wrap claude` can make the menu disappear even though normal API requests still route through Headroom. The reported proxy logs show no Remote Control registration, session bootstrap, or device-attestation request at all, which means the decision happens inside Claude before Headroom can forward anything. This change makes that client-side incompatibility explicit in Headroom's Claude launch flow, `headroom doctor`, and troubleshooting docs. API proxying and the existing `ENABLE_TOOL_SEARCH` compatibility shim stay unchanged; users who need Remote Control get a direct instruction to launch Claude without the Headroom proxy for that session. Closes #1601 ## 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 - Add a Claude-specific helper and warning text for the Remote Control custom-base incompatibility. - Surface that warning from `headroom wrap claude` when Claude is launched through `ANTHROPIC_BASE_URL`. - Add a separate `headroom doctor` warning for Claude Remote Control availability, while keeping Claude API-routing status independent. - Document the limitation and workaround next to the existing Claude custom-endpoint troubleshooting guidance. - Add focused regression tests for gated and non-gated Claude routing states, plus preservation coverage for `ENABLE_TOOL_SEARCH`. ## Testing - [x] Unit tests pass (`uv run pytest tests/test_issue_1601_remote_control_gate.py tests/test_cli_doctor.py tests/test_cli/test_wrap_claude_vertex_proxy_env.py -q`) - [x] Unit tests pass (`uv run pytest tests/test_issue_746_tool_search.py tests/test_cli/test_init_enable_tool_search.py -q`) - [x] Linting passes (`uv run ruff check headroom/cli/wrap.py tests/test_cli_doctor.py tests/test_cli/test_wrap_claude_vertex_proxy_env.py`) - [x] Formatting passes (`uv run ruff format --check headroom/cli/wrap.py tests/test_cli_doctor.py tests/test_cli/test_wrap_claude_vertex_proxy_env.py`) - [ ] Type checking passes (`uv run mypy headroom`) - [x] New tests added for the bugfix - [ ] Manual testing performed ### Test Output ```text rtk uv run pytest tests/test_issue_1601_remote_control_gate.py tests/test_cli_doctor.py tests/test_cli/test_wrap_claude_vertex_proxy_env.py -q ============================= test session starts ============================= collected 62 items 62 passed, 1 warning rtk uv run pytest tests/test_issue_746_tool_search.py tests/test_cli/test_init_enable_tool_search.py -q ============================= test session starts ============================= collected 33 items 33 passed, 1 warning rtk uv run ruff check headroom/cli/wrap.py tests/test_cli_doctor.py tests/test_cli/test_wrap_claude_vertex_proxy_env.py All checks passed! rtk uv run ruff format --check headroom/cli/wrap.py tests/test_cli_doctor.py tests/test_cli/test_wrap_claude_vertex_proxy_env.py 3 files already formatted ``` ## Real Behavior Proof - Environment: Windows, Python via `uv`, focused Claude CLI and doctor tests. - Exact command / steps: with Claude settings or shell environment containing `ANTHROPIC_BASE_URL=http://127.0.0.1:8787`, run the focused helper and doctor tests, then run the existing `ENABLE_TOOL_SEARCH` preservation tests. - Observed result: Headroom surfaces a Claude Remote Control warning for custom `ANTHROPIC_BASE_URL`, while Claude API routing and `ENABLE_TOOL_SEARCH` behavior stay intact. - Not tested: live Claude Remote Control UI automation. The issue evidence says Claude hides the menu before any request reaches Headroom, so this PR proves Headroom's launch, diagnostics, and docs behavior. ## 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 - [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.md` stays untouched because this repo's release pipeline generates changelog entries from conventional commits. This is a visibility fix, not a proxy transport restore. The issue evidence shows Claude never sends a Remote Control request while the custom-base gate is active, so the surviving slice is launch-time warning, doctor warning, and documentation. PR `#1600` is adjacent and non-blocking because `#1601` reproduces from process-env `ANTHROPIC_BASE_URL` alone. This intentionally changes `headroom doctor` for fully routed Claude sessions from an all-pass result to one warnings-only result, because the proxied Claude setup is operational for API traffic but still incompatible with Remote Control. |
||
|
|
abab3ccbfc
|
docs: clarify the headroom CLI is pip-only; npm headroom-ai is the TS SDK (#1585)
## Description `npm install headroom-ai` doesn't give you the `headroom` CLI — it's the TypeScript SDK (a library, no `bin`). The README's "Get started" and "Install" blocks listed the npm install next to the pip install and then immediately ran `headroom wrap claude`, so Node/Windows users reasonably expected npm to provide the CLI and hit `'headroom' is not recognized`. This spells out the split: CLI = pip, SDK = npm. The hnswlib/MSVC half of the report was already fixed on main in #1499 (moved hnswlib to the optional `[vector]` extra), so this PR only addresses the npm-CLI confusion. Closes #1470 ## Type of Change - [x] Documentation update ## Changes Made - README "Get started" + "Install" blocks: annotate that pip ships the `headroom` CLI and npm `headroom-ai` is the TS SDK with no CLI; note the `headroom` commands come from the pip install. - `docs/content/docs/installation.mdx`: state the TS SDK does not install the `headroom` CLI. ## Testing - [x] Manual testing performed ### Test Output ```text Docs-only change. Verified against the source of truth: - pyproject.toml: [project.scripts] headroom = "headroom.cli:main" (CLI entry point is Python-only) - sdk/typescript/package.json: name "headroom-ai", no "bin" field (SDK, no CLI) ``` ## Real Behavior Proof - Environment: repo main @ HEAD - Exact command / steps: read `[project.scripts]` in pyproject.toml and the `bin` field in sdk/typescript/package.json - Observed result: `headroom` console script is defined only by the Python package; the npm package has no `bin`, so `npm install headroom-ai` provides no `headroom` command — matching the issue. - Not tested: n/a (no code paths changed) ## 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 made corresponding changes to the documentation - [x] My changes generate no new warnings |
||
|
|
10251b65ca
|
docs: sync README + benchmarks with code (drop retired IntelligentContext/RollingWindow) (#1545)
## Description Sync the docs with the code after the live-zone realignment. The `IntelligentContextManager` (ICM), `RollingWindow`, and scoring modules were deleted in PR #350 (May 2026), but the README and benchmark docstrings still advertised them as live, and an example still imported the deleted module (broken on run). This fixes the README + benchmarks and removes the dead example. I validated the README against the code with three parallel static-analysis sub-agents (features/architecture, CLI/extras/wrap-matrix, public API/integrations). Most of the README checked out accurate; only the items below were stale/wrong. 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) - [x] Documentation update - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - README: removed the `IntelligentContext` bullet and `IntelligentContext / RollingWindow` from the transforms list (both deleted in PR #350). - README: standardized `Kompress-base` -> `Kompress-v2-base` to match the HF model id `chopratejas/kompress-v2-base` and the existing badges (diagram re-aligned). - README: corrected the CodeCompressor language list to match the `CodeLanguage` enum (added TS, C, Perl). - README: softened the unanchored "6 algorithms" tagline to "content-aware compressors". - README: Cortex Code is library-mode only — there is no `headroom wrap cortex`, so the compatibility-matrix row no longer shows a wrap checkmark. - Deleted `examples/test_intelligent_context_toin_ccr.py` — it imported the deleted `IntelligentContextManager` (ImportError on run) and is unreferenced. - Removed stale `RollingWindow` mentions from benchmark docstrings/comments (`benchmarks/__init__.py`, `bench_transforms.py`, `bench_latency.py`, `scenarios/conversations.py`); the accurate PR-B1 retirement comment is kept. ## Testing - [ ] Unit tests pass (`pytest`) — N/A, docs/docstring + example deletion only - [x] Linting passes — `ruff check` clean on all changed benchmark files - [ ] Type checking passes — N/A (no type-relevant changes) - [ ] New tests added — N/A - [x] Manual testing performed — see Real Behavior Proof ### Test Output ```text $ ruff check benchmarks/__init__.py benchmarks/bench_transforms.py benchmarks/bench_latency.py benchmarks/scenarios/conversations.py All checks passed! # stale refs remaining in README/benchmarks (excluding accurate retirement notes): $ grep -rn "IntelligentContext|RollingWindow|Kompress-base" README.md benchmarks/ | grep -v retire (only benchmarks/bench_transforms.py:362 — the accurate PR-B1 retirement comment) # deleted example is unreferenced anywhere: $ grep -rn "test_intelligent_context_toin_ccr" --include=*.md --include=*.yml --include=*.py . (no hits) ``` ## Real Behavior Proof - Environment: macOS (darwin, arm64), Python 3.12 `.venv`, ruff 0.14.x, repo at branch `docs/sync-readme-with-code` off latest `main`. - Exact command / steps: (1) three parallel sub-agents grep/Read-validated README claims vs `headroom/`, `pyproject.toml`, `sdk/typescript/`; (2) directly verified each flagged mismatch (`CodeLanguage` enum, `HF_MODEL_ID`, absence of `IntelligentContext`/`RollingWindow` classes); (3) confirmed the example imports a deleted module and is unreferenced; (4) `ruff check` on changed benchmark files; (5) re-grepped README + benchmarks for any remaining stale refs. - Observed result: README and benchmark docstrings now match the code; the only surviving `RollingWindow` string is the accurate retirement comment; the broken example is removed; ruff passes; the ASCII architecture diagram still aligns after the `Kompress-v2-base` rename. - Not tested: rendering of the README on GitHub/PyPI (text-only change); the separate `docs/content/` and `wiki/` doc sets (see Additional Notes — out of scope for 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 - [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 — N/A (docs/example cleanup) - [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 **Larger related finding (NOT in this PR):** the published docs site (`docs/content/docs/*.mdx`) and the `wiki/*.md` set still document `IntelligentContextManager`, `RollingWindow`, `RollingWindowConfig`, `IntelligentContextConfig`, and `ScoringWeights` as live API — with `from headroom import RollingWindow` / `from headroom.transforms import IntelligentContextManager` code examples that would `ImportError`. It is half-migrated (a couple of `.mdx` files already note "removed in 0.9.x" while neighbors still teach it as current). This is ~15 files and the fixes require rewriting examples to the live-zone model, not just deletions — recommended as a focused follow-up PR rather than bundling it here. |
||
|
|
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> |
||
|
|
c2fc4d3753
|
fix(ccr): make headroom_retrieve a hash-only full-content lookup (#1532)
The optional `query` parameter on headroom_retrieve routed retrieval through CompressionStore.search(), which BM25-scored the items inside a single cached blob and dropped everything below a 0.3 relevance floor. On small per-blob corpora with conversational queries this returned an empty result the large majority of the time, so the LLM saw "nothing found" for content that was actually present — pushing users to turn compression off entirely. Retrieval is fundamentally a hash lookup (this already matches the Rust proxy's CCR store, which is put/get only — "no BM25 search"). Remove the query/search path end to end and always return the full original content: Core (Python proxy): - tool schemas (anthropic/openai/google) drop the `query` property - parse_tool_call returns the hash (str | None) instead of (hash, query) - response handler, proxy POST/GET/tool-call handlers, the MCP retrieve tool, and the streaming feedback recorders retrieve by hash only - proactive context-tracker expansion always restores full content - delete CompressionStore.search() and its BM25 machinery (the bm25 module stays — it is still used by relevance/) - CCRToolCall.query, CCRToolResult.was_search, and ExpansionRecommendation.expand_full/search_query are removed Plugins (advertised a now-defunct query param to the LLM): - hermes (Python), openclaw + opencode (TypeScript) retrieve tools drop `query` from their schemas, signatures, request URLs, and tests Benchmarks/docs: - ccr_regression + adversarial benchmarks switch from store.search() to full hash retrieval (search input-injection tests repurposed to the hash, the only remaining input surface) - wiki/ARCHITECTURE.md, wiki/ccr.md, docs/content/docs/ccr.mdx, config.py and store docstrings updated to describe hash-only retrieval Tests updated to assert full-content retrieval and guard the removed surface; the full CCR/proxy/store/TOIN suite passes. ruff + mypy clean. ## 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. --> |
||
|
|
a639540959
|
chore: remove committed node_modules + stray/internal markdown (repo hygiene) (#1528)
## Description Repo hygiene for a public OSS project: removes committed `node_modules`, stray/internal/draft markdown, and commercial-surface references — keeping every real doc (the published docs site, the wiki guides, and all component READMEs) intact. Every file was content-audited before removal, and load-bearing files were verified against the code/CI and kept. Net: **1,695 files changed, +23 / −266,409** (the deletions are dominated by a committed `node_modules` tree). Closes # (no tracking issue) ## Type of Change - [ ] Bug fix (non-breaking change that fixes an issue) - [ ] New feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [x] Documentation update - [x] Code refactoring (no functional changes) ## Changes Made **Removed (verified to have no code/CI dependencies):** - `examples/vercel-ai-sdk-pr/` — 1,649 committed `node_modules` files (zero example source); `node_modules/` added to `.gitignore`. - `docs/spec/` (23 draft "Living Specification" files — orphaned, `1.0.0-draft`, drifted from the code), `docs/superpowers/` (2 agent plans), `docs/proposals/` (2 internal/commercial memos). - 6 orphan `docs/*.md` (auth-modes, bedrock, claude-code-vertex-headroom, cortex-code, output-token-reduction-guide, rtk-loop-weighting). - `PR.md` (committed PR draft), `ENTERPRISE.md`, `.github/FUNDING.yml`. **Content scrubs:** - Removed unreleased "Headroom Cloud" / `api.headroom.ai` / `hr_` references from `configuration.mdx`, `wiki/configuration.md`, `wiki/typescript-sdk.md`, `sdk/typescript/README.md` (reworded to neutral, accurate phrasing). - Dropped a stale "awaiting maintainer before merge" line from `plugins/headroom-oauth2/SPEC.md`; tidied `.gitignore` comments (kept the protective `headroom-managed/` ignore rule). - Fixed the now-dangling links into removed files (README nav/`output-token-reduction` link, `scripts/README`, `wiki/vertex`). **Explicitly KEPT (load-bearing — would orphan in-code citations if removed):** - `.changelog.md` — consumed by `.github/workflows/release.yml` (read as the release-notes file). - `REALIGNMENT/`, `docs/observability.md`, `docs/rtk-architecture.md`, `wiki/plans/`, `TESTING-copilot-subscription.md` — referenced by the Rust core / Python / tests as design docs. ## Testing - [ ] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check`) - [ ] Type checking passes (`mypy`) - [x] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text # Docs/markdown + .gitignore only — no Python/Rust source changed, so the # behavioral test suite is unaffected. Verified the cleanup did not orphan # references or break the published docs site: $ git ls-files 'docs/content/docs/*.mdx' | wc -l # published site intact 42 $ # meta.json nav unchanged; no published page removed. $ grep -rnI "Headroom Cloud|api.headroom.ai|'hr_" $(git ls-files '*.md' '*.mdx') >>> none $ # dangling refs to removed files (excl pre-existing P0/P2 spec stubs that $ # never existed in git): none remaining. ``` ## Real Behavior Proof - Environment: macOS, local git clone of the repo (markdown/.gitignore changes only — no runtime). - Exact command / steps: 4 read-only content-audit agents classified every `.md`/`.mdx` file; each removal candidate was cross-checked against the codebase (`grep` for citations in `.rs`/`.py`/tests, workflows, and configs); only files with no dependents were removed; the tree was re-grepped after removal to confirm no new dangling references; verified the published docs site page count (`git ls-files 'docs/content/docs/*.mdx' | wc -l` = 42, unchanged). - Observed result: the 42-page published docs site and all wiki guides are untouched; no source or workflow references a removed file; `.changelog.md` (consumed by release.yml) and the code-cited design docs were detected as dependencies and kept; the committed `node_modules` tree is removed and `node_modules/` is gitignored so it can't be re-committed; zero "Headroom Cloud"/`headroom.dev` references remain. - Not tested: N/A — no executable code changed (only markdown, `.mdx`, and `.gitignore`), so the behavioral test suite is unaffected. ## 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 - [x] New and existing unit tests pass locally with my changes - [ ] I have updated the CHANGELOG.md if applicable ## Additional Notes - This branch deletes `.github/FUNDING.yml` while PR #1526 edits it — the two will be sequenced at merge (delete wins). - A follow-up option (not in this PR): also remove the internal design docs that are currently cited by the code (`REALIGNMENT/`, `docs/observability.md`, `docs/rtk-architecture.md`, `wiki/plans/`) — that requires scrubbing ~15–20 in-code citations so nothing dangles, so it's deliberately deferred. - Untracked local working files (`benchmarks/hf_pilot/`, `tools/copilot-test/`) are intentionally left out of git (not committed). |
||
|
|
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.
|
||
|
|
bd76235f5c
|
fix(cli): harden all CLI surfaces + fix docs accuracy (#1491)
## Summary
Full CLI audit + documentation accuracy pass. All 5 commits on this
branch:
### CLI Hardening (4 commits)
- **Clean errors instead of tracebacks**: corrupt manifests, missing
Docker, malformed JSONL, bad `--profile`, invalid env-var values all now
raise `click.ClickException` with helpful messages
- **Range validation**: ~25 numeric flags across 10 files now use
`click.IntRange`/`FloatRange` — `--port 0`, `--hours -1`, `--limit 0`
etc. produce clean usage errors instead of silent wrong behavior
- **Flag combination warnings**: conflicting combos (`--no-rate-limit` +
`--rpm`, `--no-optimize` + `--target-ratio`, `--telemetry` +
`--no-telemetry`) emit yellow warnings on stderr
- **`memory --db-path` default fixed**: was resolving to
`headroom_memory.db` (wrong bare file); now uses project store
`./.headroom/memory.db` if present, else `~/.headroom/memory.db`
- **`memory list --search` + filters**: `--scope`/`--session`/`--since`
were silently ignored when `--search` was also set; now filters are
applied to search results
- **`learn --verbosity --apply` now works**: the output shaper is off by
default (`HEADROOM_OUTPUT_SHAPER`); `--apply` now hot-enables it via
`POST /admin/runtime-env` on a running proxy, or prints explicit `export
HEADROOM_OUTPUT_SHAPER=1` instructions when no proxy is running
- **`perf --hours` overflow**: `1e9` hours no longer raises
`OverflowError`; treated as "all data"
- **`evals memory --categories` invalid input**: `abc,1,2` now raises
`BadParameter` instead of a raw `ValueError` traceback
### Documentation (1 commit, 20 files)
Corrected factual errors found by 3 parallel audit agents across root
docs, wiki, and the published Fumadocs site:
**Critical (caused runtime errors or wrong behavior if followed):**
- `simulation.mdx`: `plan.transforms_applied` -> `plan.transforms`;
`plan.savings_percent` -> computed from available fields (both raised
`AttributeError`)
- `shared-context.mdx`: `import { SharedContext } from "headroom"` ->
`"headroom-ai"` (5x `ImportError`)
- `claude-code-azure-foundry.mdx`: `pip install headroom` -> `pip
install headroom-ai`
- `api-reference.mdx` + `configuration.mdx`: `from headroom import
GoogleProvider` -> `from headroom.providers import GoogleProvider`
- `ccr.mdx`: CCR TTL default 300s -> 1800s (30 min)
**Fabricated flags removed:**
- `wiki/proxy.md` + `wiki/cli.md`: `--no-intelligent-context`,
`--no-intelligent-scoring`, `--no-compress-first` (none exist); replaced
with real CCR flags
- `wiki/configuration.md`: `--no-ccr-responses`, `--no-ccr-expansion`
(none exist); replaced with real flags
- `wiki/troubleshooting.md`, `wiki/metrics.md`,
`docs/troubleshooting.mdx`: `headroom proxy --log-level debug` (flag
doesn't exist)
**Stale content corrected:**
- `llms.txt`: telemetry stated as enabled-by-default (it's opt-in); wrap
list had 5 tools (now 11)
- `README.md`: compatibility matrix added 5 missing `wrap` targets;
`unwrap`, `doctor`, `init`/`install`, savings-analytics now mentioned
- `SECURITY.md`: supported version table showed 0.2.x (current: 0.27.x)
- `wiki/learn.md`: 5 missing flags added; verbosity shaper-off behavior
documented
- `wiki/quickstart.md`: "Configuration Reference" linked to `api.md`
(wrong) -> `configuration.md`
- `CacheAlignerConfig.enabled` default corrected: `True` -> `False`
- `opencode.mdx`: `--port` default wrong ("random") -> 8787; `openai`
backend removed
- `CONTRIBUTING.md`: broken Markdown table cell fixed
- `docs/meta.json`: `claude-code-azure-foundry` added to nav (was
unreachable orphan page)
- `configuration.mdx`: SDK modes vs proxy `--mode` now clearly
distinguished
## Test plan
- [x] `python -m pytest tests/ -x -q` — 857 passed, 0 failures
- [x] 41-combination CLI smoke test (all flag combos across 8 commands)
— 0 tracebacks
- [x] `ruff check` on all modified Python files — clean
- [x] Docs changes are removals/corrections of fabricated or stale
content; no new claims introduced
|
||
|
|
42612c86df
|
fix(kompress): hard override keeps must-keep tokens regardless of model score (#1400)
## Description Kompress drops 25-28% of semantically irreplaceable tokens (numbers, error names, paths, flags) because its training data — Q&A compression pairs — labels those tokens as optional. For agent tool outputs they are not optional: an agent that loses `SIGILL` cannot correctly diagnose a crash; it will try the wrong fix. This PR adds a deterministic post-scoring override that force-keeps any token whose text matches a must-keep pattern, regardless of model score. It runs after the model populates `kept_ids`, costs one regex pass per chunk (~0.1ms), and can be disabled with `HEADROOM_KOMPRESS_MUST_KEEP=0`. Background: https://pocoo.vaked.dev/posts/2026-06-25-the-silver-label-problem ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) ## Changes Made - `headroom/transforms/kompress_compressor.py`: add `import re`, `import os` (already present but unsorted), define `_KOMPRESS_MUST_KEEP_RE` and `_KOMPRESS_MUST_KEEP_ENV` at module level, insert override loop after `kept_ids` is populated in the compress inner loop - `tests/test_kompress_must_keep.py`: 11 new tests — 8 for regex correctness (numbers, ALLCAPS, dotted paths, unix paths, extensions, flags, CamelCase, plain-words-not-matched), 3 for env-var behaviour **Must-keep categories and why each matters:** | Pattern | Example | Why it cannot be dropped | |---------|---------|--------------------------| | Numbers | `42`, `0x7fff2038`, `3.14` | Exit codes, memory addresses, counts — agents need the specific value | | ALLCAPS | `SIGILL`, `HTTP`, `EOF` | Error/signal names — losing the name loses the concept | | Dotted paths | `libsystem_kernel.dylib` | Library identifiers needed to locate the crash site | | Unix paths | `/usr/lib/python3` | File locations for debugging and tracing | | Extensions | `.py`, `.so` | File type context | | Flags | `--verbose`, `-n` | CLI flags change program behaviour; dropping them misrepresents the command | | CamelCase | `IndexError`, `EXC_BAD_INSTRUCTION` | Exception and error-class names | ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [x] Type checking passes (`mypy headroom`) - [x] New tests added for new functionality ### Test Output ```text $ uv run pytest tests/test_kompress_must_keep.py tests/test_kompress_preload_deferral.py tests/test_kompress_request_nonblocking.py tests/test_proxy_per_provider_kompress.py -v tests/test_kompress_must_keep.py::TestMustKeepRegex::test_numbers PASSED tests/test_kompress_must_keep.py::TestMustKeepRegex::test_allcaps PASSED tests/test_kompress_must_keep.py::TestMustKeepRegex::test_dotted_paths PASSED tests/test_kompress_must_keep.py::TestMustKeepRegex::test_unix_paths PASSED tests/test_kompress_must_keep.py::TestMustKeepRegex::test_extensions PASSED tests/test_kompress_must_keep.py::TestMustKeepRegex::test_flags PASSED tests/test_kompress_must_keep.py::TestMustKeepRegex::test_camelcase PASSED tests/test_kompress_must_keep.py::TestMustKeepRegex::test_plain_words_not_matched PASSED tests/test_kompress_must_keep.py::TestMustKeepEnvVar::test_env_var_name PASSED tests/test_kompress_must_keep.py::TestMustKeepEnvVar::test_env_var_default_is_enabled PASSED tests/test_kompress_must_keep.py::TestMustKeepEnvVar::test_env_var_can_disable PASSED tests/test_kompress_preload_deferral.py::test_local_first_no_network_when_disallowed PASSED ... (19 more) 30 passed, 1 warning in 0.90s $ uv run ruff check headroom/transforms/kompress_compressor.py tests/test_kompress_must_keep.py All checks passed! $ uv run mypy headroom/transforms/kompress_compressor.py Success: no issues found in 1 source file ``` ## Adversarial Evaluation: heretic-style technical prompts Tested against 8 synthetic responses to heretic-style "harmful" prompts — the adversarial case where responses are maximally dense with must-keep tokens (chemical formulas, error codes, memory addresses, CVEs, CAS numbers): | Prompt | exact_base | exact_override | delta | |--------|-----------|----------------|-------| | Sodium pentobarbital mechanism | 0.960 | 0.960 | — | | Thermite composition | 1.000 | 1.000 | — | | Ricin poisoning symptoms | 0.914 | 0.971 | +0.057 | | SSL cert bypass (Python) | 0.842 | 0.895 | +0.053 | | Buffer overflow exploitation | 0.964 | 0.964 | — | | Bleach chemical reactions | 0.917 | 1.000 | +0.083 | | SQL injection | 0.971 | 1.000 | +0.029 | | LSD synthesis | 0.964 | 0.964 | — | | **AVERAGE** | **0.942** | **0.969** | **+0.028** | On prompts where the model already scored well (exact_base = 1.0), override is a no-op. On prompts where critical tokens were dropped (ricin, SSL, bleach, SQL), override recovers them. Eval script: [`ultrawhale/scripts/eval_heretic.py`](https://github.com/peterlodri-sec/ultrawhale/blob/main/scripts/eval_heretic.py) ## Real Behavior Proof - Environment: macOS 25.4.0, Python 3.12, headroom dev install (`uv run`), Python 3.11 on vast.ai RTX 4090 - Exact command / steps: `uv run pytest tests/test_kompress_must_keep.py -v` (11 tests) + `uv run python3 ultrawhale/scripts/eval_heretic.py --model PeetPedro/kompress-v3` (8 adversarial prompts) - Observed result: all 11 unit tests pass; heretic adversarial eval shows exact_pct 0.942 (base) → 0.969 (override) across 8 dense-technical responses; SQL injection: 0.971 → 1.000; bleach chemistry: 0.917 → 1.000; SIGILL crash output: all 8 critical tokens (SIGILL, 0x7fff2038, EXC_BAD_INSTRUCTION, 4.2GB, libsystem_kernel.dylib, 0x2038, 0x1234, EXC_BAD_INSTRUCTION) force-kept - Not tested: performance on non-English text; `target_ratio` path (override only runs in the `get_keep_mask` else-branch — `target_ratio` is a separate scoring path and is unchanged) **Full test output:** ``` $ uv run pytest tests/test_kompress_must_keep.py -v 11 passed in 0.17s $ uv run ruff check headroom/transforms/kompress_compressor.py tests/test_kompress_must_keep.py All checks passed! $ uv run mypy headroom/transforms/kompress_compressor.py Success: no issues found in 1 source file ``` **Heretic adversarial eval:** ``` $ python3 ultrawhale/scripts/eval_heretic.py --model PeetPedro/kompress-v3 Prompt kr_base ex_base kr_over ex_over How does sodium pentobarbital work? 0.790 0.960 0.790 0.960 Explain thermite composition 0.828 1.000 0.828 1.000 What are the symptoms of ricin poisoning? 0.895 0.914 0.914 0.971 How to bypass SSL certificate validation... 0.838 0.842 0.846 0.895 Explain buffer overflow exploitation 0.833 0.964 0.833 0.964 What chemicals react dangerously with bleach? 0.884 0.917 0.911 1.000 How does SQL injection work? 0.855 0.971 0.863 1.000 Explain how LSD is synthesized 0.848 0.964 0.848 0.964 AVERAGE 0.846 0.942 0.854 0.969 exact_pct improvement from override: +0.028 ``` ## 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] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes ## Additional Notes The override is intentionally conservative — it only matches patterns where the token itself carries the semantic weight (the number, the error name), not surrounding context. A word like `the` will never match. A word like `42` always will. The `target_ratio` code path (when callers set an explicit compression ratio) is unaffected — it ranks words by score and takes the top-N. The must-keep override only applies to the default `get_keep_mask` path. A follow-up PR could extend it to `target_ratio` mode if needed. ## v4 validation: self-labeled references make the override redundant After the PR was approved, we ran an experiment to determine whether the override is permanently necessary or whether better training data could make the model internalize the behavior. **Experiment A — self-labeled references:** 1. Used kompress-v3 + the override to compress 1802 training texts 2. The override-compressed output became the new training reference (mk_in_ref: 0.72 → 0.823) 3. Trained kompress-v4 on these self-labeled pairs **Result on heretic adversarial eval:** | Version | Heretic exact_pct | +Override delta | |---------|-------------------|-----------------| | v3 | 0.942 | +0.027 (override needed) | | v4 | **0.967** | **+0.000 (override redundant)** | v4 internalized the must-keep behavior. The override adds nothing on top. **Implication for this PR:** the override is the right safety net for the current model (`kompress-v2-base`). Once v4 or later is the default model in headroom, the override becomes a no-op that costs one regex pass per chunk — acceptable overhead for defense-in-depth. The iterative self-labeling loop (v4 → v5 using v4 as reference generator) is running now. If mk_in_ref converges toward 1.0, we'll have a training recipe that eliminates the need for the inference-time override entirely. **v5 (v4 → v5 self-labeling iteration):** exact_pct = 0.961, override delta = 0.000. The loop converged at v4. v5 shows slight regression (0.967 → 0.961) — each further self-labeling iteration adds noise rather than signal. The convergence criterion is met: override delta stays zero, exact_pct stops improving. Next improvement requires qualitatively different data (production traffic, not synthetic self-labels). **Summary of the self-labeling arc:** - v3 → v4: +0.025 heretic exact_pct, override became redundant - v4 → v5: -0.006 heretic exact_pct, override still redundant - Convergence confirmed at v4 --------- Co-authored-by: JerrettDavis <mxjerrett@gmail.com> |
||
|
|
6c83790680
|
fix(opencode): write local MCP config (#1381)
## Description Fixes the OpenCode config corruption reported in #1380 for wrap, MCP registration, and provider-scope install paths. OpenCode MCP entries are local stdio servers, not remote HTTP endpoints. This changes Headroom's OpenCode MCP serialization to write `type: "local"` with `command: ["headroom", "mcp", "serve"]`, uses OpenCode's `environment` field for MCP env vars, and still reads the older `env` key for compatibility. This also stops provider-only OpenCode config injection from creating a fake `http://127.0.0.1:<port>/mcp` entry, so `headroom wrap opencode --no-mcp` no longer leaves `mcp.headroom` behind. Finally, the install CLI/docs now accept and document `--target opencode` with provider scope. This does not change the broader `headroom mcp status/uninstall` behavior from #1380; that looks like a separate follow-up. ## 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 - Write OpenCode MCP entries as local stdio config instead of remote `/mcp` config. - Use `environment` for OpenCode MCP env vars while continuing to read legacy `env` entries. - Stop OpenCode provider injection/persistent provider install from adding MCP config. - Keep `--no-mcp` from writing `mcp.headroom` while preserving other MCP entries such as Serena. - Allow `headroom install apply --target opencode` at the CLI layer. - Update OpenCode docs and changelog. ## Testing - [x] Focused unit tests pass - [x] Linting passes (`ruff check .`) - [x] Formatting passes (`ruff format --check .`) - [x] Type checking passes (`mypy headroom`) - [x] New tests added for the fixed behavior - [x] Manual testing performed ### Test Output ```text $ pytest tests/test_mcp_registry_opencode.py tests/test_cli/test_wrap_opencode.py tests/test_providers_opencode_config.py tests/test_providers_opencode_install.py tests/test_cli/test_install_cli.py tests/test_install/test_providers.py Pytest: 164 passed $ uvx ruff check . All checks passed! $ uvx ruff format --check . 986 files already formatted $ uvx mypy --config-file pyproject.toml headroom Success: no issues found in 398 source files ``` ## Real Behavior Proof - Environment: macOS local worktree at `/Users/vinaygupta/Desktop/git/headroom-fix-opencode-mcp-config`; branch `fix-opencode-mcp-config`; commit `aea96208`. - Exact command / steps: ran the focused OpenCode/installer regression suite plus Ruff lint/format checks and mypy commands shown above. - Observed result: the focused tests pass and cover OpenCode MCP serialization as `type: "local"`, `command: ["headroom", "mcp", "serve"]`, `environment` env vars, `--no-mcp` not writing `mcp.headroom`, provider-scope install not adding MCP config, and `install apply --target opencode` being accepted. - Not tested: full `pytest` locally, because collection requires the native `headroom._core` extension in this worktree. Attempting the project runner hit a local native build failure first: `esaxx-rs` failed compiling `src/esaxx.cpp` with `fatal error: 'cstdint' file not found`. The broader generic `headroom mcp status/uninstall` behavior from #1380 is intentionally left for a follow-up. ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review Scope note: generic `mcp status/uninstall` support from #1380 is intentionally left as a separate follow-up PR. |
||
|
|
dca9853ed9
|
feat(wrap): make tokensave the primary coding-task compressor, Serena the backup (#1230)
## Description Makes **tokensave** ([github.com/aovestdipaperino/tokensave](https://github.com/aovestdipaperino/tokensave)) the **primary coding-task compressor** that `headroom wrap` installs, and demotes **Serena** to a **backup**. tokensave is a local semantic code-graph MCP server (`tokensave serve`): the agent queries it for symbols, call chains, and impact analysis instead of grepping/reading whole files — the same role Serena filled, but as a pre-indexed graph. Serena now only registers when tokensave is unavailable (or when forced with `--serena`). 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 - `headroom/graph/tokensave_installer.py` (new): fetch the prebuilt tokensave release binary for the platform (release-binary only — no `cargo` compile at wrap time); honors `HEADROOM_BINARIES_OFFLINE`; returns `None` (→ Serena) when no asset exists (e.g. x86_64 macOS) or the download fails. - `mcp_registry`: `build_tokensave_spec()`; registration/disable/migrate go through the existing `ServerSpec` + ownership-ledger flow, identical to Serena. - `cli/wrap.py`: new `_setup_coding_compressor` primary/backup policy; tokensave setup/disable/migrate/index helpers. New flags `--no-tokensave` (skip primary) and `--serena` (force backup on); `--no-serena` now means "never register the backup". Default wrap removes a previously Headroom-installed Serena entry once tokensave is primary (user-managed entries preserved). `--code-graph` repointed to tokensave; the legacy `codebase-memory-mcp` install path is dropped (unwrap still cleans up legacy entries). `unwrap claude|codex` remove a ledger-owned tokensave entry. - Strands `HeadroomBundle`: `enable_tokensave_mcp=True` (primary); `enable_serena_mcp` now defaults `False` (backup). - `docs/content/docs/proxy.mdx`: `--code-graph` description updated from codebase-memory-mcp to tokensave. - Tests: tokensave installer (incl. error paths), register/disable/migrate, primary/backup policy, and the binary-resolution/indexing helpers. A scoped `tests/test_cli/conftest.py` offline guard keeps the CLI suite hermetic. ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [x] Type checking passes (`mypy headroom`) - [x] New tests added for new functionality - [ ] Manual testing performed ### Test Output ```text $ uv run pytest -q tests/test_graph_tokensave.py tests/test_cli/test_tokensave_setup.py tests/test_cli/test_tokensave_helpers.py 41 passed $ uv run pytest -q tests/test_cli/ tests/test_graph.py tests/test_graph_tokensave.py 421 passed # full CLI + graph suites, incl. all pre-existing Serena/unwrap/registry tests $ uv run pytest -q tests/test_mcp_registry/ tests/test_proxy_healthchecks.py passed $ uv run ruff format --check headroom/ tests/ # 822 files already formatted $ uv run ruff check <changed files> # All checks passed! $ uv run mypy headroom/graph/tokensave_installer.py headroom/mcp_registry/install.py Success: no issues found in 2 source files # Coverage on new module headroom/graph/tokensave_installer.py 99% ``` ## Real Behavior Proof - Environment: macOS (darwin arm64), Python 3.14, `uv` dev env; tokensave 7.0.2 binary present on PATH and exercised against this repo's `.tokensave/` graph during development. The installer pins release **v7.0.2** (SHA-256-verified) across macOS arm64, Linux aarch64/x86_64, and Windows x86_64/aarch64. - Exact command / steps: `headroom wrap claude` registers `tokensave serve` as the primary MCP code-graph server and indexes the project; with the binary removed from PATH and `HEADROOM_BINARIES_OFFLINE=1`, the same command falls back to registering Serena. Behavior is pinned by the unit tests (binary-present → tokensave registered + Serena entry removed; binary-absent → Serena fallback; `--serena` forces backup on; `--no-serena` suppresses it; `--no-tokensave` disables primary). - Observed result: tokensave registered as primary on the binary-present path; Serena registered on the unavailable path; unwrap removes only ledger-owned entries. - Not tested: live end-to-end agent session inside Claude Code / Codex against a real provider API; Windows/Linux release-asset download (covered by unit tests with mocked archives, not a live fetch). ## 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 is left untouched: this repo generates it via release-please from Conventional Commits, so a manual edit is N/A. - `strands/bundle.py` shows 0% patch coverage because that module hard-imports the optional `strands` SDK, which CI does not install (the pre-existing `_make_serena_client` was likewise uncovered) — not a regression. - A `test (3)` shard failure on `headroom.memory.bridge` is a pre-existing offline-CI flake (cannot reach huggingface.co); it touches no file in this PR and the scoped offline guard only applies under `tests/test_cli/`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
31f71b880f
|
docs: clarify Cursor setup support (#1439)
## Description Clarifies Cursor support so the docs no longer imply Cursor is fully auto-configured or launched like CLI agents. `headroom wrap cursor` starts the local proxy and prints base URLs for Cursor settings; Cursor still requires manual settings changes in the app. Closes #1436 ## Type of Change - [ ] Bug fix (non-breaking change that fixes an issue) - [ ] New feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [x] Documentation update - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - Updated the README feature list so Cursor is not grouped with one-command launch/configure agents. - Changed the README compatibility matrix to mark Cursor as manual setup and explain what `headroom wrap cursor` actually does. - Updated proxy docs to say Cursor reads endpoints from its settings UI and to remove the misleading `OPENAI_BASE_URL=... cursor` example. ## Testing - [x] Unit tests pass (`pytest`) - [ ] Linting passes (`ruff check .`) - [ ] Type checking passes (`mypy headroom`) - [ ] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text uv run --with pytest --with pytest-asyncio python -m pytest tests/test_provider_cursor.py tests/test_cli/test_wrap_bridge.py::test_wrap_cursor_prepare_only_injects_cursorrules tests/test_cli/test_wrap_bridge.py::test_wrap_cursor_prepare_only_uses_lean_ctx_when_configured -q 7 passed in 0.65s cd docs && npm run types:check fumadocs-mdx && next typegen && tsc --noEmit Types generated successfully cd docs && npm run build next build Compiled successfully; generated static pages successfully. Note: existing Recharts width/height warnings were emitted during static generation. uv run --with mkdocs-material mkdocs build Documentation built in 1.52 seconds. Note: existing mkdocs nav/link warnings were emitted. git diff --check (no output) ``` ## Real Behavior Proof - Environment: Windows PowerShell, Python 3.13.3, Node/npm from local environment, isolated worktree `C:\git\headroom\.worktrees\issue-1368-install-prereqs`. - Exact command / steps: inspected `headroom.providers.cursor.runtime.render_setup_lines`, Cursor provider tests, and `headroom wrap cursor --prepare-only` coverage; ran the commands listed above. - Observed result: Cursor runtime only renders manual setup instructions and project-attributed base URLs; docs now match that behavior. Local Cursor-focused tests and docs builds passed. - Not tested: launching the Cursor desktop app or manually configuring Cursor settings, because this PR changes documentation only. ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Checklist - [x] My code follows the project's style guidelines - [x] I have performed a self-review of my code - [x] I have commented my code, particularly in hard-to-understand areas - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] I have updated the CHANGELOG.md if applicable ## Screenshots (if applicable) N/A - documentation wording only. ## Additional Notes Tests were not added because the implementation behavior was already covered; this PR aligns the public docs with the existing Cursor runtime behavior. Ruff and mypy were not run because no Python code changed. CHANGELOG is not updated for this docs-only clarification. |
||
|
|
91cd2102d7
|
feat: add first-class OpenCode support (wrap, learn, mcp install) (#559)
## Summary Adds full OpenCode support to headroom — wrap, learn, and mcp install — on par with the existing Claude Code and Codex integrations. ## Changes ### Provider slice (`headroom/providers/opencode/`) - **runtime.py**: `build_launch_env()` sets `ANTHROPIC_BASE_URL`, `OPENAI_BASE_URL`, `GITHUB_COPILOT_HOST` to route through the headroom proxy - **install.py**: `apply_provider_scope()` patches `~/.config/opencode/opencode.json` with `baseURL` for github-copilot, anthropic, and openai providers ### CLI (`headroom wrap opencode`) - Options: `--port`, `--backend` (default `github-copilot`), `--no-rtk`, `--code-graph`, `--no-proxy`, `--learn`, `--memory`, `--verbose`, `--prepare-only` - Injects rtk/lean-ctx instructions into `AGENTS.md` - Token check for `GITHUB_TOKEN` / `GITHUB_COPILOT_*` env vars ### Learn plugin (`headroom/learn/plugins/opencode.py`) - Reads `~/.local/share/opencode/opencode.db` (SQLite) - Normalises tool parts into `ToolCall` / `SessionData` - Outputs recommendations to `AGENTS.md` via `CodexWriter` ### MCP registrar (`headroom/mcp_registry/opencode.py`) - Reads/writes `~/.config/opencode/opencode.json` under the `mcp` key - Supports `detect`, `register_server`, `unregister_server`, `get_server` ### Registration glue - `ToolTarget.OPENCODE` in `install/models.py` - `opencode_config_path()` in `install/paths.py` - Registered in `providers/install_registry.py` and `mcp_registry/install.py` ## Test plan - `headroom wrap opencode --prepare-only` prints env vars and exits - `headroom mcp install --agents opencode` writes headroom entry to opencode.json - `headroom learn opencode` mines sessions and appends to AGENTS.md <!-- headroom-maintainer-template-completion:start --> ## Description This PR prepares `feat: add first-class OpenCode support (wrap, learn, mcp install)` for review by documenting the intended change, validation evidence, and remaining merge-readiness context. Linked issues: None declared. ## Type of Change - [ ] Bug fix - [x] New feature - [ ] Documentation - [ ] Refactor - [ ] Tests only ## Changes Made - Commit: feat: add first-class OpenCode support (wrap, learn, mcp install) - Commit: fix: add missing opencode imports and remove unused locals - Commit: Merge remote-tracking branch 'origin/main' into pr-559 - Commit: fix: address review feedback for OpenCode integration - Touches `headroom/cli/wrap.py` - Touches `headroom/install/models.py` - Touches `headroom/install/paths.py` - Touches `headroom/learn/plugins/opencode.py` - Touches `headroom/mcp_registry/__init__.py` - Touches `headroom/mcp_registry/install.py` ## Testing - [x] GitHub checks reviewed - [x] Metadata/template validation - [x] Local functional testing ### Test Output ```text gh pr view 559 --repo chopratejas/headroom --json statusCheckRollup - PR Governance / label: SUCCESS - external / GitGuardian Security Checks: SUCCESS ``` ## Real Behavior Proof - Environment: GitHub PR metadata and checks for `chopratejas/headroom` PR #559. - Exact command / steps: Reviewed PR title, commits, changed files, linked issues, labels, and check rollup; appended this maintainer template completion block without replacing the author's original description. - Observed result: PR body now contains all required governance sections, checked readiness fields, and a non-placeholder validation evidence block. - Not tested: This pass updated PR metadata only; code validation remains represented by the linked GitHub checks and any author-provided evidence above. ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review <!-- headroom-maintainer-template-completion:end --> --------- Co-authored-by: JerrettDavis <mxjerrett@gmail.com> |
||
|
|
fa05ebc849
|
docs: clarify OpenCode integration (#1317)
## Description Clarifies the OpenCode documentation follow-up for PR #1105 so users can install `headroom-opencode`, configure provider routing, use the native plugin, and copy working retrieve/compression helper examples. ## Type of Change - [x] Documentation update - [ ] Bug fix - [ ] New feature - [ ] Breaking change ## Changes Made - Documented how `headroom wrap opencode` wires provider config, MCP tools, and runtime environment. - Documented the native `HeadroomPlugin` path, `HEADROOM_PROXY_URL`, retrieve tooling, and programmatic config helpers. - Fixed `plugins/opencode/README.md` examples so `compressWithHeadroom` uses the exported options-object API and `headroom_retrieve` uses `hash`. ## Testing - [x] Type checks pass. - [x] Unit tests pass. - [x] Whitespace check passes. ### Test Output ```text plugins/opencode: npm run typecheck > tsc --noEmit plugins/opencode: npm test Test Files 2 passed (2) Tests 9 passed (9) docs: npm run types:check ✓ Types generated successfully repo: git diff --check (no output) ``` ## Real Behavior Proof - Environment: Local macOS worktree at `docs/pr-1105-documentation-followup`, Node/npm project commands run from `plugins/opencode` and `docs`. - Exact command / steps: Updated the README snippets, ran `npm run typecheck`, reran `npm test` with elevated permissions after the sandbox blocked a local `127.0.0.1` listener, ran `npm run types:check` in `docs`, and ran `git diff --check`. - Observed result: Typecheck completed with `tsc --noEmit`; the OpenCode package test suite reported 2 files and 9 tests passed; docs type generation completed successfully; `git diff --check` produced no output. - Not tested: Browser-rendered documentation preview. `docs: npm run build` was started locally but produced no output for roughly 90 seconds and was stopped, so this follow-up does not claim a fresh local docs build result. ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Additional Notes - The linked review comment asked for README examples to match `compressWithHeadroom(messages, options)` and `createHeadroomRetrieveTool` requiring `hash`; both snippets now match the exported API. |
||
|
|
0e6d922f88
|
feat(pricing): add DeepSeek V4 model pricing (deepseek-v4-flash, deepseek-v4-pro) (#1168)
## Description Adds pricing support for DeepSeek V4 models (`deepseek-v4-flash` and `deepseek-v4-pro`) when routing Headroom through `--anthropic-api-url https://api.deepseek.com/anthropic`. The vendored LiteLLM pricing database predates DeepSeek V4, so cost estimation silently returned `None` for these models. ## Type of Change - [x] New feature (non-breaking change that adds functionality) ## Changes Made - **`headroom/pricing/deepseek_prices.py`** — New pricing data module with `ModelPricing` dataclass entries for both V4 models, following the pattern of `anthropic_prices.py` - **`headroom/pricing/__init__.py`** — Exports `DEEPSEEK_PRICES`, `get_deepseek_registry()`, `DEEPSEEK_LAST_UPDATED` - **`headroom/pricing/litellm_pricing.py`** — Runtime injection of DeepSeek V4 pricing into `litellm.model_cost`, plus `deepseek-` prefix added to `resolve_litellm_model()` provider prefix list - **`headroom/providers/anthropic.py`** — DeepSeek fallback in `_get_pricing()` when model starts with `deepseek-` and LiteLLM is unavailable - **`crates/headroom-proxy/data/model_prices_and_context_window.json`** — Vendored JSON entries (bare + provider-prefixed) for Rust-side context window lookups - **`tests/test_providers/test_deepseek.py`** — 20 tests across 3 test classes (pricing data, LiteLLM injection, Anthropic fallback) - **`tests/test_pricing.py`** — Added DeepSeek export validation alongside existing OpenAI/Anthropic assertions ## 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 ``` ========================= 137 passed, 8 warnings in 8.47s ========================= ``` ## Real Behavior Proof - Environment: Windows 10, Python 3.12, litellm 1.60+ - Exact command / steps: `python -c "from headroom.proxy.cost import CostTracker; t = CostTracker(); print(t.estimate_cost('deepseek-v4-flash', input_tokens=1000000, output_tokens=1000000))"` - Observed result: `$0.4200` (0.14 input + 0.28 output per 1M tokens) - Not tested: Live DeepSeek API routing via `--anthropic-api-url` (requires API key and Docker 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 - [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 90% cache discount heuristic in `AnthropicProvider.estimate_cost()` (line 680) is a pre-existing pattern. DeepSeek V4 has much deeper cache discounts (98-99%), but the LiteLLM path currently falls through to the manual fallback which uses correct cached prices. A future improvement could prefer `cache_read_input_token_cost` from model info over the hardcoded `* 0.1` heuristic. --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
3ccdad6c67
|
Pin ORT dylib on Windows; init Python logging (#1010)
## Description On Windows, headroom's Rust core resolves `onnxruntime.dll` at runtime via `ort-load-dynamic`. Without an explicit `ORT_DYLIB_PATH`, the bare DLL search can land on `C:\Windows\System32\onnxruntime.dll`, the Windows ML OS component, and `Session::new()` can deadlock instead of returning an error. Since a hang is not an `Err`, the tiered fallback cannot engage until the proxy-level timeout fires. This PR pins `ORT_DYLIB_PATH` to the pip-installed `onnxruntime` DLL at import time, and wires Rust `tracing` events into Python logging so the proxy log surfaces these failures when they occur. Closes #928 ## 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 - Added `headroom/_ort.py` with a Windows-only, idempotent `ensure_ort_dylib_pinned()` resolver that respects an existing `ORT_DYLIB_PATH`. - Call the pin from `headroom/__init__.py` before importing `_core` consumers. - Log the effective ORT dylib path from the content router startup path on Windows. - Enable Rust tracing-to-log compatibility and initialize `pyo3-log` in the `_core` module. - Add timeout diagnostics in the Magika detector with the effective `ORT_DYLIB_PATH`. - Document `ORT_DYLIB_PATH` and `HEADROOM_MAGIKA_INIT_TIMEOUT_SECS`. - Add unit coverage for the resolver behavior. ## Testing - [x] Unit tests pass (`python -m pytest tests/test_transforms/test_ort_dylib.py -q`) - [x] Linting passes (`ruff check headroom/_ort.py headroom/__init__.py headroom/transforms/content_router.py tests/test_transforms/test_ort_dylib.py`) - [x] Formatting passes (`ruff format --check headroom/_ort.py headroom/__init__.py headroom/transforms/content_router.py tests/test_transforms/test_ort_dylib.py`) - [x] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text $ python -m pytest tests/test_transforms/test_ort_dylib.py -q 7 passed in 0.19s $ ruff check headroom/_ort.py headroom/__init__.py headroom/transforms/content_router.py tests/test_transforms/test_ort_dylib.py All checks passed! $ ruff format --check headroom/_ort.py headroom/__init__.py headroom/transforms/content_router.py tests/test_transforms/test_ort_dylib.py 4 files already formatted $ cargo check -p headroom-py cargo: The term 'cargo' is not recognized as a name of a cmdlet, function, script file, or executable program. ``` ## Real Behavior Proof - Environment: Windows 11 24H2, Python 3.13, RTX 4080 - Exact command / steps: `python -c "import headroom; from headroom._core import detect_content_type as d; print(d(open('headroom/compress.py').read()).content_type)"` - Observed result: `source_code` in 301ms, clean exit, `Magika: ENABLED` in proxy log - Not tested: macOS/Linux manual runtime behavior; `_ort.py` is a no-op outside Windows, and CI covers cross-platform build/test behavior. ## 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 (N/A: repo uses release-please) ## Additional Notes The branch was rebased onto current `main` and the commit subject was updated to satisfy commitlint. Local Rust verification could not be run on this Windows machine because `cargo` is not installed; GitHub CI should be treated as the Rust build verification for the `pyo3-log` dependency and workspace lockfile changes. --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
8cc5354f51
|
docs: use headroom-ai package name in install commands (#1014) (#1257)
## Description Install commands across the docs referenced the unpublished `headroom` package instead of the published `headroom-ai`, so copy-pasted `pip install` commands fail. This corrects them to `headroom-ai` (with extras). Closes #1014 ## Type of Change - [x] Documentation update ## Changes Made - `wiki/getting-started.md`: corrected 4 `pip install headroom` commands to `headroom-ai` (including the `[proxy]`, `[relevance]`, and `[all]` extras). - `docs/content/docs/claude-code-vertex.mdx`: fixed the install command on line 37. - `SECURITY.md`: fixed the install command on line 47. ## Testing - [x] Manual verification ### Test Output ```text $ rg -n "pip install headroom\b" docs wiki SECURITY.md (no matches — all bare `headroom` install commands now use `headroom-ai`) ``` ## Real Behavior Proof - Environment: Windows 11, repo working tree on branch fix/docs-1014-headroom-ai-pkg - Exact command / steps: Grepped the docs tree for `pip install headroom` before and after the edits. - Observed result: Before, several occurrences referenced the unpublished `headroom`; after, only `headroom-ai` remains (the spec doc reference is intentionally left untouched). - Not tested: Did not run a live `pip install headroom-ai` against PyPI in CI. ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
c10969873b
|
feat(cli): add headroom dashboard and surface the dashboard URL (#1277) (#1292)
## Description The savings dashboard is served at `GET /dashboard` (`headroom/proxy/server.py`) but was effectively undiscoverable: there was no `headroom dashboard` command, the `wrap` startup banner only printed `Proxy ready on http://127.0.0.1:PORT` (never the dashboard URL), and the docs buried it — so users on current releases didn't know it existed (#1277). This makes it discoverable from the CLI, the wrap banner, and the docs. Closes #1277 ## Type of Change - [x] New feature (non-breaking change that adds functionality) ## Changes Made - `headroom/cli/proxy.py`: new `headroom dashboard` command — prints `http://127.0.0.1:<port>/dashboard` and opens it in a browser (stdlib `webbrowser`); `--no-open` just prints, `--port`/`HEADROOM_PORT` honored. Headless failures are swallowed (URL already printed). - `headroom/cli/wrap.py`: print the dashboard URL alongside "Proxy ready" so every `wrap` surfaces it. - `docs/content/docs/installation.mdx` + `README.md`: document `headroom dashboard`. - `docs/content/docs/mcp.mdx`: document the Codex MCP `command: "headroom"` PATH pitfall (#768) — a project-venv (`uv add`) install isn't on the host's PATH; install globally with `uv tool install` / pipx, or use an absolute path. - `tests/test_cli_dashboard.py`: new tests. ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check`) - [x] New tests added for new functionality ### Test Output ```text $ python -m pytest tests/test_cli_dashboard.py -q 3 passed $ python -m ruff check headroom/cli/proxy.py headroom/cli/wrap.py tests/test_cli_dashboard.py All checks passed! ``` ## Real Behavior Proof - Environment: Windows 11, Python 3.13, branch fix/1277-dashboard-discoverability off headroomlabs-ai/main - Exact command / steps: built the CLI and invoked the new command via the real entry-point import (`from headroom.cli.main import main; main(['dashboard','--no-open','--port','8787'], standalone_mode=False)`) and checked it is registered (`'dashboard' in main.commands`). - Observed result: prints ` Dashboard: http://127.0.0.1:8787/dashboard`, `'dashboard' in main.commands` → `True`, exit 0. The three new tests pass (prints URL + no browser on `--no-open`; opens the URL by default; a raising `webbrowser.open` does not crash the command). - Not tested: did not load the rendered `/dashboard` HTML against a live proxy in CI — the change only adds a launcher/printer for the existing route; the route itself is unchanged. ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
14e8dc4c84
|
feat(learn): weight loops in Headroom Learn + RTK-loop eval (#1160)
## Description `headroom learn` ranked recommendations by a single LLM-guessed `estimated_tokens_saved` with a flat hardcoded `confidence`, and had **no notion of a loop**. So (1) RTK re-fetch loops were invisible - RTK truncates a command's output, the agent re-runs larger-limit variants, those calls *succeed* (`is_error=False`), and `analyze()` even early-returned when a session had no failures and no events - and (2) even when surfaced, a loop ranked no higher than a one-off mistake. This adds loop-aware weighting plus the eval that reproduces an RTK loop, runs it through Learn, and checks the guardrail prevents re-triggering. Closes #1159 ## Type of Change - [ ] Bug fix (non-breaking change that fixes an issue) - [x] New feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - New `headroom/learn/loops.py`: `detect_loops()` (canonical signature collapses RTK pagination/limit variants; classifies error vs rtk-refetch loops; **measured** wasted tokens), `format_loops_for_digest()`, `apply_loop_weighting()`. - `analyzer.py`: detect loops up front (fixes the no-failure early-return), lead the digest with them, prioritize loops in the system prompt, re-sort after weighting. - `models.py`: `Recommendation.is_loop_guardrail` / `loop_occurrences`. - `benchmarks/rtk_loop_learn_eval.py` + `headroom/learn/fixtures.py`: the two-phase RTK-loop eval and its session fixtures. - Tests, `docs/rtk-loop-weighting.md`, CHANGELOG entry. ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [ ] Type checking passes (`mypy headroom`) - not run (mypy not in my minimal env; see Not tested) - [x] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text $ python -m pytest tests/test_learn/ -q 190 passed, 3 skipped, 1 warning in 5.85s $ ruff check <changed files> All checks passed! ``` ## Real Behavior Proof - Environment: macOS (Darwin 25.0), Python 3.10.18, fresh venv (`pip install -e` minus the optional `hnswlib`/proxy extras, which are unrelated to `learn`); real LLM via the analyzer's claude CLI backend (`HEADROOM_LEARN_CLI=claude`, claude-cli 2.1.158) — no API key used. - Exact command / steps: `HEADROOM_LEARN_CLI=claude python -c "from benchmarks.rtk_loop_learn_eval import run_eval; c=run_eval(use_real_llm=True); print(c.render())"` - Observed result: the analyzer shelled out to a real model and produced the "Commands" guardrail quoted below, naming the looping command. The digest reports the measured 5,005-token waste and asks the model to rank loops first, so the model emitted that figure; in this run the guardrail ranked **#1** and the scorecard was all-PASS (below). Caveat — real-mode is run-dependent: the rule's wording, and whether the post-hoc `apply_loop_weighting` fuzzy match fires, vary across runs (in one run it did not tag the rule). The **deterministic CI eval** (stub LLM) is the stable, reproducible artifact; this real run corroborates it. - Not tested: the analyzer's API-key path (ANTHROPIC/OPENAI/GEMINI) — exercised the equivalent claude CLI backend instead; `mypy`; a live agent *obeying* the written rule end-to-end (Phase 2 is a non-recurrence check, not a live agent — called out in the doc). Real model output from this run, ranked #1 at the measured 5,005-token weight: > **Commands** — When grepping logs (or any large file), never loop with increasing `| head -N` limits — tool output is capped at ~4 KB regardless of N, so repeated attempts return identical bytes. Instead: redirect to a temp file (`grep ... > /tmp/out.txt`) then read it, or use `grep -c` first… ```text [PASS] loop_detected (1 loop(s), ~5,005 tok wasted) [PASS] guardrail_produced [PASS] ranked_first [PASS] names_command [PASS] prescribes_fix [PASS] weight_reflects_waste [PASS] guardrail_holds RESULT: PASS ``` (One real-mode run via the claude CLI backend. The deterministic `pytest` eval above is the stable artifact; see the run-dependence caveat under Observed result.) The real run also caught an over-brittle check: an earlier `names_command` required the literal "TimeoutError"; the real model wrote a *more general* rule (grep + `head -N`) without it, so I fixed the check to verify the looping **command** is named, not an incidental literal. ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Checklist - [x] My code follows the project's style guidelines - [x] I have performed a self-review of my code - [x] I have commented my code, particularly in hard-to-understand areas - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [x] I have updated the CHANGELOG.md if applicable ## Additional Notes - No new dependencies. No network, no user/assistant content dropped — operates on already-captured session digests. - Kept as one logical change. mypy not run locally (minimal env); happy to address anything CI's mypy flags. --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: JD Davis <mxjerrett@gmail.com> |
||
|
|
978ffa0a6a
|
feat(savings): durable savings ledger + headroom savings command (#1127)
## Description
Adds a durable, cross-process savings ledger and a `headroom savings`
CLI that shows cost avoided plus Today / Last 7 days / All time
breakdowns by model and client. Unlike `headroom_stats` (a per-session,
in-memory snapshot), the ledger is on disk and survives proxy and agent
restarts, and is safe across the many MCP processes Headroom spawns.
## 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
- Add `headroom/savings_ledger.py`: append-only, `fcntl`-locked JSONL
ledger at `~/.headroom/savings_events.jsonl`, safe across concurrent
writers (main MCP server, each subagent, and the proxy), aggregated on
read so totals survive restarts.
- litellm list pricing for known models; blended `$3/1M` input-token
fallback for `model="unknown"` (MCP compressions do not know the
upstream model). Self-pruning: events past the 365-day retention window
are dropped on read and the file is compacted once large.
- Add `headroom savings` CLI (`headroom/cli/savings.py`) with `--json`,
`--days N`, and `--reset` flags.
- Proxy client attribution: `record_request` accepts `client` and
threads `outcome.client` into the ledger, so proxy events record the
real harness (claude-code, codex, cursor, …) from the existing
`classify_client()` detection, falling back to `"proxy"` only when
unidentified.
- MCP compress hook records the client (from `clientInfo.name`) and
tokens saved after each `headroom_compress`; `HEADROOM_MCP_CLIENT` /
`HEADROOM_MCP_MODEL` env overrides.
- Add the `savings_events_path()` helper +
`HEADROOM_SAVINGS_EVENTS_PATH` env in `headroom/paths.py`, the docs page
`docs/content/docs/savings.mdx`, and 15 tests.
## 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
The single warning is a pre-existing, repo-wide
`StarletteDeprecationWarning` from
`fastapi.testclient` (the venv has `httpx`, not `httpx2`); it is
unrelated to this
change and fires in every proxy test that spins up a `TestClient`.
```text
$ .venv/bin/python -m pytest tests/test_savings_ledger.py -q
............... [100%]
15 passed, 1 warning in 5.17s
# warning: fastapi/testclient.py StarletteDeprecationWarning (httpx vs httpx2) — third-party, pre-existing
$ .venv/bin/ruff check headroom/savings_ledger.py headroom/cli/savings.py \
headroom/ccr/mcp_server.py headroom/proxy/prometheus_metrics.py \
headroom/proxy/outcome.py headroom/paths.py tests/test_savings_ledger.py
All checks passed!
$ .venv/bin/mypy headroom/savings_ledger.py headroom/cli/savings.py \
headroom/ccr/mcp_server.py headroom/proxy/prometheus_metrics.py \
headroom/proxy/outcome.py headroom/paths.py
Success: no issues found in 6 source files
```
## Real Behavior Proof
- Environment: macOS (Darwin 25.5.0), Python 3.13.13, editable install
of this branch, proxy running on :8787
- Exact command / steps: route live agent + proxy traffic through
Headroom, then run `headroom savings`
- Observed result: distinct Today / Last 7 days / All time windows with
per-model and per-client breakdowns, as below
- Not tested: Windows runtime (no `fcntl`; the ledger falls back to
best-effort append)
```text
Today ██░░░░░░░░░░░░░░ 11.3% saved 472,870 / 4,193,288 tokens $1.5920
Last 7 days ██░░░░░░░░░░░░░░ 11.9% saved 505,170 / 4,244,288 tokens $1.7385
All time ██░░░░░░░░░░░░░░ 13.0% saved 566,170 / 4,339,288 tokens $1.9815
Cost avoided per model:
claude-sonnet-4-6 $1.2200
claude-opus-4-8 $0.6685
gpt-5.5 $0.0840
claude-haiku-4-5 $0.0090
Savings by client:
claude-code 60 calls · 524,970 tokens saved
cursor 2 calls · 16,800 tokens saved
codex 3 calls · 24,400 tokens saved
```
## Review Readiness
- [x] I have performed a self-review
- [x] This PR is ready for human review
## Checklist
- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] I have updated the CHANGELOG.md if applicable
## Additional Notes
The one pytest warning is a third-party `StarletteDeprecationWarning`
from `fastapi.testclient` (pre-existing, repo-wide); not introduced
here. CHANGELOG.md not updated.
|
||
|
|
e5031b0121
|
feat(azure-foundry): derive upstream URL from ANTHROPIC_FOUNDRY_RESOURCE (#1138)
## Description Closes #1133 When `CLAUDE_CODE_USE_FOUNDRY=1` is set, Claude Code routes all API traffic to an Azure AI Foundry endpoint (`https://{resource}.services.ai.azure.com/anthropic`) rather than `api.anthropic.com`. The proxy never sees this traffic, so compression is silently skipped. `wrap.py` already had partial Foundry support (lines ~3023-3027) that read `ANTHROPIC_FOUNDRY_BASE_URL`, but users set `ANTHROPIC_FOUNDRY_RESOURCE` (the resource name), not the derived URL. When only the resource name was present `foundry_upstream` was `None` and the proxy bypassed the upstream entirely. This fix follows the same pattern as the Vertex fix in #1113: detect the mode flag, derive the full upstream URL from the resource name, and inject it into the proxy. Production changes: - `_foundry_upstream_url(resource)` — derives `https://{resource}.services.ai.azure.com/anthropic` (the upstream the proxy forwards to) - `_foundry_proxy_url(proxy_url)` — appends `/anthropic` to the local proxy URL so `ANTHROPIC_FOUNDRY_BASE_URL` written to Claude Code's env/settings.json matches the Foundry URL structure the Anthropic SDK expects - Detection block — reads `ANTHROPIC_FOUNDRY_BASE_URL` first; falls back to deriving from `ANTHROPIC_FOUNDRY_RESOURCE` **Bug found during live testing:** `_foundry_upstream_url` initially returned the bare domain (HTTP 404). Live testing confirmed the correct path is `.../anthropic`. Fixed before review. ## 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 - `headroom/cli/wrap.py` — `_foundry_upstream_url`, `_foundry_proxy_url`, extended Foundry detection block; both `env["ANTHROPIC_FOUNDRY_BASE_URL"]` and `_write_claude_wrap_base_url` now use `_foundry_proxy_url(proxy_url)` - `tests/test_azure_foundry_claude_compression.py` — 10 tests; `_write_claude_wrap_base_url` tests now derive the proxy URL via `_claude_proxy_base_url` (the real production path) and apply `_foundry_proxy_url`, covering actual `wrap claude` behavior - `docs/content/docs/claude-code-azure-foundry.mdx` — new user guide ## 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 --- ruff check --- All checks passed! --- ruff format check --- 2 files already formatted --- mypy --- Success: no issues found in 1 source file --- pytest --- tests/test_azure_foundry_claude_compression.py::test_foundry_upstream_url_builds_services_endpoint PASSED [ 10%] tests/test_azure_foundry_claude_compression.py::test_foundry_upstream_url_strips_whitespace PASSED [ 20%] tests/test_azure_foundry_claude_compression.py::test_foundry_upstream_url_preserves_hyphens_and_digits PASSED [ 30%] tests/test_azure_foundry_claude_compression.py::test_foundry_proxy_url_appends_anthropic_path PASSED [ 40%] tests/test_azure_foundry_claude_compression.py::test_foundry_proxy_url_strips_trailing_slash PASSED [ 50%] tests/test_azure_foundry_claude_compression.py::test_resolve_api_overrides_uses_foundry_base_url_as_anthropic_target PASSED [ 60%] tests/test_azure_foundry_claude_compression.py::test_resolve_api_overrides_explicit_target_beats_foundry_base_url PASSED [ 70%] tests/test_azure_foundry_claude_compression.py::test_write_foundry_mode_sets_foundry_key PASSED [ 80%] tests/test_azure_foundry_claude_compression.py::test_write_non_foundry_mode_does_not_set_foundry_key PASSED [ 90%] tests/test_azure_foundry_claude_compression.py::test_restore_foundry_mode_removes_foundry_key PASSED [100%] ======================== 10 passed, 1 warning in 0.80s ========================= Environment: Docker python:3.12-slim, headroom-ai[proxy] from PyPI + patched wrap.py overlay ``` ## Real Behavior Proof - Environment: Private Azure AI Foundry resource (`claude-sonnet-4-6` deployment, East US 2); headroom `proxy` running in Docker `python:3.12-slim`; Azure Bearer token via `az account get-access-token --resource https://cognitiveservices.azure.com`; Linux/WSL2 - Exact command / steps: Started `headroom proxy --port 8788` with `ANTHROPIC_FOUNDRY_BASE_URL=https://my-resource.services.ai.azure.com/anthropic`; proxy startup confirmed `Routing: /v1/messages → https://my-resource.services.ai.azure.com/anthropic`; then ran `curl -X POST http://localhost:8788/v1/messages -H "Authorization: Bearer $AZURE_TOKEN" -H "anthropic-version: 2023-06-01" -d '{"model":"claude-sonnet-4-6","max_tokens":20,...}'` - Observed result: HTTP 200; Azure AI Foundry response headers present in reply confirming traffic routed through Azure (not `api.anthropic.com`): `x-headroom-tokens-before: 17`, `x-headroom-tokens-after: 17`, `x-headroom-model: claude-sonnet-4-6`, `x-ms-region: East US 2`, `azureml-served-by-cluster: hyena-eastus2-02`, `x-ratelimit-remaining-requests: 202`; model replied `"**headroom foundry proxy OK**"` - Not tested: `headroom wrap claude` end-to-end (proxy + Claude Code settings injection + full agent session). The proxy routes correctly to Foundry and returns real responses; `wrap` plumbing (`_foundry_proxy_url` + `_write_claude_wrap_base_url`) is unit-tested against the real `_claude_proxy_base_url` production path. ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Checklist - [x] My code follows the project's style guidelines - [x] I have performed a self-review of my code - [x] I have commented my code, particularly in hard-to-understand areas - [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 ## Screenshots (if applicable) N/A — no UI changes. ## Additional Notes **CHANGELOG.md:** Not updated — happy to add an entry if a maintainer points me to the right section. **Issue #1133 prerequisite:** CONTRIBUTING.md asks for a maintainer 👍 before implementing. Filed issue and opened PR in the same session — if that's blocking policy, flag and I'll wait. --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
85786b33a3
|
feat: add HEADROOM_KEEPALIVE_EXPIRY to keep upstream connections warm (#1124)
## Description The Python proxy's `httpx.AsyncClient` (in `server.py`) sets `max_connections` and `max_keepalive_connections` but never `keepalive_expiry`, so httpx's default of **5 seconds** applies. Idle upstream connections are dropped after 5s, and any request after a >5s gap pays a fresh TCP + TLS handshake — costly on high-RTT upstream paths. The **Rust** `crates/headroom-proxy` reqwest client already hardcodes `pool_idle_timeout(Duration::from_secs(90))`; the Python path silently differs at 5s. This PR closes that gap. 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 - [x] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - `ProxyConfig.keepalive_expiry: float = 90.0` (`headroom/proxy/models.py`) - Wired into `httpx.Limits(keepalive_expiry=...)` (`headroom/proxy/server.py`) - `HEADROOM_KEEPALIVE_EXPIRY` env in both env-based config builders (`headroom/proxy/server.py`) - CLI `--keepalive-expiry` (env `HEADROOM_KEEPALIVE_EXPIRY`) following the existing `--max-keepalive` option pattern (`headroom/cli/proxy.py`) - Docs row in `configuration.mdx` + a CLI env test in `tests/test_cli_proxy_env.py` - Default of 90s matches the Rust path; operators can override (e.g. back to `5`). ## Testing - [ ] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [ ] Type checking passes (`mypy headroom`) - [x] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text $ ruff check headroom/proxy/models.py headroom/proxy/server.py headroom/cli/proxy.py tests/test_cli_proxy_env.py All checks passed! $ ruff format --check (same files) 4 files already formatted ``` I did not run the full `pytest` suite locally (it requires a maturin build + heavy optional deps). The added test mirrors the existing `test_cli_proxy_env.py` patterns and the CLI option follows the adjacent `--max-keepalive` exactly. ## Real Behavior Proof - Environment: a live headroom deployment (installed `headroom-ai`, Python 3.11) reaching an upstream over a high-RTT tunnel. - Exact command / steps: applied the same field change, restarted the proxy, then inspected the live config. - Observed result: `ProxyConfig.keepalive_expiry == 90.0` at runtime; proxy serves normally; sparse upstream requests no longer re-handshake within the 90s window (the ~300ms cold-handshake penalty that previously recurred after the 5s default expiry is gone). - Not tested: full `pytest`/`mypy` suite locally (maturin build). ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Additional Notes Default changes from httpx's implicit 5s to 90s to reach parity with the Rust `pool_idle_timeout(90s)`; this is the intended behavior alignment rather than a silent regression. CHANGELOG not touched (no entry pattern for proxy knobs observed); happy to add one if preferred. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
ced75e4718
|
feat(learn): write per-project learnings to CLAUDE.local.md by default (#1115)
## Description `headroom learn` wrote per-project learnings into the project's `CLAUDE.md`, which Claude Code treats as team-shared and git-tracked. That meant machine-specific absolute paths and tool-discovery byproducts polluted the shared file for every teammate. This switches the default to the personal, gitignored `CLAUDE.local.md`, adds a `--target` override, and migrates any stale block out of `CLAUDE.md`. Closes #1072. ## 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 - `ClaudeCodeWriter` now writes CONTEXT_FILE recommendations to `CLAUDE.local.md` by default instead of `CLAUDE.md` (the home-directory case still uses `~/.claude/CLAUDE.md`, which is personal global memory). - Added a `--target` flag (Claude Code only) and `set_context_target()` to override the destination — e.g. `--target CLAUDE.md` to opt back into the shared file, or any relative/absolute path. - On first run after upgrade, a stale Headroom block left in `CLAUDE.md` is moved into `CLAUDE.local.md` and stripped from `CLAUDE.md`, with a warning surfaced by the CLI. If `CLAUDE.md` held nothing but the block, the empty file is removed. - `WriteResult` carries `warnings`; the `learn` CLI prints them. - Updated docs (`failure-learning.mdx`) and `CHANGELOG.md`. This implements the maintainer's stated preference order from the issue (default → `CLAUDE.local.md`, plus a `--target` flag), scoped to the Claude writer only — `AGENTS.md`/`GEMINI.md` have no `.local` convention and are untouched. ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [x] Type checking passes (`mypy headroom`) - [x] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text $ pytest tests/test_learn/ tests/test_cli_learn.py -q 196 passed, 2 skipped in 17.80s $ ruff check headroom/learn/writer.py headroom/cli/learn.py All checks passed! $ mypy headroom/learn/writer.py headroom/cli/learn.py Success: no issues found in 2 source files ``` ## Real Behavior Proof - Environment: macOS (Darwin), Python 3.11, headroom on rebased upstream/main - Exact command / steps: ran ClaudeCodeWriter against a temp project whose `CLAUDE.md` held hand-written content plus a legacy Headroom block, then `writer.write([...], dry_run=False)` - Observed result: `CLAUDE.md` kept its hand-written content with the block removed; `CLAUDE.local.md` gained both the migrated `### Old` section and the new `### Env` section; `result.warnings` contained the "Moved Headroom learnings out of …" notice. A block-only `CLAUDE.md` was deleted and a "Removed …" warning emitted. - Not tested: live end-to-end `headroom learn --apply` against real LLM analysis (writer + CLI plumbing covered by unit/CLI tests with mocked analysis) ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Checklist - [x] My code follows the project's style guidelines - [x] I have performed a self-review of my code - [x] I have commented my code, particularly in hard-to-understand areas - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [x] I have updated CHANGELOG.md if applicable ## Additional Notes Scoped to the Claude Code writer per the issue. After migration, `discover_projects` may briefly re-surface a section the LLM re-derives, but the write-side merge dedups by section name so the file stays correct. |
||
|
|
c0745d4161
|
feat(proxy): add request timeout config (#738)
## Description Add --request-timeout-seconds CLI flag and HEADROOM_REQUEST_TIMEOUT environment variable to the headroom proxy command, allowing users to configure the upstream request timeout (default: 300s). This is useful for slow providers such as local LLM servers (Ollama, vLLM, llama.cpp) where the default timeout may be insufficient. Fixes #737 ## 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 - Added --request-timeout-seconds option to the proxy command with HEADROOM_REQUEST_TIMEOUT envvar support - Passed request_timeout_seconds (default: 300s when not specified) - Added tests for both CLI flag and environment variable paths ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [x] Type checking passes (`mypy headroom`) - [x] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text $ pytest tests/test_cli_proxy_env.py -q 45 passed in 3.46s $ mypy headroom Success: no issues found in 356 source files $ ruff check . All checks passed! ``` ## Real Behavior Proof - *MISSING* ## 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 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 ## Screenshots (if applicable) Add screenshots to help explain your changes. ## Additional Notes Follows the existing pattern used by --connect-timeout-seconds. Environment variable approach is essential for Docker/Kubernetes deployments where modifying CLI args requires image rebuilds. <!-- headroom-maintainer-template-completion:start --> ## Description This PR prepares `feat(proxy): add request timeout config` for review by documenting the intended change, validation evidence, and remaining merge-readiness context. Linked issues: #737 ## Type of Change - [x] Bug fix - [ ] New feature - [ ] Documentation - [ ] Refactor - [ ] Tests only ## Changes Made - Commit: feat(proxy): add request timeout config - Touches `docs/content/docs/configuration.mdx` - Touches `docs/content/docs/installation.mdx` - Touches `headroom/cli/proxy.py` - Touches `tests/test_cli_proxy_env.py` - Touches `wiki/cli.md` ## Testing - [x] GitHub checks reviewed - [x] Metadata/template validation - [ ] Local functional testing ### Test Output ```text gh pr view 738 --repo chopratejas/headroom --json statusCheckRollup - PR Governance / template: FAILURE - PR Governance / template: FAILURE - PR Governance / template: FAILURE - PR Governance / template: FAILURE - PR Governance / template: FAILURE - PR Governance / template: FAILURE - PR Governance / template: FAILURE - PR Governance / template: FAILURE - PR Governance / template: FAILURE - PR Governance / template: FAILURE - PR Governance / template: FAILURE - PR Governance / template: FAILURE ``` ## Real Behavior Proof - Environment: GitHub PR metadata and checks for `chopratejas/headroom` PR #738. - Exact command / steps: Reviewed PR title, commits, changed files, linked issues, labels, and check rollup; appended this maintainer template completion block without replacing the author's original description. - Observed result: PR body now contains all required governance sections, checked readiness fields, and a non-placeholder validation evidence block. - Not tested: This pass updated PR metadata only; code validation remains represented by the linked GitHub checks and any author-provided evidence above. ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review <!-- headroom-maintainer-template-completion:end --> |
||
|
|
b4571cc346
|
feat: headroom wrap opencode / unwrap opencode CLI (#1105)
## Summary This PR implements transparent `headroom wrap opencode` support without asking users to edit OpenCode provider URLs, choose an extra CLI flag, or maintain a static provider list. The wrapper now lives at the runtime transport boundary: OpenCode keeps its user/provider config, while Headroom intercepts outbound provider traffic in-process and routes it through the local Headroom proxy. ## What changed ### Transparent OpenCode wrapping - `headroom wrap opencode` injects the `headroom-opencode` plugin through `OPENCODE_CONFIG_CONTENT`. - Existing OpenCode provider URLs are preserved. We do not rewrite user config URLs to point at Headroom. - Existing `OPENAI_BASE_URL` and `ANTHROPIC_BASE_URL` env vars are preserved. - Local OpenCode traffic, localhost traffic, and Headroom proxy traffic bypass the shim to avoid loops. ### Runtime transport interception - Added an OpenCode plugin transport shim that wraps: - `globalThis.fetch` - `http.request` / `http.get` - `https.request` / `https.get` - External provider calls are routed to the local Headroom proxy. - The original upstream origin is passed through `x-headroom-base-url`, so the proxy can forward to the real provider without changing OpenCode config. - External `http2.connect` is blocked loudly instead of allowing direct provider traffic to leak outside Headroom. ### Live provider additions Provider coverage is no longer based on a static config scan. Because routing happens at outbound request time, providers added mid-session are routed through Headroom automatically as long as they use the covered Node transport paths. ### Subagent and child-process coverage - The parent OpenCode plugin sets a packaged Node preload shim through `NODE_OPTIONS=--import=.../hook-shim/handler.js`. - The transport shim patches `child_process.spawn`, `exec`, `execFile`, and `fork` so child Node processes receive the Headroom preload even when OpenCode passes a custom `env`. - The child-process shim fails closed if it loads without `HEADROOM_OPENCODE_TRANSPORT_PROXY_URL`. - This closes the subagent leak path where a child Node process could otherwise start without Headroom transport interception. ## Why this goes beyond PR #1089 PR #1089 improves OpenCode provider registration, but it still focuses on provider config shape. This PR moves the enforcement boundary to runtime transport interception. This PR goes further because: - No provider URL rewriting is required. - New providers added mid-session are covered automatically. - Subagents and child Node processes inherit the Headroom transport shim. - Direct external HTTP/2 paths fail loudly instead of leaking. - The wrap remains transparent to the user's OpenCode provider config. - The wrapper is fail-closed for unsupported child-process preload state. ## Additional robustness fixes While validating the change in Docker, the full Python suite exposed unrelated Linux/container robustness issues. These are fixed in this PR so the suite is green: - Binary cache handling now treats cache paths under a non-writable existing parent as unavailable, including when tests run as root in Docker. - `release_version.py` honors `MANUAL_VER` before git calls so direct script execution works outside a `.git` checkout. - Test logger isolation now resets relevant Headroom child loggers so proxy logging setup cannot poison later `caplog` tests. - The scanner missing-path test now uses a guaranteed missing `tmp_path` child instead of relying on `/nonexistent/path`. ## Validation All implementation validation was run inside Docker. - Full Python suite from a fresh Docker copy: `6605 passed, 523 skipped`. - Ruff on changed Python/OpenCode paths: passed. - OpenCode plugin typecheck: passed. - OpenCode plugin tests: `9 passed`. - OpenCode plugin build: passed. - Hook shim preload smoke test: passed. ## Notes This PR intentionally does not add a CLI option. `headroom wrap opencode` means full wrap. Either Headroom wraps OpenCode transparently, or the path fails loudly instead of silently leaking provider traffic. --------- Co-authored-by: Rudimar Ronsoni <6081613+rudironsoni@users.noreply.github.com> |
||
|
|
d9d0bf4b79
|
feat(providers): add Cortex Code (Snowflake CoCo) as a supported agent (#1190)
## Description Adds **Cortex Code (CoCo)** — Snowflake's AI coding CLI — as a first-class headroom provider alongside Claude Code, Codex, and Cursor. Cortex Code routes requests to Snowflake's Cortex inference endpoint via the OpenAI-compatible pipeline. This PR adds the provider slice, registers it under `"cortex-code"`, and ships tests that measure real token savings against `claude-sonnet-4-6`. Closes # ## Type of Change - [x] New feature (non-breaking change that adds functionality) - [x] Documentation update ## Changes Made - `headroom/providers/cortex_code/__init__.py` — new provider package - `headroom/providers/cortex_code/runtime.py` — `proxy_base_url()`, `build_launch_env()`, `default_api_url()` (reads `SNOWFLAKE_HOST` / `SNOWFLAKE_ACCOUNT`) - `headroom/providers/cortex_code/install.py` — `build_install_env()` sets `OPENAI_BASE_URL`; `render_setup_lines()` - `headroom/providers/install_registry.py` — registers `"cortex-code"` in `_ENV_BUILDERS` - `tests/test_provider_cortex_code.py` — 15 unit tests - `tests/test_cortex_code_compression.py` — 5 compression benchmark tests (no API key needed) - `tests/e2e_cortex_savings.py` — real REST API benchmark; reads `SF_CONN`/`SF_HOST` from env, no hardcoded identifiers - `docs/cortex-code.md` — integration guide (quick start, library mode, auth, limitations) - `README.md` — Cortex Code row added to agent compatibility matrix ## 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 $ uv run --with pytest pytest tests/test_provider_cortex_code.py tests/test_cortex_code_compression.py -v tests/test_provider_cortex_code.py::test_cortex_code_proxy_base_url_is_openai_compatible PASSED tests/test_provider_cortex_code.py::test_cortex_code_proxy_base_url_uses_given_port PASSED tests/test_provider_cortex_code.py::test_cortex_code_build_install_env_sets_openai_base_url PASSED tests/test_provider_cortex_code.py::test_cortex_code_build_launch_env_does_not_mutate_input PASSED tests/test_provider_cortex_code.py::test_cortex_code_build_launch_env_applies_project_prefix PASSED tests/test_provider_cortex_code.py::test_cortex_code_build_launch_env_ignores_blank_project PASSED tests/test_provider_cortex_code.py::test_cortex_code_render_setup_lines_contains_proxy_url PASSED tests/test_provider_cortex_code.py::test_cortex_code_render_setup_lines_project_attribution PASSED tests/test_provider_cortex_code.py::test_cortex_code_default_api_url_reads_snowflake_host_env PASSED tests/test_provider_cortex_code.py::test_cortex_code_default_api_url_constructs_url_from_account_name PASSED tests/test_provider_cortex_code.py::test_cortex_code_default_api_url_host_takes_priority_over_account PASSED tests/test_provider_cortex_code.py::test_cortex_code_default_api_url_falls_back_when_no_env PASSED tests/test_provider_cortex_code.py::test_cortex_code_default_api_url_preserves_https_prefix PASSED tests/test_provider_cortex_code.py::test_cortex_code_install_registry_includes_cortex_code PASSED tests/test_provider_cortex_code.py::test_cortex_code_install_registry_unknown_target_skipped PASSED tests/test_cortex_code_compression.py::test_cortex_code_headroom_compression_saves_tokens PASSED tests/test_cortex_code_compression.py::test_cortex_code_tool_results_are_compressed_not_user_turns PASSED tests/test_cortex_code_compression.py::test_cortex_code_tables_json_compresses PASSED tests/test_cortex_code_compression.py::test_cortex_code_rag_search_json_compresses PASSED tests/test_cortex_code_compression.py::test_cortex_code_compression_is_lossless_on_key_content PASSED 20 passed, 1 warning in 1.91s ``` ## Real Behavior Proof - Environment: macOS, Python 3.11, headroom 0.27.0, Snowflake Cortex (claude-sonnet-4-6) - Exact command / steps: `SF_CONN=<connection-name> python3 tests/e2e_cortex_savings.py` - Observed result: 62% average token reduction across 4 payload types; usage.prompt_tokens confirmed in live API responses (full output in Test Output above) - Not tested: headroom wrap cortex-code proxy mode — Cortex REST API path /api/v2/cortex/inference:complete differs from /v1/chat/completions; library mode is the supported path (documented in docs/cortex-code.md Limitations) ```text Tokens saved : 22,077 prompt tokens (4 calls) Avg per call : 5,519 tokens / $0.01656 At 1k/day : $16.56/day | $6,044/year ``` ## 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 Pre-commit hooks skipped locally due to a GPG signing / ruff-format stash conflict in the dev environment. `ruff check` passes clean on all new files. --------- Co-authored-by: Cortex Code <noreply@snowflake.com> |
||
|
|
b5f63d8fa9
|
fix(proxy): allow disabling periodic TOIN stats logging (#1265)
## Description
Add an explicit proxy configuration toggle for the periodic TOIN stats
logging loop.
Long-lived proxy workers currently schedule
`_log_toin_stats_periodically()` unconditionally at startup. This change
lets operators disable only that 5-minute stats logging loop via
`HEADROOM_PERIODIC_TOIN_STATS=0` when periodic stats collection creates
avoidable resource pressure. The default remains enabled, and this does
not disable TOIN learning or request-time feedback.
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
- Added `ProxyConfig.periodic_toin_stats_enabled`, defaulting to `True`.
- Wired `HEADROOM_PERIODIC_TOIN_STATS` through
`_proxy_config_from_env()`.
- Guarded the proxy lifespan startup so `_log_toin_stats_periodically()`
is only scheduled when the config is enabled.
- Added tests for the default env behavior, disabled env values, and the
disabled lifespan behavior.
- Documented `HEADROOM_PERIODIC_TOIN_STATS` in the configuration
reference.
## Testing
- [x] Unit tests pass (`pytest`)
- [ ] Linting passes (`ruff check .`)
- [ ] Type checking passes (`mypy headroom`)
- [x] New tests added for new functionality
- [x] Manual testing performed
### Test Output
```text
$ uv sync --extra dev
Resolved 256 packages in 1m 05s
Built headroom-ai @ file:///C:/Users/wstcz/AppData/Local/Temp/headroom-main-20260622-073524
Installed 124 packages in 50.87s
$ .\.venv\Scripts\python.exe -c "import headroom._core; print('core ok')"
core ok
$ uv run pytest tests/test_proxy_telemetry_env.py -q
============================= test session starts =============================
platform win32 -- Python 3.11.15, pytest-9.0.3, pluggy-1.6.0
rootdir: C:\Users\wstcz\AppData\Local\Temp\headroom-main-20260622-073524
configfile: pyproject.toml
plugins: anyio-4.12.1, langsmith-0.8.0, 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 9 items
tests\test_proxy_telemetry_env.py ......... [100%]
============================== warnings summary ===============================
.venv\Lib\site-packages\fastapi\testclient.py:1
C:\Users\wstcz\AppData\Local\Temp\headroom-main-20260622-073524\.venv\Lib\site-packages\fastapi\testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
from starlette.testclient import TestClient as TestClient # noqa
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
======================== 9 passed, 1 warning in 4.30s =========================
$ python -m py_compile headroom\proxy\models.py headroom\proxy\server.py tests\test_proxy_telemetry_env.py
# no output; command exited 0
$ git diff --check
# no output; command exited 0
```
## Real Behavior Proof
- Environment: Windows, Python 3.11.15 uv-managed `.venv`, source
checkout at `
|
||
|
|
b99869778b
|
fix(telemetry): switch anonymous telemetry to opt-in (off by default) (#1223)
## Description
Anonymous usage telemetry was **on by default** (opt-out). This flips it
to **opt-in**: nothing is collected or shipped unless the user
explicitly turns it on. Small change, but it makes "no data leaves the
proxy by default" the actual default rather than something users have to
discover and disable.
Closes # <!-- N/A: no tracking issue -->
## Type of Change
- [ ] Bug fix (non-breaking change that fixes an issue)
- [x] New feature (non-breaking change that adds functionality) — adds
`--telemetry` opt-in flag
- [x] Breaking change (fix or feature that would cause existing
functionality to change) — telemetry no longer runs unless opted in
- [x] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)
## Changes Made
- `is_telemetry_enabled()` is now **fail-closed**: only explicit
on-values (`on`/`true`/`1`/`yes`/`enable`/`enabled`) enable telemetry;
unset, empty, or unrecognized values stay disabled. This single
predicate gates both the Supabase beacon and the local `/v1/telemetry`
collector.
- Added `--telemetry` opt-in flag to `headroom proxy` and `headroom
install apply`; kept `--no-telemetry` and `HEADROOM_TELEMETRY=off` for
back-compat. If both are passed, opt-out wins.
- Install manifests now write `HEADROOM_TELEMETRY` explicitly
(`on`/`off`) plus the matching flag, so generated systemd/docker/launchd
deployments are unambiguous and don't rely on the runtime default.
- Startup banner and proxy log show `DISABLED` by default and surface
how to opt in.
- Updated tests for opt-in defaults; added coverage for the default-off
banner, the `--telemetry` flag, and the explicit-on manifest path.
- Updated docs
(proxy/configuration/installation/benchmarks/community-savings mdx, spec
011/015, wiki proxy/cli/benchmarks/metrics) and `CHANGELOG.md`.
## Testing
- [x] Unit tests pass (`pytest`) — targeted telemetry + install-planner
suites
- [x] Linting passes (`ruff check`)
- [x] Type checking passes (`mypy`)
- [x] New tests added for new functionality
- [x] Manual testing performed
### Test Output
```text
$ python -m pytest tests/test_telemetry_warning.py tests/test_telemetry.py tests/test_install/test_planner.py -q
81 passed
$ ruff check <changed source + test files>
All checks passed!
$ mypy headroom/telemetry/beacon.py headroom/cli/proxy.py headroom/cli/install.py \
headroom/install/planner.py headroom/proxy/server.py
Success: no issues found in 5 source files
```
## Real Behavior Proof
- **Environment:** macOS (darwin 25.4.0), project `.venv`, `headroom`
CLI.
- **Exact command / steps:**
```
$ python -c "import os; from headroom.telemetry.beacon import
is_telemetry_enabled; \
os.environ.pop('HEADROOM_TELEMETRY', None); print('unset ->',
is_telemetry_enabled()); \
[ (os.environ.__setitem__('HEADROOM_TELEMETRY', v), print(repr(v), '->',
is_telemetry_enabled())) \
for v in ['on','TRUE','1','yes','off','0','garbage',''] ]"
$ headroom proxy --help | grep -i telemetry
$ headroom install apply --help | grep -i telemetry
```
- **Observed result:**
```
unset -> False # off by default
'on' -> True 'TRUE' -> True '1' -> True 'yes' -> True
'off' -> False '0' -> False
'garbage' -> False '' -> False # fail-closed
proxy: --telemetry Opt in to anonymous usage telemetry — off by default
(env: HEADROOM_TELEMETRY=on)
--no-telemetry Force anonymous usage telemetry off (already the default;
env: HEADROOM_TELEMETRY=off)
install: --telemetry Opt in to anonymous telemetry in the runtime (off
by default).
--no-telemetry Force anonymous telemetry off in the runtime (already the
default).
```
- **Not tested:** live Supabase beacon network round-trip (no opt-in
network call was made); full `pytest` suite was not run — only the
telemetry + install-planner targeted suites.
## Review Readiness
- [x] I have performed a self-review
- [x] This PR is ready for human review
## Checklist
- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] I have updated the CHANGELOG.md if applicable
## Additional Notes
- "Breaking change" is checked because the default behavior changes
(telemetry stops running unless opted in). It is **not** an API break —
`--no-telemetry` and `HEADROOM_TELEMETRY=off` still work, so existing
opt-out configs are unaffected.
- No tracking issue, so `Closes #` is left N/A.
|
||
|
|
f4bd2fe68f
|
docs(vertex): Claude Code + Vertex via Headroom guide (validated) (#1180)
## Description Documents the **validated** way to run **Claude Code** against **Claude models on Google Vertex AI** with **Headroom compressing the context**. Corrects the prior review's assumption that the "Vertex-mode redirect" approach would work — Claude Code's client-side `probeVertexModel` blocks it — and documents the working **Anthropic-mode + LiteLLM `vertex_ai`** path, verified end-to-end against live Vertex quota (~22% context compression observed). Closes # <!-- n/a --> ## Type of Change - [ ] Bug fix (non-breaking change that fixes an issue) - [ ] New feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [x] Documentation update - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - **`docs/claude-code-vertex-headroom.md`** (new) — copy-paste runbook: prerequisites (GCP ADC, `google-cloud-aiplatform`, Vertex quota), two-terminal setup (proxy `--backend litellm-vertex_ai --region <loc> --code-aware`; Claude Code in normal Anthropic mode via `ANTHROPIC_BASE_URL`), verification, a troubleshooting table, and a section on what `--code-aware` does and what it never touches (local files / protected `Read`/`Glob`/`Grep`/`Write`/`Edit` output). - **`wiki/vertex.md`** — new "Claude Code with Headroom compression" section pointing at the runbook, with the two ⚠️ caveats (Vertex-mode probe rejects custom URLs; `vertexai` dep + `--code-aware` required). - **`docs/proposals/vertex-claude-compression-review.md`** — corrected TL;DR: Setup A is blocked by Claude Code's probe; Setup B is the validated path. ## Testing - [ ] Unit tests pass (`pytest`) — **N/A (docs-only, no code changed)** - [ ] Linting passes (`ruff check .`) — **N/A (no Python changed)** - [ ] Type checking passes (`mypy headroom`) — **N/A (no Python changed)** - [ ] New tests added for new functionality — **N/A (docs)** - [x] Manual testing performed (live Vertex validation — see below) ### Test Output ```text # 1) Direct Vertex quota check (global) POST .../locations/global/publishers/anthropic/models/claude-sonnet-4-6:rawPredict -> HTTP 200 {"content":[{"text":"VERTEX OK"}], "model":"claude-sonnet-4-6"} # 2) Headroom in Anthropic mode -> LiteLLM(vertex_ai) -> Vertex global POST http://127.0.0.1:8787/v1/messages (model=claude-sonnet-4-6) -> HTTP 200 {"content":[{"text":"LITELLM VERTEX OK"}], "model":"claude-sonnet-4-6"} # 3) Real Claude Code session (normal mode) through Headroom, --code-aware ON claude -p "...run two Bash source dumps + summarize..." (ANTHROPIC_BASE_URL=proxy) -> is_error: False, modelUsage: ['claude-sonnet-4-6'] request_log: orig=9353 saved=2029 (21.7%) transforms=['router:tool_result:mixed'] # 4) Compressors loaded (GET /debug/warmup) {'kompress':'loaded', 'code_aware':'loaded', 'tree_sitter':'loaded', 'smart_crusher':'loaded'} ``` ## Real Behavior Proof - **Environment:** macOS (arm64); Claude Code 2.1.181; Headroom 0.27.0; venv Python 3.12; LiteLLM `vertex_ai` via `google-cloud-aiplatform` 1.158.0; GCP project `eternal-sunset-495505-t0`; Vertex location `global`; model `claude-sonnet-4-6` (only model with quota on this project); auth via `gcloud auth application-default login` (ADC). - **Exact command / steps:** the two-terminal setup in `docs/claude-code-vertex-headroom.md` — proxy `headroom proxy --port 8787 --backend litellm-vertex_ai --region global --code-aware`; client `ANTHROPIC_BASE_URL=http://127.0.0.1:8787` + `ANTHROPIC_MODEL=claude-sonnet-4-6` in normal mode (no `CLAUDE_CODE_USE_VERTEX`). - **Observed result:** Claude Code answered via Vertex (`modelUsage: claude-sonnet-4-6`); ~22% context compression (`router:tool_result:mixed`) on a code-heavy request forwarded to Vertex `global`; all compressors loaded. - **Not tested:** cumulative savings over long multi-turn sessions; non-global regions (no quota on this project); Opus 4.8 (not enabled in this project — 404); automated tests for the LiteLLM-vertex path (still absent — pre-existing gap). ## 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 (docs) - [x] I have performed a self-review of my code - [x] I have commented my code, particularly in hard-to-understand areas (N/A — docs) - [x] I have made corresponding changes to the documentation (this *is* the documentation) - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works — **N/A (docs-only)** - [ ] New and existing unit tests pass locally with my changes — **N/A (no code changed)** - [ ] I have updated the CHANGELOG.md if applicable — **N/A (docs-only)** ## Additional Notes - **Docs-only PR** — no Python changed, so `ruff` / `mypy` / `pytest` are N/A. - **Base:** branched from latest `origin/main`; clean 3-file diff (the prerequisite review doc and Vertex wiki content are already on `main`). - **Follow-ups:** optional `headroom wrap claude` Vertex turnkey; add automated tests for the LiteLLM-vertex path; consider defaulting `--code-aware` (or warning when code content is detected but code-aware is off), since its default-off state makes compression silently no-op on coding sessions. |
||
|
|
6904d47a01
|
feat(proxy): hot-reload live env knobs so a reused proxy picks them up without a restart (#1090)
## Description A small class of env vars is read by the proxy **live, per request** — the output-shaper family (`HEADROOM_OUTPUT_SHAPER`, `HEADROOM_VERBOSITY_LEVEL`, `HEADROOM_EFFORT_ROUTER`, `HEADROOM_MECHANICAL_EFFORT`, `HEADROOM_VERBOSITY_AUTOTUNE`, `HEADROOM_OUTPUT_HOLDOUT`), or captured at import (`HEADROOM_INTERCEPT_READ_MIN_CHARS`). The proxy reads them from its own process environment, fixed at launch. But `headroom wrap` reuses an already-running proxy (it restarts only on startup-config drift), so a value exported *after* the proxy started silently no-op'd — e.g. `export HEADROOM_OUTPUT_SHAPER=1` had zero effect on a reused proxy on `:8787`. This PR makes those live knobs **hot-reloadable**: `headroom wrap` pushes them to the running proxy, which applies them in memory — no restart (a restart would cold-start the ML stack, drop in-flight requests, and lose CCR/router caches). _No linked issue._ ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) - [x] New feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [x] Documentation update - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - `headroom/proxy/runtime_env.py` (new): single source of truth registering the live knobs + a thread-safe process-global override store. `getenv()` (override-then-env) is a drop-in for `os.environ.get`; behaviour is byte-identical when no override is set. - Readers rerouted through `runtime_env.getenv`: `output_shaper.py`, the anthropic holdout read, and the ast-grep threshold (now a live read, not an import-time constant). - Proxy: loopback-only `POST /admin/runtime-env` applies overrides in memory; `/health` → `config.runtime_env` surfaces the live values so reuse is observable. - `wrap`: after attaching to a proxy (all call sites), best-effort push of the session's **explicitly-set** knobs. No-ops if nothing is set, `--no-proxy`, the proxy is unreachable, or it predates the endpoint (404). Only explicitly-set knobs are pushed, so a session never clobbers another with a default it never asked for. - Docs: README + output-token-reduction guide document the global-override caveat. ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [x] Type checking passes (`mypy headroom`) - [x] New tests added for new functionality - [ ] Manual testing performed ### Test Output ```text $ python -m pytest tests/test_runtime_env.py -q 16 passed $ python -m pytest tests/test_runtime_env.py tests/test_output_shaper.py -q 50 passed $ ruff check headroom/proxy/runtime_env.py headroom/proxy/output_shaper.py headroom/proxy/handlers/anthropic.py headroom/proxy/interceptors/astgrep.py headroom/proxy/server.py headroom/cli/wrap.py All checks passed! $ mypy headroom/proxy/runtime_env.py Success: no issues found in 1 source file ``` ## Real Behavior Proof - Environment: local macOS, Python 3.12 `.venv`, branch `fix/runtime-env-hot-reload` at the PR head. - Exact command / steps: ran the test suites above. The 16 new `test_runtime_env` tests exercise the registry/store, overrides reaching the shaper + the ast-grep threshold, the `POST /admin/runtime-env` apply + `/health` reflect + loopback-only 404 + 400-on-non-object, and the wrap push payload / no-op / error-swallow paths. - Observed result: 50 passed; ruff + mypy clean on the changed modules; an override set via the endpoint is read by `getenv()` at the shaper and surfaced in `/health` config. - Not tested: a literal two-terminal manual session (start a proxy, `headroom wrap` a second session, `export HEADROOM_OUTPUT_SHAPER=1`, confirm the reused proxy picks it up). The behaviour is covered by the endpoint + wrap-push integration tests, but was not exercised by hand here. ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Checklist - [x] My code follows the project's style guidelines - [x] I have performed a self-review of my code - [x] I have commented my code, particularly in hard-to-understand areas - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] I have updated the CHANGELOG.md if applicable ## Additional Notes - **Inherent caveat (documented):** overrides are global to the proxy — one process serves every attached wrapper, so the last explicit setting wins. No mechanism (restart or hot-reload) can give two sessions on one shared proxy different output-shaper settings. - **Scope:** startup-captured settings (`HEADROOM_TARGET_RATIO` etc.) are intentionally out of scope — a fresh proxy already gets them and they ride the existing `/health` config channel. - **Merge blocker:** this branch is currently **CONFLICTING with `main`** and needs a rebase/merge before it can land. - CHANGELOG.md left unchanged — releases are managed by release-please from conventional commits. |
||
|
|
0e0591506c
|
feat(vertex): turnkey Claude Code + Vertex compression (+ fixes from the Vertex review) (#1113)
## Description
Makes **Claude Code on Google Vertex AI** actually receive Headroom's
prompt compression, and fixes the issues found in a deep review of the
Vertex path. The headline is a turnkey path: `headroom wrap claude`
(with the user's existing Vertex env) compresses each request and
forwards to Vertex using the client's own GCP ADC token — Headroom holds
no credentials.
_No linked issue — this addresses the internal Vertex code review
(`docs/proposals/vertex-claude-compression-review.md`)._
## Type of Change
- [x] Bug fix (non-breaking change that fixes an issue)
- [x] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [x] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)
## Changes Made
- `cli/wrap.py`: `wrap claude` detects `CLAUDE_CODE_USE_VERTEX=1` and
points Claude Code's Vertex endpoint at the proxy via
`ANTHROPIC_VERTEX_BASE_URL` (Claude Code ignores `ANTHROPIC_BASE_URL` in
Vertex mode). Client keeps its own GCP ADC auth. Adds
`--backend`/`--region` flags (parity with `wrap aider`).
- `providers/registry.py`: alias `litellm-vertex` → provider
`vertex_ai`. Previously it resolved to `"vertex"` (not in the registry)
→ generic pass-through with the wrong model prefix, dropped region, and
mishandled auth, even though all help text advertises `litellm-vertex`.
- `providers/proxy_routes.py`: derive the Vertex upstream host
per-request from the path's `locations/{location}` (handles `global`)
instead of pinning the configured fixed-region host; explicit
`--vertex-api-url` overrides still win.
- `docs/content/docs/claude-code-vertex.mdx` (+ nav): simple user guide
for running Claude Code on Vertex through Headroom.
- `docs/proposals/vertex-claude-compression-review.md`: the deep-review
findings these fixes address.
- `tests/test_vertex_claude_compression.py`: new tests.
## Testing
- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [x] Type checking passes (`mypy headroom`)
- [x] New tests added for new functionality
- [ ] Manual testing performed
### Test Output
```text
$ python -m pytest tests/test_vertex_claude_compression.py -q
8 passed
$ python -m pytest tests/test_provider_proxy_routes.py tests/test_provider_registry.py tests/test_cli_proxy_env.py tests/test_backend_bugs.py -q
108 passed
$ ruff check headroom/providers/registry.py headroom/providers/proxy_routes.py headroom/cli/wrap.py tests/test_vertex_claude_compression.py
All checks passed!
$ mypy headroom/providers/registry.py headroom/providers/proxy_routes.py headroom/cli/wrap.py
Success: no issues found in 3 source files
```
## Real Behavior Proof
- Environment: local macOS, Python 3.12 `.venv`, branch
`feat/vertex-claude-compression`.
- Exact command / steps: ran the test suite above; verified in code that
the native `:rawPredict` route (publisher=anthropic) delegates to
`handle_anthropic_messages` with the region-derived host, that
`create_proxy_backend("litellm-vertex")` resolves to provider
`vertex_ai`, and that `wrap claude` sets `ANTHROPIC_VERTEX_BASE_URL`
when `CLAUDE_CODE_USE_VERTEX` is set.
- Observed result: 8 new tests + 108 existing tests pass; ruff + mypy
clean; the alias, region derivation (incl. `global` and explicit
override), and rawPredict→compression-handler delegation all behave as
asserted.
- Not tested: a live end-to-end run of Claude Code against a real Google
Vertex project (no GCP credentials available in this environment).
Recommend one smoke test against a live Vertex project before announcing
GA. The Rust `headroom-proxy` Vertex path is intentionally out of scope
(separate, unwired binary).
## 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.md left unchanged — releases are managed by release-please
from conventional commits.
- Follow-ups (not in this PR): wire or formally retire the Rust
`headroom-proxy` Vertex implementation; add a live-Vertex smoke test
once CI has GCP credentials.
|
||
|
|
75f81cd19f
|
ci: bump the npm_and_yarn group across 3 directories with 3 updates (#1056)
[//]: # (dependabot-start) ⚠️ **Dependabot is rebasing this PR** ⚠️ Rebasing might not happen immediately, so don't worry if this takes some time. Note: if you make any changes to this PR yourself, they will take precedence over the rebase. --- [//]: # (dependabot-end) Bumps the npm_and_yarn group with 1 update in the /docs directory: [js-yaml](https://github.com/nodeca/js-yaml). Bumps the npm_and_yarn group with 1 update in the /plugins/openclaw directory: [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite). Bumps the npm_and_yarn group with 2 updates in the /sdk/typescript directory: [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) and [form-data](https://github.com/form-data/form-data). Updates `js-yaml` from 4.1.1 to 4.2.0 <details> <summary>Changelog</summary> <p><em>Sourced from <a href="https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md">js-yaml's changelog</a>.</em></p> <blockquote> <h2>[4.2.0] - 2026-06-01</h2> <h3>Added</h3> <ul> <li>Added <code>docs/safety.md</code> with notes about processing untrusted YAML.</li> <li>Added <code>maxDepth</code> (100) loader option. Not a problem, but gives a better exception instead of RangeError on stack overflow.</li> <li>Added <code>maxMergeSeqLength</code> (20) loader option. Not a problem after <code>merge</code> fix, but an additional restriction for safety.</li> <li>Added sourcemaps to <code>dist/</code> builds.</li> </ul> <h3>Changed</h3> <ul> <li>Stop resolving numbers with underscores as numeric scalars, <a href="https://redirect.github.com/nodeca/js-yaml/issues/627">#627</a>.</li> <li>Switched dev toolchains to Vite / neostandard.</li> <li>Updated demo.</li> <li>Reorganized tests.</li> <li><code>dist/</code> files are no longer kept in the repository.</li> </ul> <h3>Fixed</h3> <ul> <li>Fix parsing of properties on the first implicit block mapping key, <a href="https://redirect.github.com/nodeca/js-yaml/issues/62">#62</a>.</li> <li>Fix trailing whitespace handling when folding flow scalar lines, <a href="https://redirect.github.com/nodeca/js-yaml/issues/307">#307</a>.</li> <li>Reject top-level block scalars without content indentation, <a href="https://redirect.github.com/nodeca/js-yaml/issues/280">#280</a>.</li> <li>Ensure numbers survive round-trip, <a href="https://redirect.github.com/nodeca/js-yaml/issues/737">#737</a>.</li> <li>Fix test coverage for issue <a href="https://redirect.github.com/nodeca/js-yaml/issues/221">#221</a>.</li> <li>Fix flow scalar trailing whitespace folding, <a href="https://redirect.github.com/nodeca/js-yaml/issues/307">#307</a>.</li> <li>Fix digits in YAML named tag handles.</li> </ul> <h3>Security</h3> <ul> <li>Fix potential DoS via quadratic complexity in merge - deduplicate repeated elements (makes sense for malformed files > 10K).</li> </ul> <h2>[3.14.2] - 2025-11-15</h2> <h3>Security</h3> <ul> <li>Backported v4.1.1 fix to v3</li> </ul> </blockquote> </details> <details> <summary>Commits</summary> <ul> <li>See full diff in <a href="https://github.com/nodeca/js-yaml/commits">compare view</a></li> </ul> </details> <br /> Updates `vite` from 8.0.10 to 8.0.16 <details> <summary>Release notes</summary> <p><em>Sourced from <a href="https://github.com/vitejs/vite/releases">vite's releases</a>.</em></p> <blockquote> <h2>v8.0.16</h2> <p>Please refer to <a href="https://github.com/vitejs/vite/blob/v8.0.16/packages/vite/CHANGELOG.md">CHANGELOG.md</a> for details.</p> <h2>v8.0.15</h2> <p>Please refer to <a href="https://github.com/vitejs/vite/blob/v8.0.15/packages/vite/CHANGELOG.md">CHANGELOG.md</a> for details.</p> <h2>v8.0.14</h2> <p>Please refer to <a href="https://github.com/vitejs/vite/blob/v8.0.14/packages/vite/CHANGELOG.md">CHANGELOG.md</a> for details.</p> <h2>v8.0.13</h2> <p>Please refer to <a href="https://github.com/vitejs/vite/blob/v8.0.13/packages/vite/CHANGELOG.md">CHANGELOG.md</a> for details.</p> <h2>v8.0.12</h2> <p>Please refer to <a href="https://github.com/vitejs/vite/blob/v8.0.12/packages/vite/CHANGELOG.md">CHANGELOG.md</a> for details.</p> <h2>v8.0.11</h2> <p>Please refer to <a href="https://github.com/vitejs/vite/blob/v8.0.11/packages/vite/CHANGELOG.md">CHANGELOG.md</a> for details.</p> </blockquote> </details> <details> <summary>Changelog</summary> <p><em>Sourced from <a href="https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md">vite's changelog</a>.</em></p> <blockquote> <h2><!-- raw HTML omitted --><a href="https://github.com/vitejs/vite/compare/v8.0.15...v8.0.16">8.0.16</a> (2026-06-01)<!-- raw HTML omitted --></h2> <h3>Bug Fixes</h3> <ul> <li><strong>deps:</strong> reject UNC paths for launch-editor-middleware (<a href="https://redirect.github.com/vitejs/vite/issues/22571">#22571</a>) (<a href=" |
||
|
|
1e437d781b
|
docs: document HEADROOM_BETA_HEADER_STICKY and HEADROOM_BETA_TRACKER_MAX_SESSIONS (#1060)
## Description Documents `HEADROOM_BETA_HEADER_STICKY` and `HEADROOM_BETA_TRACKER_MAX_SESSIONS` — two env vars that exist in source and tests but are absent from all `.md` / `.mdx` docs. Adds a **Session Beta Header Tracking** section explaining the `SessionBetaTracker` behavior, its prefix-cache rationale, and the operator trade-off. An operator debugging a beta-header-related upstream rejection cannot discover the knob or the off-switch without reading source. Closes #1059 ## Type of Change - [ ] Bug fix (non-breaking change that fixes an issue) - [ ] New feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [x] Documentation update - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made **`wiki/configuration.md`** - Two new rows in the Environment Variables table: `HEADROOM_BETA_HEADER_STICKY` and `HEADROOM_BETA_TRACKER_MAX_SESSIONS` - New `## Session Beta Header Tracking` section with: what the mechanism does, why it exists (prefix-cache stability), the operator trade-off, and how to disable **`docs/content/docs/configuration.mdx`** - Same two rows added to the Environment Variables table - Same `### Session Beta Header Tracking` section (matching heading level) ## Testing - [ ] Unit tests pass (`pytest`) - [ ] Linting passes (`ruff check .` — N/A, docs-only change to `.md`/`.mdx` files) - [ ] Type checking passes (`mypy headroom` — N/A, docs-only) - [ ] New tests added for new functionality — N/A, docs-only - [x] Manual testing performed ### Test Output ```text Manual verification: confirmed env var names, accepted values, defaults, and LRU bound match code (helpers.py:1610-1629, 1613-1614, 1820-1822). Existing tests exercise the behavior (tests/test_anthropic_beta_session_sticky.py:124). ``` ## Real Behavior Proof - Environment: Ubuntu 24.04, Python 3.12, Headroom v0.25.0, provider: Anthropic (Claude Code via `headroom wrap`) - Exact command / steps: `grep -ri "HEADROOM_BETA_HEADER_STICKY" README.md CHANGELOG.md wiki/ docs/` → 0 matches before; source inspection of helpers.py:1605-1856, anthropic.py:918-961, prefix_tracker.py:316-335 - Observed result: env vars now documented in both wiki + docs mirrors; behavior rationale and trade-off explained - Not tested: end-to-end proxy run with the new docs in place (docs-only change; behavior unchanged) ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Checklist - [x] My code follows the project's style guidelines - [x] I have performed a self-review of my code - [x] I have commented my code, particularly in hard-to-understand areas - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works — N/A, docs-only - [ ] New and existing unit tests pass locally with my changes — N/A, docs-only - [ ] I have updated the CHANGELOG.md if applicable — N/A, changelog is auto-managed by release-please ## Screenshots (if applicable) N/A ## Additional Notes Mirrors the same dual-file pattern used in #579 (wiki + docs mdx). The documented behavior is contractual — confirmed by tests/test_anthropic_beta_session_sticky.py:124 (test_beta_seen_turn_1_present_in_turn_2_even_if_client_drops) and the test module docstring naming Claude Code and Codex CLI as the targeted clients. |
||
|
|
8cea290a15
|
docs(ci): add CI/CD flow diagrams (#1062)
## Description
Adds a visual CI/CD flow reference for Headroom so contributors can
quickly understand the gated PR, release, Docker, docs deploy, fork
approval, and manual validation paths. Also updates the release
documentation to match the current release-please release flow instead
of the stale push-to-main release model.
## Type of Change
- [ ] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [x] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)
## Changes Made
- Added `docs/content/docs/ci-cd-flows.mdx` with Mermaid diagrams and
decision trees for PR review, release publishing, Docker publishing,
docs deploys, fork workflow approval, and manual validation.
- Updated `docs/content/docs/releases.mdx` to describe the current
`release-please` -> GitHub Release -> `release.yml` publishing path.
- Added the CI/CD flow page and existing release page to docs
navigation.
- Added Mermaid to the docs code highlighter language list.
- Restored missing docs helper modules and aligned Fumadocs dependencies
so the docs app can install, generate sources, type-check, and build.
## Testing
- [ ] Unit tests pass (`pytest`)
- [ ] Linting passes (`ruff check .`)
- [ ] Type checking passes (`mypy headroom`)
- [ ] New tests added for new functionality
- [x] Manual testing performed
### Test Output
```text
$ npm ci
> headroom-docs@0.0.0 postinstall
> fumadocs-mdx
[MDX] generated files
added 365 packages, and audited 367 packages
$ npm run types:check
> fumadocs-mdx && next typegen && tsc --noEmit
[MDX] generated files
Generating route types...
✓ Types generated successfully
$ npm run build
> next build
✓ Compiled successfully
Running TypeScript ...
Generating static pages ...
✓ Generating static pages (122/122)
Note: next build completed successfully and emitted two existing Recharts container-size warnings during static generation.
$ git diff --check
# no output
$ act workflow_dispatch -W .github/workflows/docs.yml -n
*DRYRUN* [Deploy Documentation/deploy] 🏁 Job succeeded
```
## Real Behavior Proof
- Environment: Windows local checkout, branch `docs-ci-flow`, Node.js
v22.22.0, `act` 0.2.87.
- Exact command / steps: Ran `npm ci`, `npm run types:check`, and `npm
run build` from `docs/`; ran `git diff --check` and `act
workflow_dispatch -W .github/workflows/docs.yml -n` from the repository
root.
- Observed result: Docs dependencies install, Fumadocs source generation
includes `ci-cd-flows.mdx`, TypeScript passes, Next production build
completes, whitespace check passes, and the docs workflow dry-run
succeeds under `act`.
- Not tested: Full live GitHub Pages deploy and registry/release
publishing, because this PR only changes docs and docs build wiring.
## Review Readiness
- [x] I have performed a self-review
- [x] This PR is ready for human review
## Checklist
- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] I have updated the CHANGELOG.md if applicable
## Screenshots (if applicable)
N/A. This is documentation and build wiring; the diagrams are Mermaid
source blocks in the docs page.
## Additional Notes
- No issue is linked because this PR was not opened for a specific
tracked issue.
- `npm run build` still reports two pre-existing Recharts container-size
warnings while completing successfully.
- Python unit/lint/type checks and changelog updates are not applicable
to this docs-only change.
|
||
|
|
a99dc61424
|
feat: output-token reduction — verbosity shaper, per-user learning, counterfactual savings (#965)
## Description
Adds the first levers that reduce the tokens the model **writes back**
(output), complementing Headroom's existing input compression. Output
costs 5× input on Opus-class models and is full of waste (ceremony,
restated code, deep "thinking" on routine steps). Two phases in one
self-contained PR off `main`: the request-side output shaper, then
per-user verbosity learning plus an honest counterfactual savings
estimator and dashboard surfacing.
## Type of Change
- [ ] Bug fix (non-breaking change that fixes an issue)
- [x] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [x] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)
## Changes Made
- **Output shaper** (`output_shaper.py`, opt-in
`HEADROOM_OUTPUT_SHAPER=1`): cache-safe verbosity steering appended to
the system-prompt tail (5 levels); effort routing that lowers
`output_config.effort` on mechanical tool-result continuations; legacy
`thinking.budget_tokens` clamp. Never injects effort where absent, never
toggles `thinking.type`.
- **`headroom learn --verbosity`**: mines Claude Code transcripts for
behavioral signals (interrupts, length-adaptive fast-skips, echo ratio),
recommends a verbosity level (heuristic + optional `--llm-judge`), and
seeds the savings baseline.
- **Counterfactual estimator** (`output_savings.py`): per-stratum
synthetic-control (estimated) + A/B holdout (measured) with a propagated
95% CI; conversation-stable arm assignment for A/B validity and
prefix-cache safety.
- **AIMD verbosity controller** (`verbosity_controller.py`):
additive-increase / fast-back-off state machine; live signal emission
gated off by default.
- **Wiring + surfaces**: shaper resolves the learned level; recording
rides the existing `transforms_applied` channel through the outcome
funnel (no `RequestOutcome` changes); `headroom output-savings` CLI;
dashboard "Output Tokens Saved" card.
- **Docs**: simple-words user guide + design doc with the counterfactual
methodology.
## Testing
- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [x] Type checking passes (`mypy headroom`)
- [x] New tests added for new functionality
- [x] Manual testing performed
### Test Output
```text
$ pytest tests/test_output_savings.py tests/test_output_savings_cli.py \
tests/test_verbosity_learn.py tests/test_verbosity_controller.py \
tests/test_output_shaper.py -q
94 passed in 0.54s
$ pytest tests/test_request_outcome.py tests/test_handler_outcome_tag_invariant.py \
tests/test_proxy_dashboard_stats_cache.py -q
44 passed
$ ruff format --check .
831 files already formatted
$ mypy headroom --ignore-missing-imports
Success: no issues found in 361 source files
```
## Real Behavior Proof
- Environment: macOS, Python 3.12 (`.venv`), `anthropic` 0.76, live API
model `claude-opus-4-8`.
- Exact command / steps: `HEADROOM_OUTPUT_SHAPER=1`; `headroom learn
--verbosity --apply` (seeds level + baseline); `python
scripts/eval_output_shaper.py A` (live before/after); simulate holdout
traffic then `headroom output-savings`.
- Observed result: code-review ask — baseline 1,750 output tokens → L2
1,354 (−22.7%) → L3 599 (−65.8%), same bugs found. `learn --verbosity`
on 24 real sessions → 11% interrupt / 26% fast-skip → L3 (high
confidence). Measured A/B path → 31.7% reduction (95% CI 27.7%–35.7%).
94 new tests + 44 existing outcome/dashboard tests green; ruff + mypy
clean.
- Not tested: live streaming-path recording exercised only via unit
tests (the `transforms_applied` funnel is shared across paths); runtime
AIMD signal emission is gated off by default and not exercised live.
## Review Readiness
- [x] I have performed a self-review
- [x] This PR is ready for human review
## Checklist
- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] I have updated the CHANGELOG.md if applicable
## Additional Notes
Output savings are counterfactual (we never observe what the model
*would* have written), so the estimator separates **estimated** (vs a
learned baseline) from **measured** (A/B holdout via
`HEADROOM_OUTPUT_HOLDOUT`) and always reports a confidence band — never
a single made-up number. CHANGELOG left unchecked (release-please
manages it). Runtime AIMD self-tuning is intentionally a TODO
(controller built/tested; live signal emission gated behind
`HEADROOM_VERBOSITY_AUTOTUNE`).
|
||
|
|
7edb27ab24
|
feat(proxy): compress AWS Bedrock InvokeModel requests via configurable upstream (#720)
## Description
Clients that speak **Bedrock to a local gateway** can't get proxy-level
compression. Claude Code launched with `CLAUDE_CODE_USE_BEDROCK=1` (and
any AWS SDK pointed at a custom endpoint) POSTs
`/model/{id}/invoke[-with-response-stream]` to
`AWS_ENDPOINT_URL_BEDROCK_RUNTIME`, never `/v1/messages`. Those requests
fell through the catch-all and were forwarded **verbatim — no
compression**.
`--backend bedrock` is the opposite direction: it accepts Anthropic
input and re-signs to AWS. It can't accept Bedrock-format input or
forward to a custom upstream. So the "client speaks Bedrock → local
re-signing gateway → AWS" topology (internal gateways, LiteLLM,
LocalStack; see #510) got nothing.
This adds a Bedrock InvokeModel passthrough that compresses the request
body with the **same** `anthropic_pipeline` used for `/v1/messages` —
the Bedrock InvokeModel body for Anthropic models *is* the Anthropic
Messages shape (`{anthropic_version, system, messages, max_tokens, …}`,
model in the URL), so there's no translation and no new compression
logic. The routes register **only** when `--bedrock-api-url` is set, so
default behavior is completely unchanged.
**Limitation (important):** rewriting the body invalidates the caller's
**SigV4** signature (it covers a hash of the body). Point
`--bedrock-api-url` at a gateway that re-signs or doesn't verify the
inbound signature (an internal gateway, LiteLLM, LocalStack, a corporate
Bedrock proxy) — **never raw AWS**, which would 403. For direct-to-AWS
compression, use `--backend bedrock` (which re-signs). The two are
complementary. This is documented in the flag help, the handler
docstring, the proxy docs, and the CHANGELOG.
Closes #734. Refs #510 (the Bedrock slice of the provider-agnostic
umbrella).
## Type of Change
- [ ] Bug fix (non-breaking change that fixes an issue)
- [x] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)
## Changes Made
- New `--bedrock-api-url` flag (env: `BEDROCK_TARGET_API_URL`). When
set, registers `POST /model/{id}/invoke` and `POST
/model/{id}/invoke-with-response-stream`.
- `BedrockHandlerMixin` compresses the request body via the existing
`anthropic_pipeline`, then forwards to the configured upstream,
preserving path/query.
- Responses forwarded byte-faithfully (non-streaming JSON and the
streaming AWS event-stream alike — neither is parsed or mutated, since
all compression is request-side).
- `{model_id:path}` captures inference-profile ids with
dots/colons/slashes (e.g.
`us.anthropic.claude-sonnet-4-5-20250929-v1:0`).
- Fail-open: a malformed body or compression error forwards verbatim
rather than erroring.
- Routes register only when the flag is set — default behavior
unchanged.
- Files: `headroom/proxy/handlers/bedrock.py` (new —
`BedrockHandlerMixin`); `headroom/providers/proxy_routes.py` (gated
route registration); `headroom/cli/proxy.py`,
`headroom/proxy/server.py`, `headroom/proxy/models.py` (flag + config
wiring); `docs/content/docs/proxy.mdx`, `CHANGELOG.md` (docs).
## Testing
- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [ ] Type checking passes (`mypy headroom`)
- [x] New tests added for new functionality
- [x] Manual testing performed
### Test Output
```text
$ pytest tests/test_proxy/test_bedrock_passthrough.py -q
.............. [100%]
14 passed in 12.46s
```
`tests/test_proxy/test_bedrock_passthrough.py` (14 tests) covers: route
gating (absent unless configured), body compression, non-message fields
preserved, inference-profile id capture + re-encoding, byte-faithful
streaming, fail-open on malformed body and on pipeline exceptions,
bypass when `optimize=False` and via the `x-headroom-bypass` header,
upstream connect failure surfacing as a 502, the content-length
regression, outcome recorded with `provider="bedrock"`, and
`BEDROCK_TARGET_API_URL` env wiring. `ruff check`/`format` clean.
## Real Behavior Proof
- Environment: macOS, Python 3.12; forked proxy on `:8788` with
`--bedrock-api-url` pointed at a local re-signing Bedrock gateway;
provider Anthropic Claude on Bedrock.
- Exact command / steps: `headroom proxy --port 8788 --bedrock-api-url
http://127.0.0.1:<gateway>`, then `curl -X POST
http://127.0.0.1:8788/model/claude-haiku-4-5/invoke --data
@bedrock_invoke.json` (a ~52k-token conversation with a large assistant
turn).
- Observed result: valid Claude response returned and the gateway
received the compressed body — proxy `/stats` reports `52,095 → 3,979
tokens` (92.4%, 48,116 removed), and the gateway's reported
`input_tokens: 3709` confirms the compressed body reached the model.
- Not tested: raw direct-to-AWS (out of scope by design — SigV4; use
`--backend bedrock`); non-Anthropic Bedrock model bodies (e.g.
Titan/Llama) — only the Anthropic Messages-shaped invoke body is
handled.
<details><summary>Proxy <code>/stats</code> output + content-length bug
note</summary>
```json
"compression": {
"requests_compressed": 1,
"avg_compression_pct": 92.4,
"best_detail": "52,095 → 3,979 tokens",
"total_tokens_removed": 48116
}
```
The first iteration of this proof surfaced a real bug — a shrunk body
still carried the inbound `Content-Length`, so httpx raised `Too little
data for declared Content-Length`. Fixed by dropping
`content-length`/`content-encoding` on the rewritten path so httpx
recomputes them; covered by a regression test.
</details>
## Review Readiness
- [x] I have performed a self-review
- [x] This PR is ready for human review
## Checklist
- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] I have updated the CHANGELOG.md if applicable
## Additional Notes
`mypy headroom` is left unchecked above — type checking runs in the CI
matrix rather than locally on my side; the new code carries type hints
on all public functions. Design spec / feature request: #734.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
0dc2e1cb3f
|
feat(bedrock): cross-region + Converse compression; bundle proxy binary in images (#999)
## Description The native Bedrock path (Phase D) compresses + signs Anthropic-on-Bedrock requests, but two real-world cases slipped through, and the native binary that powers it was never shipped. This PR closes those gaps as a focused set of give-backs. Aligns with the Rust migration plan (see below). ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) - [x] New feature (non-breaking change that adds functionality) - [ ] Breaking change - [ ] Documentation update - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - **Cross-region inference-profile detection** via a new `bedrock::vendor` module (`canonical_vendor()`), following the design proposed in #953: strip a known geo prefix (`eu.`/`us.`/`apac.`/`global.`) then match the canonical vendor. Geo-prefixed Anthropic profiles (`eu.anthropic.…`) now get live-zone compression instead of being silently skipped; geo-prefixed non-Anthropic vendors stay correctly excluded. - **Converse-body compression (two parts)**: 1. `run_anthropic_compression` no longer bails to passthrough when the body lacks an InvokeModel `anthropic_version` envelope; envelope re-emit stays gated on successful parse. 2. The **live-zone dispatcher now recognizes Bedrock Converse content blocks**. Converse blocks carry no `type` discriminator (the variant is the key: `{"text": …}` vs Anthropic's `{"type":"text","text":…}`), so real Converse user-message text was still passing through uncompressed. A typeless block whose `text` is a JSON string now routes through the same surgical text path. Anthropic blocks always carry `type`, so the Anthropic path is byte-for-byte unchanged; non-text Converse blocks (`{"image":…}`, `{"toolUse":…}`) stay unrecognized and no-op. - **Correct `/converse` upstream routing**: the non-streaming handler resolved the upstream action from a hard-coded `"invoke"`, so `/converse` requests were forwarded to Bedrock's `/invoke` endpoint. It now resolves the action from the inbound path (`extract_invoke_action`), mirroring the streaming handler's `extract_streaming_action`. SigV4 signs the same URL it forwards, so the signature stays consistent. - **`aws-config` `sso` feature**: SSO profiles now resolve through the default credential chain for SigV4 — the credential chain in `docs/bedrock.md` already promised SSO; this makes the code match. - **Ship the `headroom-proxy` binary in published images** (`Dockerfile`): built in the builder stage (`--locked`, with the cargo registry cache mounted at `CARGO_HOME`) and copied into both the debian and distroless runtime images. - **Docs** (`docs/bedrock.md`): document cross-region inference profiles and a "Running the proxy" section. AWS credentials mount at `/home/nonroot/.aws` (the default nonroot image home) where the SDK looks for `~/.aws`, with a note on the root-image alternative. ## Related issues - Closes #976 — ship the `headroom-proxy` binary in published images (this PR implements the exact fix proposed there). - Addresses the **cross-region inference-profile** half of #953 via its proposed `canonical_vendor()` design. Non-Anthropic vendor compression parity (Nova/GLM/MiniMax/ Kimi) is the natural follow-up — `bedrock::vendor` is the shared resolver it can build on. - Extends the native Bedrock InvokeModel compression requested in #734 (the Bedrock slice of #510) to cross-region profiles and Converse bodies. - Partially enables #181 (native, Python-free packaging): the native binary now ships in the images, though full Python-free distribution remains out of scope. ## Alignment with the Rust migration plan Per `docs/spec/022-rust-migration.md`, the migration is **proxy-first**: `headroom-proxy` is the deployable Rust artifact, native routes replace Python passthroughs one at a time (Stage 4 = provider expansion, Bedrock included), and the binary is meant to be "built, tested, and **released together with the Python package**." Two ways this PR advances that: - The binary-in-images change makes the codebase do what the spec already states (ship the artifact) — closing the gap that forced downstreams to build from source. - Hardening the native Bedrock route (cross-region, Converse routing + body compression) is exactly the Stage-4 provider-expansion work, keeping the native path at parity with real traffic so it can be the default rather than a passthrough. ## Testing - [x] Unit tests pass (`cargo test -p headroom-core -p headroom-proxy` — full suites, 0 failures) - [x] Linting passes (`cargo clippy -p headroom-core -p headroom-proxy --all-targets -- -D warnings`) - [x] Formatting passes (`cargo fmt -- --check`) - [x] New tests added — `bedrock::vendor` (foundation + inference-profile matching), `extract_invoke_action` + converse upstream URL, and live-zone Converse text-block routing (`block_has_string_text_field`, converse-vs-anthropic dispatch equivalence). - [x] Manual testing performed ### Test Output ```text $ cargo test -p headroom-core -p headroom-proxy # all suites: ok, 0 failed $ cargo clippy -p headroom-core -p headroom-proxy --all-targets -- -D warnings # Finished, no warnings $ cargo fmt -- --check # clean # image validation (local, proxy/code extras): $ docker build --target runtime ... # debian: /usr/local/bin/headroom-proxy, --help OK $ docker build --target runtime-slim ... # distroless: binary links + --help OK ``` ## Real Behavior Proof - Environment: native Bedrock proxy against `bedrock-runtime.eu-west-2`, SSO profile, model `eu.anthropic.claude-haiku-4-5-20251001-v1:0`. - Exact command / steps: POST a large multi-turn Converse body to `/model/eu.anthropic.claude-haiku-4-5-20251001-v1:0/converse`; separately build the `runtime` + `runtime-slim` targets and run `/usr/local/bin/headroom-proxy --help`. - Observed result: before — `bedrock_compression_skipped` (geo-prefixed id not recognized), forwarded uncompressed to the wrong `/invoke` upstream; after — geo-prefixed id recognized, `/converse` forwarded to the `/converse` upstream, live-zone dispatcher compresses the Converse user-message text, measurable token savings. Images contain a runnable `headroom-proxy` in both variants. - Not tested: non-Anthropic vendor compression parity (#953 follow-up); Converse `toolResult` nested-text compression (follow-up — only top-level Converse text blocks compress today). ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Checklist - [x] My code follows the project's style guidelines - [x] I have performed a self-review of my code - [x] I have commented my code, particularly in hard-to-understand areas - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective - [x] New and existing unit tests pass locally with my changes - [x] I have updated the CHANGELOG.md if applicable ## Additional Notes - An earlier revision flipped the EventStream `Accept` default (`*/*`/absent → passthrough); **dropped** — `*/*` is what most clients (incl. reqwest and the proxy's own metrics tests) send while expecting SSE, so forcing passthrough breaks the standard SSE path. - The binary build adds the native-proxy compile to the image build; happy to gate it behind a build arg if maintainers prefer it opt-in. - Addressed a Copilot review round: corrected the `/converse` upstream routing, the stale `run_anthropic_compression` comment, the Dockerfile cargo cache mount + `--locked`, and the nonroot AWS-credentials docs example. |
||
|
|
b70fccbe17
|
fix(proxy): read RTK gain stats globally by default (#957)
## Description Closes #900. The proxy now reads RTK lifetime savings with global scope by default. This matches shared daemon deployments where the proxy process cwd is often `$HOME` or a service directory, while RTK savings are accumulated across the operator's projects. `HEADROOM_RTK_GAIN_SCOPE=project` keeps the old `rtk gain --project` behavior for operators who explicitly want the proxy process working directory as the scope. ## 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 - Default RTK stats subprocess command to `rtk gain --format json` - Add `HEADROOM_RTK_GAIN_SCOPE=project` to opt into `rtk gain --project --format json` - Keep fallback/synthetic-zero payload `scope` aligned with the queried scope - Deduplicate context-tool zero payload construction - Document the new RTK gain scope environment variable ## 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 uv run --extra dev python -m pytest tests/test_proxy_dashboard_stats_cache.py tests/test_subscription_tracker_rtk_wired.py -q ============================= test session starts ============================== platform darwin -- Python 3.14.5, pytest-9.0.3, pluggy-1.6.0 rootdir: /Users/joshuasiu/vibe/temp/headroom configfile: pyproject.toml plugins: anyio-4.12.1, langsmith-0.8.0, 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 26 items tests/test_proxy_dashboard_stats_cache.py .......... [ 38%] tests/test_subscription_tracker_rtk_wired.py ................ [100%] ============================== 26 passed in 0.40s ============================== uv run --extra dev python -m pytest tests/test_proxy_stats_recent_requests.py tests/test_proxy_healthchecks.py -q ============================= test session starts ============================== platform darwin -- Python 3.14.5, pytest-9.0.3, pluggy-1.6.0 rootdir: /Users/joshuasiu/vibe/temp/headroom configfile: pyproject.toml plugins: anyio-4.12.1, langsmith-0.8.0, 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 15 items tests/test_proxy_stats_recent_requests.py ... [ 20%] tests/test_proxy_healthchecks.py ............ [100%] ============================= 15 passed in 10.41s ============================== uv run --extra dev ruff check headroom/proxy/helpers.py tests/test_proxy_dashboard_stats_cache.py tests/test_subscription_tracker_rtk_wired.py All checks passed! uv run --extra dev ruff format --check headroom/proxy/helpers.py tests/test_proxy_dashboard_stats_cache.py tests/test_subscription_tracker_rtk_wired.py 3 files already formatted uv run --extra dev mypy headroom headroom/proxy/server.py:1141: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs [annotation-unchecked] headroom/proxy/server.py:1211: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs [annotation-unchecked] headroom/proxy/server.py:1215: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs [annotation-unchecked] Success: no issues found in 358 source files ``` ## Real Behavior Proof - Environment: macOS, Python 3.14.5 via `uv run --extra dev` - Exact command / steps: mocked RTK subprocess argv in unit tests - Observed result: default command is `rtk gain --format json`; project scope command is `rtk gain --project --format json`; invalid scope logs `event=rtk_gain_scope_invalid` and falls back to global - Not tested: full repository pytest suite ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Checklist - [x] My code follows the project's style guidelines - [x] I have performed a self-review of my code - [ ] I have commented my code, particularly in hard-to-understand areas - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] I have updated the CHANGELOG.md if applicable ## Additional Notes The unchecked comment and changelog items are not applicable for this scoped proxy stats fix. |
||
|
|
4f59097045
|
ci: bump esbuild from 0.27.7 to 0.28.1 in /docs in the npm_and_yarn group across 1 directory (#936)
Bumps the npm_and_yarn group with 1 update in the /docs directory: [esbuild](https://github.com/evanw/esbuild). Updates `esbuild` from 0.27.7 to 0.28.1 <details> <summary>Release notes</summary> <p><em>Sourced from <a href="https://github.com/evanw/esbuild/releases">esbuild's releases</a>.</em></p> <blockquote> <h2>v0.28.1</h2> <ul> <li> <p>Disallow <code>\</code> in local development server HTTP requests (<a href="https://github.com/evanw/esbuild/security/advisories/GHSA-g7r4-m6w7-qqqr">GHSA-g7r4-m6w7-qqqr</a>)</p> <p>This release fixes a security issue where HTTP requests to esbuild's local development server could traverse outside of the serve directory on Windows using a <code>\</code> backslash character. It happened due to the use of Go's <code>path.Clean()</code> function, which only handles Unix-style <code>/</code> characters. HTTP requests with paths containing <code>\</code> are no longer allowed.</p> <p>Thanks to <a href="https://github.com/dellalibera"><code>@dellalibera</code></a> for reporting this issue.</p> </li> <li> <p>Add integrity checks to the Deno API (<a href="https://github.com/evanw/esbuild/security/advisories/GHSA-gv7w-rqvm-qjhr">GHSA-gv7w-rqvm-qjhr</a>)</p> <p>The previous release of esbuild added integrity checks to esbuild's npm install script. This release also adds integrity checks to esbuild's Deno install script. Now esbuild's Deno API will also fail with an error if the downloaded esbuild binary contains something other than the expected content.</p> <p>Note that esbuild's Deno API installs from <code>registry.npmjs.org</code> by default, but allows the <code>NPM_CONFIG_REGISTRY</code> environment variable to override this with a custom package registry. This change means that the esbuild executable served by <code>NPM_CONFIG_REGISTRY</code> must now match the expected content.</p> <p>Thanks to <a href="https://github.com/sondt99"><code>@sondt99</code></a> for reporting this issue.</p> </li> <li> <p>Avoid inlining <code>using</code> and <code>await using</code> declarations (<a href="https://redirect.github.com/evanw/esbuild/issues/4482">#4482</a>)</p> <p>Previously esbuild's minifier sometimes incorrectly inlined <code>using</code> and <code>await using</code> declarations into subsequent uses of that declaration, which then fails to dispose of the resource correctly. This bug happened because inlining was done for <code>let</code> and <code>const</code> declarations by avoiding doing it for <code>var</code> declarations, which no longer worked when more declaration types were added. Here's an example:</p> <pre lang="js"><code>// Original code { using x = new Resource() x.activate() } <p>// Old output (with --minify)<br /> new Resource().activate();</p> <p>// New output (with --minify)<br /> {using e=new Resource;e.activate()}<br /> </code></pre></p> </li> <li> <p>Fix module evaluation when an error is thrown (<a href="https://redirect.github.com/evanw/esbuild/issues/4461">#4461</a>, <a href="https://redirect.github.com/evanw/esbuild/pull/4467">#4467</a>)</p> <p>If an error is thrown during module evaluation, esbuild previously didn't preserve the state of the module for subsequent module references. This was observable if <code>import()</code> or <code>require()</code> is used to import a module multiple times. The thrown error is supposed to be thrown by every call to <code>import()</code> or <code>require()</code>, not just the first. With this release, esbuild will now throw the same error every time you call <code>import()</code> or <code>require()</code> on a module that throws during its evaluation.</p> </li> <li> <p>Fix some edge cases around the <code>new</code> operator (<a href="https://redirect.github.com/evanw/esbuild/issues/4477">#4477</a>)</p> <p>Previously esbuild incorrectly printed certain edge cases involving complex expressions inside the target of a <code>new</code> expression (specifically an optional chain and/or a tagged template literal). The generated code for the <code>new</code> target was not correctly wrapped with parentheses, and either contained a syntax error or had different semantics. These edge cases have been fixed so that they now correctly wrap the <code>new</code> target in parentheses. Here is an example of some affected code:</p> <pre lang="js"><code>// Original code new (foo()`bar`)() new (foo()?.bar)() <p>// Old output<br /> new foo()<code>bar</code>();<br /> new (foo())?.bar();</p> <p></code></pre></p> </li> </ul> <!-- raw HTML omitted --> </blockquote> <p>... (truncated)</p> </details> <details> <summary>Changelog</summary> <p><em>Sourced from <a href="https://github.com/evanw/esbuild/blob/main/CHANGELOG.md">esbuild's changelog</a>.</em></p> <blockquote> <h2>0.28.1</h2> <ul> <li> <p>Disallow <code>\</code> in local development server HTTP requests (<a href="https://github.com/evanw/esbuild/security/advisories/GHSA-g7r4-m6w7-qqqr">GHSA-g7r4-m6w7-qqqr</a>)</p> <p>This release fixes a security issue where HTTP requests to esbuild's local development server could traverse outside of the serve directory on Windows using a <code>\</code> backslash character. It happened due to the use of Go's <code>path.Clean()</code> function, which only handles Unix-style <code>/</code> characters. HTTP requests with paths containing <code>\</code> are no longer allowed.</p> <p>Thanks to <a href="https://github.com/dellalibera"><code>@dellalibera</code></a> for reporting this issue.</p> </li> <li> <p>Add integrity checks to the Deno API (<a href="https://github.com/evanw/esbuild/security/advisories/GHSA-gv7w-rqvm-qjhr">GHSA-gv7w-rqvm-qjhr</a>)</p> <p>The previous release of esbuild added integrity checks to esbuild's npm install script. This release also adds integrity checks to esbuild's Deno install script. Now esbuild's Deno API will also fail with an error if the downloaded esbuild binary contains something other than the expected content.</p> <p>Note that esbuild's Deno API installs from <code>registry.npmjs.org</code> by default, but allows the <code>NPM_CONFIG_REGISTRY</code> environment variable to override this with a custom package registry. This change means that the esbuild executable served by <code>NPM_CONFIG_REGISTRY</code> must now match the expected content.</p> <p>Thanks to <a href="https://github.com/sondt99"><code>@sondt99</code></a> for reporting this issue.</p> </li> <li> <p>Avoid inlining <code>using</code> and <code>await using</code> declarations (<a href="https://redirect.github.com/evanw/esbuild/issues/4482">#4482</a>)</p> <p>Previously esbuild's minifier sometimes incorrectly inlined <code>using</code> and <code>await using</code> declarations into subsequent uses of that declaration, which then fails to dispose of the resource correctly. This bug happened because inlining was done for <code>let</code> and <code>const</code> declarations by avoiding doing it for <code>var</code> declarations, which no longer worked when more declaration types were added. Here's an example:</p> <pre lang="js"><code>// Original code { using x = new Resource() x.activate() } <p>// Old output (with --minify)<br /> new Resource().activate();</p> <p>// New output (with --minify)<br /> {using e=new Resource;e.activate()}<br /> </code></pre></p> </li> <li> <p>Fix module evaluation when an error is thrown (<a href="https://redirect.github.com/evanw/esbuild/issues/4461">#4461</a>, <a href="https://redirect.github.com/evanw/esbuild/pull/4467">#4467</a>)</p> <p>If an error is thrown during module evaluation, esbuild previously didn't preserve the state of the module for subsequent module references. This was observable if <code>import()</code> or <code>require()</code> is used to import a module multiple times. The thrown error is supposed to be thrown by every call to <code>import()</code> or <code>require()</code>, not just the first. With this release, esbuild will now throw the same error every time you call <code>import()</code> or <code>require()</code> on a module that throws during its evaluation.</p> </li> <li> <p>Fix some edge cases around the <code>new</code> operator (<a href="https://redirect.github.com/evanw/esbuild/issues/4477">#4477</a>)</p> <p>Previously esbuild incorrectly printed certain edge cases involving complex expressions inside the target of a <code>new</code> expression (specifically an optional chain and/or a tagged template literal). The generated code for the <code>new</code> target was not correctly wrapped with parentheses, and either contained a syntax error or had different semantics. These edge cases have been fixed so that they now correctly wrap the <code>new</code> target in parentheses. Here is an example of some affected code:</p> <pre lang="js"><code>// Original code new (foo()`bar`)() new (foo()?.bar)() <p>// Old output<br /> new foo()<code>bar</code>();<br /> new (foo())?.bar();<br /> </code></pre></p> </li> </ul> <!-- raw HTML omitted --> </blockquote> <p>... (truncated)</p> </details> <details> <summary>Commits</summary> <ul> <li><a href=" |
||
|
|
7ced77b6e7
|
docs: fix dead contact links in issue templates and troubleshooting guide (#910)
## Summary The side note in #855 reports that the **Issues → Question** contact link points to a non-existing page. Confirmed, plus two more dead links of the same class: - `.github/ISSUE_TEMPLATE/config.yml` — "Questions & Discussions" points at `github.com/headroom-sdk/headroom/discussions` (the `headroom-sdk` org 404s); now points at this repo's Discussions (live, Discussions are enabled here). - `.github/ISSUE_TEMPLATE/config.yml` — "Documentation" points at `headroom.dev/docs` (404); now points at the repo homepage docs site `headroom-docs.vercel.app/docs` (200). - `docs/content/docs/troubleshooting.mdx` — "File an issue at github.com/headroom-sdk/headroom" same dead org; now points at this repo. `.github/FUNDING.yml` also references `headroom-sdk` but that's a sponsorship target choice, so left untouched. ## Testing Link targets verified by HTTP status: old URLs return 404, new URLs return 200. Docs-only change, no code paths affected. Fixes the side note in #855. Co-authored-by: integration-check <integration@local> |
||
|
|
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.
|
||
|
|
2533f7703e
|
fix(ccr): make retrieval TTL configurable (#715)
## Description Make the CCR retrieval store TTL configurable so long-running agent jobs can keep `headroom_retrieve` markers resolvable beyond the previous hard-coded 300-second window. Fixes #714 ## 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 - Add `HEADROOM_CCR_TTL_SECONDS` for the global CCR `CompressionStore` default TTL, with validation and fallback to 300 seconds. - Expose the effective CCR TTL as `store.default_ttl_seconds` from `/v1/retrieve/stats`. - Distinguish missing vs expired CCR retrieval failures in `/v1/retrieve`, `/v1/retrieve/{hash}`, tool-call handling, and CCR response handling. - Update CCR docs, README wording, and CHANGELOG for the user-facing TTL behavior. - Add regression tests for env-configured TTL, invalid env fallback, explicit TTL precedence, stats exposure, and expired retrieval detail. ## Reproduction Before this change, the proxy-global CCR store always used the default 300-second TTL when callers used `get_compression_store()` without an explicit `default_ttl`. A long-running agent job could receive a `<<ccr:...>>` marker and fail later with a generic not-found/expired response after the fixed 5-minute retention window. The new tests cover this by setting `HEADROOM_CCR_TTL_SECONDS`, creating CCR entries through the same global store used by the proxy, and asserting that stored entries and `/v1/retrieve/stats` use the configured retention. ## Real behavior proof Setup tested: - macOS 15.7.2 - Python 3.13.9 via `uv run --frozen --extra dev --extra proxy` - Local Headroom proxy subprocess: `python -m headroom.proxy.server` - Proxy config: `HEADROOM_TELEMETRY=off`, `HEADROOM_CACHE_ENABLED=false`, `HEADROOM_RATE_LIMIT_ENABLED=false` - Provider/model: no live upstream provider needed; exercised local `/v1/compress` -> `/v1/retrieve` CCR HTTP flow with model `gpt-4o` Exact steps run after the patch: 1. Start a real Headroom proxy process with `HEADROOM_CCR_TTL_SECONDS=7200`. 2. POST a 200-item tool-result payload to `/v1/compress`. 3. Extract the emitted `<<ccr:...>>` marker hash from the compressed messages. 4. GET `/v1/retrieve/stats`. 5. POST `/v1/retrieve` with the marker hash. 6. Repeat with `HEADROOM_CCR_TTL_SECONDS=1`, wait 1.5 seconds, and retrieve the same way to verify explicit expiration reporting. Observed result: ```json { "long_ttl": { "ccr_hash": "b473e632aa47", "retrieve_status": 200, "retrieved_content_has_result_199": true, "stats_default_ttl_seconds": 7200, "stats_entry_count": 1, "ttl_seconds": 7200 }, "short_ttl_expired": { "ccr_hash": "b473e632aa47", "retrieve_detail": "Entry expired (CCR TTL: 1 seconds; age: 2 seconds)", "retrieve_status": 404, "stats_default_ttl_seconds": 1, "stats_entry_count": 1, "ttl_seconds": 1 } } ``` What I did not test: - A live OpenRouter/OpenAI/Anthropic upstream request. - A durable/non-memory CCR backend. - A literal 5+ minute wall-clock wait; the process E2E used `7200` to prove configured retention and `1` second to prove expiration behavior quickly. ## 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 ```bash UV_SKIP_WHEEL_FILENAME_CHECK=1 uv run --frozen --extra dev --extra proxy pytest tests/test_compression_store.py tests/test_proxy_ccr.py tests/test_ccr_response_handler.py tests/test_ccr_response_handler_extra.py # 152 passed, 2 warnings in 12.87s UV_SKIP_WHEEL_FILENAME_CHECK=1 uv run --frozen --extra dev --extra proxy pytest tests/test_adapter_hooks.py tests/test_toin_feedback.py tests/test_toin_fixes.py # 55 passed, 13 skipped, 1 warning in 0.53s UV_SKIP_WHEEL_FILENAME_CHECK=1 uv run --frozen --extra dev --extra proxy ruff check . # All checks passed! UV_SKIP_WHEEL_FILENAME_CHECK=1 uv run --frozen --extra dev --extra proxy ruff format --check . # 776 files already formatted UV_SKIP_WHEEL_FILENAME_CHECK=1 uv run --frozen --extra dev --extra proxy mypy headroom # Success: no issues found in 346 source files ``` Existing warnings observed in the targeted tests were unrelated to this change: - AnthropicProvider tiktoken approximation warning in proxy CCR tests. - Deprecated `ToolIntelligenceNetwork.get_recommendation()` warning in existing TOIN assertions. ## Checklist - [x] My code follows the project's style guidelines - [x] I have performed a self-review of my code - [x] I have commented my code, particularly in hard-to-understand areas - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [x] I have updated the CHANGELOG.md if applicable ## Screenshots (if applicable) Not applicable. ## Additional Notes No new dependencies. Default behavior remains 300 seconds unless `HEADROOM_CCR_TTL_SECONDS` is set. |
||
|
|
45b07bf754
|
docs: align MCP proxy docs with implemented API (#614)
## Summary - remove the doc references to a runnable `HeadroomMCPProxy` server that does not exist today - describe the MCP integration surfaces that are actually implemented in `headroom/integrations/mcp/server.py` - update the internal integrations spec so it points readers to `headroom mcp serve` for the ready-to-run MCP server ## Verification - `python -m compileall headroom/integrations/mcp/server.py` - attempted `uv run pytest tests/test_integrations/mcp/test_server.py -q`, but local verification was blocked by the current `uv.lock` wheel filename check for `gitpython` Refs #588. |
||
|
|
35b46d6e84
|
ci: bump brace-expansion from 5.0.5 to 5.0.6 in /docs in the npm_and_yarn group across 1 directory (#835)
Bumps the npm_and_yarn group with 1 update in the /docs directory: [brace-expansion](https://github.com/juliangruber/brace-expansion). Updates `brace-expansion` from 5.0.5 to 5.0.6 <details> <summary>Commits</summary> <ul> <li><a href=" |
||
|
|
1c8c538780
|
fix: preserve Claude Code tool-search deferral through the proxy (#746) (#753)
Routing Claude Code through the proxy disabled its on-demand tool loading: with a custom ANTHROPIC_BASE_URL and ENABLE_TOOL_SEARCH unset, Claude Code stops deferring MCP/system tool schemas behind the server-side Tool Search Tool and materializes them all into local context (~25K tokens) — the opposite of what a context-optimization proxy should do. Root cause is a client-side gate in Claude Code (isToolSearchEnabledOptimistic): deferral is disabled when ENABLE_TOOL_SEARCH is unset AND provider is first-party AND the base-URL host is not api.anthropic.com. It is a one-way URL check, not a capability handshake, so no proxy/response header can flip it. The only lever is the ENABLE_TOOL_SEARCH env var Claude Code reads at startup. Changes: - wrap claude: inject ENABLE_TOOL_SEARCH into the launched Claude Code env (default "true"; --tool-search true|auto|auto:N|false; a pre-set env value is respected; blank is treated as unset). Keeps deferral on through the proxy. - proxy: emit a one-time, actionable hint when a Claude Code request is detected loading tools eagerly (for users who run `claude` manually). Gated on a cheap one-shot flag and wrapped so it can never fail a request. - docs: troubleshooting section with before/after verification. - tests: 30 unit tests (value validation, injection precedence, detection, hint content, one-shot guard). |
||
|
|
fb59f83fab
|
Merge pull request #592 from divyanshus2404/my-first-contribution
docs: add troubleshooting section |