Commit graph

138 commits

Author SHA1 Message Date
dependabot[bot]
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="bb9db84c02"><code>bb9db84</code></a>
publish 0.28.1 to npm</li>
<li><a
href="9ff053e53b"><code>9ff053e</code></a>
security: add integrity checks to the Deno API</li>
<li><a
href="0a9bf2135b"><code>0a9bf21</code></a>
enforce non-negative size in gzip parser</li>
<li><a
href="e2a1a71320"><code>e2a1a71</code></a>
security: forbid <code>\\</code> in local dev server requests</li>
<li><a
href="83a2cbfc35"><code>83a2cbf</code></a>
fix <a
href="https://redirect.github.com/evanw/esbuild/issues/4482">#4482</a>:
don't inline <code>using</code> declarations</li>
<li><a
href="308ad745d8"><code>308ad74</code></a>
fix <a
href="https://redirect.github.com/evanw/esbuild/issues/4471">#4471</a>:
renaming of nested <code>var</code> declarations</li>
<li><a
href="f013f5f99a"><code>f013f5f</code></a>
fix some typos</li>
<li><a
href="aafd6e48b1"><code>aafd6e4</code></a>
chore: fix some minor issues in comments (<a
href="https://redirect.github.com/evanw/esbuild/issues/4462">#4462</a>)</li>
<li><a
href="15300c30b5"><code>15300c3</code></a>
follow up: cjs evaluation fixes</li>
<li><a
href="1bda0c31d7"><code>1bda0c3</code></a>
fix <a
href="https://redirect.github.com/evanw/esbuild/issues/4461">#4461</a>,
fix <a
href="https://redirect.github.com/evanw/esbuild/issues/4467">#4467</a>:
esm evaluation fixes</li>
<li>Additional commits viewable in <a
href="https://github.com/evanw/esbuild/compare/v0.27.7...v0.28.1">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=esbuild&package-manager=npm_and_yarn&previous-version=0.27.7&new-version=0.28.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/chopratejas/headroom/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-12 23:16:29 -05:00
Focused Instability
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>
2026-06-12 14:47:10 -07:00
Logan Kang
c71592d421
feat(memory): add opt-in Apple-GPU (MPS) embedding runtime (#766)
## Description

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

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

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

## Type of Change

- [ ] Bug fix (non-breaking change that fixes an issue)
- [x] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
- [x] Performance improvement
- [ ] Code refactoring (no functional changes)

## Changes Made

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

## Testing

- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [x] Type checking passes (`mypy headroom`)
- [x] New tests added for new functionality
- [x] Manual testing performed (CPU-offload + concurrency profiling on
Apple Silicon)

## Test Output

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

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

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

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

## Checklist

- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] I have updated the CHANGELOG.md if applicable

## Additional Notes

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

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

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

**Safety:** default behavior is unchanged (ONNX, no torch). The feature
is opt-in, env-var only, macOS-gated at the packaging layer, and
degrades gracefully (warn + the existing default embedder selection)
when MPS or the dependencies are unavailable.
2026-06-11 12:59:20 -05:00
Hc
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.
2026-06-10 23:20:46 -05:00
Andrew Barnes
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.
2026-06-10 21:14:23 -05:00
dependabot[bot]
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="46317b5d87"><code>46317b5</code></a>
5.0.6</li>
<li><a
href="c0b095bdc5"><code>c0b095b</code></a>
Merge commit from fork</li>
<li><a
href="ec5602085a"><code>ec56020</code></a>
Bump picomatch from 4.0.3 to 4.0.4 (<a
href="https://redirect.github.com/juliangruber/brace-expansion/issues/93">#93</a>)</li>
<li>See full diff in <a
href="https://github.com/juliangruber/brace-expansion/compare/v5.0.5...v5.0.6">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=brace-expansion&package-manager=npm_and_yarn&previous-version=5.0.5&new-version=5.0.6)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/chopratejas/headroom/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-10 18:32:56 -05:00
Tejas Chopra
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).
2026-06-08 11:20:48 -07:00
Devanshi Vyas
fb59f83fab
Merge pull request #592 from divyanshus2404/my-first-contribution
docs: add troubleshooting section
2026-06-06 12:08:07 -07:00
Divyanshu Singh
67f005e434 Address PR feedback: Move troubleshooting, refine rust docs, and update install options 2026-06-07 00:27:09 +05:30
oxura
10da89d3ce docs: explain Claude MCP usage attribution 2026-06-06 19:50:24 +06:00
RTCartist
794916bf1a docs(install): document Windows build prerequisites
Addresses #636.
2026-06-05 22:35:31 +08:00
Tejas Chopra
3599de8e7a
Merge branch 'main' into docs/fix-stale-and-incorrect-docs 2026-06-04 10:57:40 -07:00
Tejas Chopra
a5ff663a5e
Merge pull request #579 from praneetware/issue-561-anthropic-api-url
Issue 561 anthropic api url
2026-06-03 23:55:35 -07:00
Praneet
b1d1f8cd66 docs(proxy): document ANTHROPIC_TARGET_API_URL 2026-06-04 12:04:12 +05:30
Patrick Ancillotti
0375f7f0aa docs: fix stale API references, retired class imports, and incorrect examples
- Remove rolling_window_config from HeadroomClient Python constructor table
  (RollingWindowConfig was retired in 0.9.x)
- Fix HeadroomConfig Python example: replace config.rolling_window.preserve_recent_turns
  with a note that rolling_window was removed
- Fix ccrHashes description: cross-conversation retrieval -> Compress-Cache-Retrieve
- All other doc fixes were already applied in prior commits
2026-06-02 19:19:19 -04:00
Hermes Agent
91e0937243 fix(docs): update bun.lock to next 16.2.6 for GHSA-h64f-5h5j-jqjh (CVE-2026-44577)
The package-lock.json was already bumped to 16.2.6 in a prior commit,
but bun.lock still pinned next at 16.2.4 (vulnerable to Image Optimization
API DoS per GHSA-h64f-5h5j-jqjh). This ensures all lockfiles are consistent.

VIPER hash: 835f8d5b1d2d350d
Refs: GHSA-h64f-5h5j-jqjh / CVE-2026-44577
2026-06-02 04:25:21 +00:00
Steven Cuz Leath
db5d15f99e Fix: Update Next.js to 16.2.6 in docs/package.json and package-lock.json to address GHSA-h64f-5h5j-jqjh (CVE-2026-44577) 2026-06-01 10:32:23 +00:00
Steven Cuz Leath
6eb6fb5941 fix(docs): update brace-expansion to 5.0.6 to remediate GHSA-jxxr-4gwj-5jf2 (CVE-2026-45149) 2026-06-01 10:24:34 +00:00
Steven Cuz Leath
0b9f11a223 Fix: Update Next.js to 16.2.4 in docs/bun.lock to address GHSA-gx5p-jg67-6x7h (CVE-2026-44580) 2026-06-01 10:16:15 +00:00
pratikbin
42b1cd24de docs: fix env var discrepancies across markdown files
Audit all .md files against codebase; fix wrong names, remove phantom
variables, and correct outdated values:

- HEADROOM_PROXY_PORT → HEADROOM_PORT (proxy.py envvar="HEADROOM_PORT")
- HEADROOM_BIND → HEADROOM_HOST + HEADROOM_PORT (RUST_DEV.md)
- HEADROOM_LEARN_{CLAUDE,CODEX,GEMINI}_ENABLED → HEADROOM_LEARN_CLI
  (only HEADROOM_LEARN_CLI exists in learn/analyzer.py)
- HEADROOM_TRACING_ENABLED → HEADROOM_LANGFUSE_ENABLED=1 with correct
  LANGFUSE_PUBLIC_KEY/SECRET_KEY vars (tracing.py)
- HEADROOM_LOG_LEVEL/LOG_FORMAT → --log-level CLI flag / RUST_LOG
  (no HEADROOM_LOG_LEVEL var exists in code)
- HEADROOM_LOG_LEVEL/HEADROOM_STORE_URL/HEADROOM_DEFAULT_MODE rows
  removed from wiki/configuration.md (all phantom)
- HEADROOM_SUMMARY_{ENABLED,THRESHOLD,RATIO} noted as not yet
  implemented (no code exists)
- HEADROOM_DB_URL/HEADROOM_CACHE_BACKEND → explanatory notes pointing
  to HEADROOM_WORKSPACE_DIR (no external DB support in code)
- HEADROOM_DB_PATH/HEADROOM_CACHE_PATH table rows replaced with actual
  HEADROOM_WORKSPACE_DIR/CONFIG_DIR (paths.py)
2026-05-29 15:04:13 +05:30
Tejas Chopra
2132dc2399
Merge pull request #461 from SvenMeyer/docs/clarify-mcp-proxy-setup
docs: clarify proxy-backed MCP setup
2026-05-26 13:44:58 -07:00
chopratejas
2a717a993e fix(observability): G3 remediation — bound cardinality + wire dead metrics
Phase G PR-G3 review identified 5 Critical + 4 High + 5 Medium
findings. This commit lands all 14 fixes plus the optional nits.

CRITICAL

* C1 (cardinality DoS): `service_tier` was read from inbound JSON
  and used verbatim as a metric label. A malicious client could
  blow up the metric vector unboundedly. Added bounded vocabulary
  in `metric_names.rs::service_tier` ({auto, default, flex,
  on_demand, priority, scale, other-sentinel}) + a `validate()`
  helper. Both request-side (`handlers/responses.rs`) and
  response-side (`proxy.rs` Responses arm) gate raw values through
  it.

* C2 (dead metric): `proxy_passthrough_bytes_modified_total` had
  no production emit site. Wired it in `proxy.rs` to fire when a
  dispatcher arm returning `NoCompression`/`Passthrough` produces
  a body of a different byte length (a true cache-poisoning
  regression detector). The check runs BEFORE the PR-E4
  prompt_cache_key injector so legitimate injector mutations do
  not trip the alarm.

* C3 (Python/Rust boundary): `proxy_image_generation_call_log_redacted_total`
  was a dead Rust counter — the redaction happens entirely in the
  Python proxy's request_logger. Removed the Rust counter; moved
  the metric to the Python proxy's `/metrics` exporter via the
  existing `redactions_total()` module-level counter.

* C4 (Python/Rust boundary): `wrap_rtk_invocations_total` was a
  dead Rust counter with no wrap-side bridge. Removed the Rust
  counter; added new `headroom/cli/wrap_rtk_metrics.py` with
  `record_rtk_invocation(tool, delta)` + `rtk_invocation_counts()`
  primitives and surfaced them via the Python proxy's `/metrics`
  exporter.

* C5 (dead metric): `proxy_compression_rejected_by_token_check_total`
  had no production caller. Wired it in
  `live_zone_anthropic.rs`, `live_zone_openai.rs`, and
  `live_zone_responses.rs` to increment on every
  `BlockAction::RejectedNotSmaller` block in the manifest. The
  metric now reflects real "compressor ran but kept original"
  cases.

HIGH

* H1 (per-strategy ratio garbage): `proxy_compression_ratio_by_strategy`
  emitted the same aggregate ratio for every strategy in
  `strategies_applied` when multiple strategies ran on one body.
  Added `per_strategy_tokens: Vec<PerStrategyTokens>` to
  `Outcome::Compressed`; per-strategy `(before, after)` is
  accumulated from the manifest at the wrapper sites and emitted
  one sample per strategy in `proxy.rs`. Empty vec → fallback to
  one aggregate-labelled sample with a debug log (Phase E
  normalization paths that don't track per-strategy tokens).

* H2 (aborted stream): cache_hit_rate observed on client
  disconnects mid-stream. Added a gate: Anthropic only fires when
  `state.status == MessageStop`, OpenAI Responses only when
  `terminal_status().is_some()`. Extracted the gate into the
  pure function `compute_anthropic_session_hit_rate(state)` so
  the H2 contract is unit-testable independent of the shared
  global registry.

* H3 (docs lie + alarm contract): docs claimed HELP/TYPE is
  reachable on fresh boot, then contradicted itself. Force-zero
  every counter / gauge MetricVec with an `__init__` sentinel
  label on each scrape so HELP/TYPE + a zero row are visible from
  boot. Histograms are NOT force-zeroed (a synthetic observe(0.0)
  would pollute percentiles). PromQL queries in docs filter
  `{... != "__init__"}` so the sentinel rows are excluded from
  aggregations.

* H4 (crate-version dependency): pinned `prometheus = "=0.13.4"`
  exactly (no caret) so a future minor bump cannot silently break
  the H3 force-zero contract that relies on this crate's gather()
  semantics. Added a clear "retest the alarm contract on bump"
  paragraph in docs.

MEDIUM

* M1 (saturate on cached > input): OpenAI Chat + Responses cache-
  hit-rate computed `non_cached = input.saturating_sub(cached)`,
  silently clamping to 0 if `cached > input`. Per "no silent
  fallbacks", log + skip the emit on this wire-format pathology.

* M2 (over-fire on non-image base64): Python redactor's "density
  heuristic" over-fired on encrypted blobs / signed tokens /
  minified JSON / tool outputs. Tightened: only redact strings
  inside known image-bearing JSON paths (`data`, `url`,
  `image_url`, `image`) OR strings starting with `data:image/`.

* M3 (NaN clamp): cache_hit_rate::observe used `f64::clamp(0,1)`
  which returns NaN for NaN input; the `debug_assert!` was
  compiled out in release. Added `is_finite()` guard with a
  loud-log + skip before observe.

* M4 (PromQL median-only): added p95, p99, mean (sum/count), and
  Phase H canary-gate query section to docs. Canary fails if ANY
  of {p50, p95, p99, mean} regresses below the Python baseline.

* M5 (label byte vs char): the `<image:base64-redacted bytes=N>`
  placeholder reported character count, not UTF-8 byte count.
  Switched to `.encode('utf-8').__len__()` so the label is
  honest for non-ASCII payloads (ASCII base64 still has byte ==
  char so existing scrapes are unchanged).

OPTIONAL

* Removed dead `debug_assert_eq!(buffered.len(), buffered.len(),
  ...)` no-op in proxy.rs.
* Normalised `record_response_status` log level from `info` to
  `debug` to match peer metric helpers.

Tests:

* Rust: 11 integration_metrics tests (was 6) + 9 cache_hit_rate
  unit tests (was 4) + 2 compression_ratio (unchanged). New
  coverage: service_tier known/unknown bucketing, C2 alarm wire,
  H1 per-strategy ratio, H2 abort gate, M3 NaN/inf skip.
* Python: 27 tests (was 13). New coverage: M2 path-gated
  redaction, M5 byte vs char label, wrap_rtk_metrics primitive
  thread safety and validation.

`cargo fmt --check`, `cargo clippy --workspace -- -D warnings`,
`cargo test -p headroom-proxy --lib` (221 passed) and the
integration_metrics + integration_compression +
integration_volatile_detector + integration_cache_control +
integration_cache_drift + integration_responses +
integration_bedrock_metrics test files all green. Full
`cargo test --workspace` deferred — disk pressure during the
agent session left insufficient space for the linker to write
the full integration test artifacts; runs that did fit all
passed. `make ci-precheck` deferred for the same reason.

ruff check + ruff format + mypy headroom/proxy/request_logger.py
+ headroom/cli/wrap_rtk_metrics.py + headroom/proxy/prometheus_metrics.py
green.
2026-05-24 10:41:56 -07:00
chopratejas
5f264a5329 fix(observability): wire Phase G PR-G3 RTK + proxy metrics (H-blocker)
Phase H ("retire the Python proxy") needs cache-hit-rate parity
between the Rust and Python proxies during canary. This PR lands
the per-invocation RTK metrics and the proxy-side observability
surface that the canary gate depends on.

Rust observability:
- `proxy_cache_hit_rate_per_session{provider}` — histogram, emitted
  per session at SSE state-machine close (Anthropic message_delta,
  OpenAI Chat final usage chunk, OpenAI Responses response.completed).
  The Phase H canary gate metric.
- `proxy_compression_ratio_by_strategy{strategy, content_type}` —
  histogram; one sample per shrunk block.
- `proxy_compression_rejected_by_token_check_total{strategy}` —
  counter for tokenizer-validated rejections.
- `proxy_passthrough_bytes_modified_total{path}` — counter (must
  stay 0 outside compression hot path; alarmable via PromQL rate).
- `proxy_rate_limit_remaining_{requests,tokens,input_tokens,output_tokens}{provider}` —
  gauges populated from anthropic-ratelimit-* / x-ratelimit-* headers.
- `proxy_service_tier_count_total{tier}` and
  `proxy_response_status_count_total{status}` — counters for
  Responses-API outcome telemetry.
- `proxy_image_generation_call_log_redacted_total` — counter.
- `wrap_rtk_invocations_total{tool}` and
  `wrap_rtk_tokens_saved_per_session` — RTK metrics exposed via
  the proxy's /metrics scrape so wrap-side tail can increment
  through one observability surface.

All metric names and label keys live in a single
`observability/metric_names.rs` constants module per realignment
build-constraint "configurable". Bounded label vocabularies
(service_tier, response_status, provider) are defined alongside.

Python (P4-45):
- `headroom/proxy/request_logger.py` — base64-image payloads in
  request/response logs over 1024 bytes are replaced with
  `<image:base64-redacted bytes=N>` placeholders. Walks Anthropic
  source.data and OpenAI data URLs. No regexes — substring +
  density heuristic.

Tests:
- `crates/headroom-proxy/tests/integration_metrics.rs` — 6 tests
  covering cache-hit-rate, compression-ratio, passthrough-bytes,
  service-tier, response-status, and rate-limit-snapshot.
- `tests/test_image_log_redaction.py` — 13 tests for the Python
  redaction helper.
- Existing tests: 1100+ Rust + 76 Python regression checks green.

Docs:
- `docs/observability.md` — metric catalogue + PromQL queries.
- `docs/rtk-architecture.md` — locks the wrap-CLI-only decision so
  future contributors don't relitigate proxy-side RTK.

No silent fallbacks: zero-denominator cache-hit-rate logs and
skips rather than synthesising 0.0. Unparseable rate-limit headers
stay None rather than coerced to 0. Missing upstream JSON fields
log + skip emit rather than fabricating data.
2026-05-22 13:18:42 -07:00
chopratejas
c1d2eec588 docs: improve discoverability for AI agents and search crawlers
Several signals AI agents and search engines use to discover and
install a project were misaligned or missing:

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

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

No Python or Rust code changes; ``make ci-precheck`` was run to
confirm the test slice still passes.
2026-05-13 17:36:06 -07:00
Sven Meyer
59dc4a508a docs: clarify proxy-backed MCP setup 2026-05-13 20:56:38 +10:00
chopratejas
0f6df1fef0 docs(readme): redesign with lean-ctx-style crispness
- ASCII block logo replaces plain # heading
- Power-stats line + nav links above the fold
- Time-boxed section headings (30s / 60s)
- What-it-does bullets pruned to one clause each
- Agent table notes trimmed to ≤5 words with ● markers
- Pipeline internals + provider slices moved to collapsed <details>
- New When-to-use / When-to-skip section
- GIFs centered via HTML with captions
- Integrations and What's-inside remain collapsed <details>
2026-05-11 22:33:03 -07:00
chopratejas
3432ee3a96 docs: add README redesign spec (lean-ctx parity + Headroom-first blend) 2026-05-11 22:22:34 -07:00
Gili Tzabari
4b061792b2 feat: add lean-ctx context tool support 2026-05-11 17:54:17 -04:00
Tejas Chopra
359e8c9a5b fix: align docs and prune dead compatibility surfaces 2026-05-09 14:07:59 -07:00
Daniel Munoz
9492386398 fix: harden release gating and clarify pipx compatibility 2026-05-06 12:41:17 +02:00
chopratejas
90ef66213d fix(proxy): PR-D3 Bedrock observability + auth-mode integration
Phase D close. Adds the operator-facing observability surface that
PRs D1 (native invoke) and D2 (streaming EventStream) deferred, and
wires the Phase F PR-F1 auth-mode classifier into the Bedrock route
so downstream cache/compression policy gates have something to read.

Changes
-------

* New `bedrock::auth_mode_layer` middleware. Classifies every
  inbound Bedrock request via F1's `classify`, coerces the result
  to `AuthMode::OAuth` per the Bedrock policy matrix (SigV4 IAM is
  OAuth-equivalent), and stores the resolved value in
  `request.extensions()` so PR-F2/F3 can read it without
  re-classifying. Mismatches are logged at WARN with
  `event=bedrock_auth_mode_unexpected` — no silent coercion.

* New `observability` module with three Prometheus families:
    - `bedrock_invoke_count_total{model, region, auth_mode}` (counter)
    - `bedrock_invoke_latency_seconds{model, region}` (histogram)
    - `bedrock_eventstream_message_count_total{model, region, event_type}`
      (counter)
  Registered lazily via `OnceLock` so per-request work is just
  `inc_with_label_values` / `observe`. Latency observed via an
  RAII `LatencyGuard` so every error path is instrumented; a
  future regression that adds a new return path can't drop the
  observation.

* New `GET /metrics` endpoint serves the registry in Prometheus
  text format. Mounted unconditionally — no feature flag gate — so
  scrape works regardless of which provider routes are mounted.

* Bedrock invoke + invoke-streaming handlers now extract
  `Extension<AuthMode>`, log it in their entry breadcrumbs
  (`event=bedrock_invoke_received`, `event=bedrock_invoke_streaming_received`),
  and pass `model`/`region` into `translate_stream` so per-message
  metrics carry the right labels.

* Operator docs at `docs/bedrock.md`: AWS credential chain,
  region/endpoint config, supported model IDs (`anthropic.*`
  literal-match — no regexes), compression behaviour, sample
  PromQL queries, structured-log correlation, rollback path.

Tests added (6, all green)
--------------------------

Auth-mode (`integration_bedrock_authmode.rs`):
  1. `bedrock_classified_as_oauth` — empty headers → OAuth in
     extensions.
  2. `oauth_policy_passthrough_prefer` — body byte-equal upstream;
     no auto cache_control / prompt_cache_key injected.

Metrics (`integration_bedrock_metrics.rs`):
  3. `metrics_increment_per_invoke` — 3 invokes → counter=3 with
     correct labels.
  4. `metrics_observe_latency` — 1 invoke → histogram count=1,
     sum>0.
  5. `eventstream_metrics_per_message_type` — 5 chunks → counter=5
     with `event_type=chunk`.
  6. `metrics_endpoint_serves_scrape` — `/metrics` returns 200,
     `text/plain`, all three metric families' HELP/TYPE lines
     present.

Each metrics test owns a unique (model, region) tuple so the
global `prometheus` registry — shared across parallel tests in
the same binary — gives each test isolated label rows. Without
isolation, parallel tests cross-contaminate counters.

Constraints honoured
--------------------

* No silent fallbacks — auth-mode coercion is logged at WARN.
* No hardcodes — region from `--bedrock-region`, model from axum
  path parameter.
* No regexes — vendor prefix is literal `anthropic.`.
* Comprehensive structured logs — every metric increment paired
  with `tracing::debug!` carrying the same labels for incident
  correlation.
* Performant — `OnceLock`-cached descriptors, RAII guard, total
  D3 overhead well under 1us per request.
* Cardinality bounded — labels driven by config + bounded enums,
  never by user-controlled bytes.

Live cloud validation deferred
------------------------------

The wiremock-backed integration tests are the canonical correctness
gate for D3. A real Bedrock smoke test requires `bedrock:InvokeModel`
permissions in the developer's AWS account and is documented in
`docs/bedrock.md` — both D1 and D2 hit sandbox permission issues
trying this path; D3 follows the same convention.

Stacked on
----------

PR #364 (D1 native invoke), PR #365 (D2 streaming EventStream),
PR #366 (F1 classifier helper). Merge those first; this PR will be
rebased onto main once they land.
2026-05-04 11:07:47 -07:00
chopratejas
ca9de93cfc fix: PR-F1 classify_auth_mode helper (Phase F kickoff)
Add the classify_auth_mode helper that maps inbound request headers to
one of three auth modes — Payg / OAuth / Subscription — at request
entry. The mode is the first-class policy axis Phase F's remaining PRs
(F2 cache+lossy gates, F3 TOIN per-tenant aggregation, F4
X-Forwarded-* skip) gate behavior on.

Detection rules (most-specific signal wins):
- Subscription UA prefix in user-agent → Subscription
- Bearer sk-ant-oat-* → OAuth (Claude Pro/Max)
- Bearer sk-ant-api* / Bearer sk-* → Payg
- Bearer <jwt> (3 dot-segments) → OAuth (Codex/Cursor/Copilot)
- Authorization present but not Bearer (AWS SigV4) → OAuth (Bedrock)
- x-api-key / x-goog-api-key → Payg
- Default → Payg

Hard constraints met: pure function, no regex, no silent fallback
(non-UTF-8 headers warn! and fall through), no hardcoded list (UA
prefixes in module-scope const ready to swap for config in a follow-up).

Files:
- crates/headroom-core/src/auth_mode.rs (new) — Rust impl
- crates/headroom-core/tests/auth_mode.rs (new) — 14 unit + 1 perf
- crates/headroom-core/benches/auth_mode.rs (new) — Criterion bench
- crates/headroom-core/Cargo.toml — add http dep + bench entry
- crates/headroom-core/src/lib.rs — pub mod auth_mode
- crates/headroom-proxy/src/proxy.rs — classify at request entry,
  store in extensions, log event=auth_mode_classified
- headroom/proxy/auth_mode.py (new) — Python port (parity)
- headroom/proxy/handlers/anthropic.py — wire into messages handler
- headroom/proxy/handlers/openai.py — wire into chat + responses
- tests/test_auth_mode.py (new) — 23 Python parity tests
- docs/auth-modes.md (new) — detection rules + how-to-extend

Tests: 15 Rust + 23 Python all green. cargo fmt + clippy + workspace
tests + ci-precheck all green.

Performance (criterion, M-series):
- auth_mode/classify/empty: 68 ns
- auth_mode/classify/payg_anthropic_api_key: 75 ns
- auth_mode/classify/oauth_jwt: 182 ns
- auth_mode/classify/subscription_claude_code: 81 ns

All paths well under the <10us budget (~50-150x headroom).

Refs: REALIGNMENT/08-phase-F-auth-mode.md PR-F1.
2026-05-03 17:20:14 -07:00
chopratejas
2e874c5e3e fix: A5 — strip x-headroom-* from upstream-bound headers (P5-49)
Eliminate P5-49: every Python forwarder and the Rust transparent proxy
now drop internal `x-headroom-*` request headers (`x-headroom-bypass`,
`x-headroom-mode`, `x-headroom-user-id`, `x-headroom-stack`,
`x-headroom-base-url`) before the upstream call. Stops fingerprinting
of the proxy by subscription-revocation enforcers and prevents leakage
of internal user-id / stack / base-url internals to whichever vendor
terminates the request.

Python:
- `_strip_internal_headers(headers)` in `headroom/proxy/helpers.py`
  returns a NEW dict with `x-headroom-*` keys removed (case-insensitive
  prefix match, no regex). Pure function. Operator opt-in
  `HEADROOM_STRIP_INTERNAL_HEADERS=disabled` keeps internal headers in
  the upstream-bound dict for diagnostic shadow tracing — explicit, not
  a fallback.
- Strip applied at every handler entry capture in `anthropic.py`,
  `openai.py`, `batch.py`, `gemini.py` (chat completions, responses,
  WebSocket handshake, Copilot passthrough, batch passthroughs, Gemini
  generate / stream / countTokens / cloudcode-assist, Anthropic
  passthrough + batch results). Inbound reads of x-headroom (bypass
  gating, memory user-id) migrated to `request.headers.get(...)` so
  they continue working off the original dict.
- `log_outbound_headers` emits `event=outbound_headers forwarder=...
  stripped_count=N request_id=...` per call. Never logs header values.

Rust (crates/headroom-proxy):
- `strip_internal_headers(&mut HeaderMap)` and `is_internal_header`
  helpers in `src/headers.rs`. `build_forward_request_headers` accepts
  a `strip_internal: bool` so the same path serves HTTP and WebSocket.
- `Config::strip_internal_headers: StripInternalHeaders` driven by CLI
  flag `--strip-internal-headers` and env var
  `HEADROOM_PROXY_STRIP_INTERNAL_HEADERS` (default `enabled`).
- `proxy.rs` and `websocket.rs` call `build_forward_request_headers`
  with the resolved policy; structured `tracing::info!` /
  `tracing::warn!` line per request describes the strip decision.

Tests: 24 Python (`tests/test_header_isolation.py`) + 4 Rust
integration (`crates/headroom-proxy/tests/integration_headers.rs`) +
4 Rust unit tests in `headers.rs`. Covers every named header
(`bypass`, `mode`, `user-id`, `stack`, `base-url`), case-insensitive
prefix matching, legitimate-headers passthrough, the `disabled`
operator-opt-in mode, and that the inbound bypass-gating read path
is unaffected by the strip.

Acceptance: targeted `pytest -x` suite green (87 tests across
test_header_isolation, test_proxy_byte_faithful_forwarding,
test_proxy_anthropic_cache_stability, test_proxy_system_prompt_immutable,
test_proxy_openai_cache_stability, test_proxy_pipeline_lifecycle).
`cargo test -p headroom-proxy` green (23 tests across all integrations
plus 7 lib unit tests). `cargo clippy -p headroom-proxy -- -D warnings`
clean. `cargo fmt --all -- --check` clean. `cargo test --workspace`
green (~900 tests total).

Per realignment build constraints: configurable (env + CLI), no
hardcodes, no regex (pure `.lower().starts_with()` match), no silent
fallbacks (`disabled` is loud operator opt-in), structured logs
(`event=outbound_headers`).

Remaining `x-headroom-` references in `headroom/proxy/handlers/` are
inbound-read sites only: `request.headers.get("x-headroom-bypass")` /
`x-headroom-mode` for behavior gating, `request.headers.get
("x-headroom-user-id")` for memory user-id resolution, and `ws_headers
.get(...)` on the WebSocket inbound path. Response-side `X-Headroom-*`
injection (e.g. `x-headroom-tokens-saved`) is unrelated to upstream
forwarding and untouched.
2026-05-02 09:35:27 -07:00
chopratejas
f0dcc02775 fix: A3 — byte-faithful Python forwarders; serialize canonical only when mutated
Eliminates P0-2 universally. Every Python forwarder (server.py
`_retry_request`, handlers/streaming.py `_stream_response`,
handlers/openai.py `_ws_http_fallback`, handlers/batch.py `_batch_passthrough`
+ batch-create + Google batch passthrough, handlers/anthropic.py CCR
continuation + batch endpoint) now switches from `httpx ... json=body` to
`httpx ... content=raw_bytes`. The default httpx JSON encoder was
re-serializing every request with `, `/`: ` separators and `\\uXXXX` ASCII
escapes — collapsing Anthropic prompt-cache hit-rate.

Forwarder strategy:
  - unmutated body → forward `await request.body()` verbatim;
  - mutated body  → re-serialize once via the new
    `serialize_body_canonical(body) -> bytes` helper (compact separators,
    `ensure_ascii=False`, dict insertion order preserved).

`HEADROOM_PROXY_PYTHON_FORWARDER_MODE` env var configures the mode:
  - `byte_faithful` (default) — the new behavior;
  - `legacy_json_kwarg` — explicit operator opt-in for emergency rollback.
Documented in `docs/content/docs/configuration.mdx`. NOT a fallback —
unknown values raise loudly per build constraint #4.

`BodyMutationTracker` accompanies each request through the handler so
transform sites mark the tracker (`memory_injection`,
`image_compression`, `compression_*`, `batch_compression`,
`ccr_continuation`, etc.). At forwarder dispatch we additionally compare
the final body dict against the parsed original bytes as a structural
safety net — any silent mutation we missed still triggers canonical
re-serialization.

A2 follow-up: `handlers/openai.py:534-540` (Chat Completions memory
injection) was prepending a system message; replaced with
`append_text_to_latest_user_chat_message`, the OpenAI Chat Completions
analog of `_append_context_to_latest_non_frozen_user_turn`. The cache
hot zone (system messages) is now sacrosanct on /v1/chat/completions
too. Honors `HEADROOM_MEMORY_INJECTION_MODE=disabled`.

Structured logging: every forwarder emits an `event=outbound_request`
log line with `forwarder`, `path`, `body_bytes`, `body_mutated`,
`mutation_reasons`, `source` (passthrough|canonical|legacy),
`request_id`. Never logs Authorization or full body.

`_read_request_json` factored to share `_read_request_body_bytes` with
new `read_request_json_with_bytes` so the anthropic handler can capture
both the parsed dict and the original (decompressed) bytes.

Tests:
  - `tests/test_proxy_byte_faithful_forwarding.py` (28 tests):
    SHA-256 byte-equality on /v1/messages and streaming, unicode
    preservation, numeric precision, mutation-tracker invariants,
    canonical-serializer properties, legacy-mode rollback, OpenAI
    Chat memory routing.
  - Existing test mocks updated to accept the new `**kwargs` on
    `_retry_request` (no behavior change).
  - `tests/test_proxy_handlers_batch.py` updated to read the captured
    `content=` bytes (formerly `json=`).
  - One A2 test corrected (`test_anthropic_tool_sort_and_context_append_helpers`)
    to match the live-zone-tail semantics introduced by A2.

Constraints satisfied: configurable env var; no new regex / hardcodes;
no silent fallback (`legacy_json_kwarg` is operator opt-in);
performant (`prepare_outbound_body_bytes` is O(1) for passthrough);
elegant single-responsibility helpers; structured tracing logs.
2026-05-02 09:02:10 -07:00
chopratejas
4429a11166 Merge remote-tracking branch 'origin/main' into rust-rewrite
# Conflicts:
#	headroom/proxy/server.py
2026-04-25 13:01:37 -07:00
dependabot[bot]
2f659535d2
chore(deps): bump the npm_and_yarn group across 3 directories with 4 updates
Bumps the npm_and_yarn group with 1 update in the /sdk/typescript directory: [esbuild](https://github.com/evanw/esbuild).
Bumps the npm_and_yarn group with 1 update in the /plugins/openclaw directory: [esbuild](https://github.com/evanw/esbuild).
Bumps the npm_and_yarn group with 2 updates in the /docs directory: [postcss](https://github.com/postcss/postcss) and [next](https://github.com/vercel/next.js).


Updates `esbuild` from 0.21.5 to 0.27.4
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG-2024.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.21.5...v0.27.4)

Updates `postcss` from 8.5.8 to 8.5.10
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.5.8...8.5.10)

Updates `vite` from 5.4.21 to 8.0.10
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v8.0.10/packages/vite)

Updates `esbuild` from 0.21.5 to 0.27.4
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG-2024.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.21.5...v0.27.4)

Updates `postcss` from 8.5.8 to 8.5.10
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.5.8...8.5.10)

Updates `vite` from 5.4.21 to 8.0.10
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v8.0.10/packages/vite)

Updates `postcss` from 8.5.8 to 8.5.10
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.5.8...8.5.10)

Updates `next` from 16.2.2 to 16.2.4
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v16.2.2...v16.2.4)

---
updated-dependencies:
- dependency-name: esbuild
  dependency-version: 0.27.4
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: postcss
  dependency-version: 8.5.10
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: vite
  dependency-version: 8.0.10
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: esbuild
  dependency-version: 0.27.4
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: postcss
  dependency-version: 8.5.10
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: vite
  dependency-version: 8.0.10
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: postcss
  dependency-version: 8.5.10
  dependency-type: direct:development
  dependency-group: npm_and_yarn
- dependency-name: next
  dependency-version: 16.2.4
  dependency-type: direct:production
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-24 22:15:53 +00:00
chopratejas
0414cb70e4 feat(rust): scaffold workspace + parity harness (phase-0)
Bootstrap the Rust port of Headroom. Additive only — no existing Python
code modified. Ships the workshop, not the widgets.

Layout
  Cargo.toml (workspace) + rust-toolchain.toml
  crates/headroom-core    — transform library, stub only
  crates/headroom-proxy   — axum binary, /healthz only
  crates/headroom-py      — PyO3 cdylib, exposes headroom._core.hello()
  crates/headroom-parity  — Rust-vs-Python oracle harness + parity-run CLI

Tooling
  Makefile: test, test-parity, bench, build-proxy, build-wheel, fmt, lint
  .github/workflows/rust.yml: test, wheels (linux/mac), audit, parity-nightly
  deny.toml for cargo-deny

Parity corpus
  tests/parity/recorder.py + scripts/record_fixtures.py
  125 recorded fixtures across 5 leaf transforms (ccr, tokenizer,
  log_compressor, diff_compressor, cache_aligner)

Docs
  RUST_DEV.md — developer setup and workspace reference
  docs/spec/022-rust-migration.md — migration plan and stage breakdown

.gitignore: whitelist scripts/record_fixtures.py; ignore target/

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 13:39:48 -07:00
JD Davis
e854ba1771
Merge branch 'main' into fix/python-github-packages-publish 2026-04-21 07:10:17 -05:00
Tejas Chopra
724c2987b3
Merge pull request #221 from chopratejas/docs/port-wiki-pages-to-fumadocs
docs: port Docker-native, filesystem-contract, and persistent-installs pages to Fumadocs
2026-04-20 23:07:40 -07:00
chopratejas
6d250554f7 docs: port Docker-native, filesystem-contract, and persistent-installs pages to Fumadocs
These three wiki pages documented shipped features (PRs #139, #145, #191)
but were never ported into the new Fumadocs site introduced in 911eb85
("new docs UI + ts doc coverage"). Users browsing docs.* couldn't find
the Docker-native install flow, the canonical filesystem contract, or
the persistent-install CLI surface.

- Add docs/content/docs/docker-install.mdx (one-line installer, native
  wrapper behavior, persistent-docker lifecycle, Compose runtime).
- Add docs/content/docs/filesystem-contract.mdx (two-root model,
  precedence, bucket assignments, Docker overlap between
  HEADROOM_WORKSPACE and HEADROOM_WORKSPACE_DIR).
- Add docs/content/docs/persistent-installs.mdx (runtime matrix,
  presets, scopes, health/wrap behavior, Docker-native relationship).
- installation.mdx: add a Callout in the Docker section linking to the
  new docker-install page so pip/npm/docker landing users find it.
- meta.json: surface the three new pages in the sidebar under
  Getting Started and Configuration.

Wiki source files in wiki/ are left in place for now; they can be
deprecated in a follow-up once the new site is confirmed as canonical.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 23:05:15 -07:00
JerrettDavis
f5dfdda253 ci: publish Python distributions to GitHub releases 2026-04-20 23:32:31 -05:00
JerrettDavis
d239e6f41f fix: complete fork-friendly release publishing
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-20 19:00:43 -05:00
Kayzo
6690740218 chore(merge): resolve upstream main conflicts for pi codex branch 2026-04-20 21:50:33 +00:00
JerrettDavis
3ddc7ff33a fix: restore release and compress regressions
Fix workflow validation failures by wiring detect-version outputs into all
release publish jobs, renaming the GitHub Packages skip variable to a
valid Actions variable name, and adjusting the macOS PATH export for
actionlint.

Also make min_tokens_to_compress use token counting instead of whitespace
splits so compact JSON tool outputs still compress after merging the
latest main branch changes, and add a regression test for that path.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-18 16:01:57 -05:00
JerrettDavis
d8c2ae88cd ci: validate release workflows with act
Add a workflow-validation CI job that installs actionlint and act,
checks the release and Docker workflows against checked-in event
fixtures, and shares the same validation script developers can run
locally.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-18 15:38:10 -05:00
JerrettDavis
a36f3e2d3e fix: publish release artifacts and docker together
Call the Docker workflow from the release pipeline so Docker publishes in

the same run, build npm tarballs alongside Python distributions, and

attach those artifacts to the GitHub release page.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-17 21:33:37 -05:00
Tejas Chopra
80cfcd7e5b
Merge pull request #192 from JerrettDavis/feature/spec-183
feat(specs): #183 scaffold initial application specs.
2026-04-17 11:00:33 -07:00
Kayzo
4bdc1482af feat: add Pi/Codex and Cloud Code Assist compatibility routes
- Adds /v1/codex/responses aliases for OpenAI Codex clients configured with /v1 base URLs
- Uses JWT-derived account routing for /v1/responses/* subpaths (compact, cancel, etc)
- Adds /v1internal:streamGenerateContent aliases for Cloud Code Assist / Antigravity
- Preserves upstream HTTP error status/body in StreamingResponse (fixes empty SSE drops)
2026-04-17 17:45:08 +00:00
JerrettDavis
bde7aa9c30 fix: align docker image versions with releases
Derive the exact Docker image version from the release tag or manual
workflow input, sync versioned files in the build workspace before the
image build, and publish an explicit matching image tag.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-17 12:17:49 -05:00
JerrettDavis
8872b8be3b fix: take highest release bump across unreleased commits
Determine the release bump from all unreleased commits since the previous
release tag and apply the highest required semantic version increment.
This keeps feat commits at a minor bump unless a breaking change requires
major, even when later patch-level commits are present.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-16 23:38:29 -05:00