mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-27 14:17:10 -04:00
232 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
f624d3a00a
|
perf(proxy): bound upstream calls and hot-path costs (#2852)
Seven commits from one week of load testing: one hang, two request-path correctness fixes, and four hot-path costs that only show up in production. ## Reliability **Bound every upstream call.** The litellm backend had no timeout at all, so a request the upstream never answered blocked its caller forever. Observed under load on 2026-08-07: four agent workers on ESTABLISHED connections for 36+ minutes while `/readyz` answered in 0.11s. No error, no retry, no log line — indistinguishable from slow work, which is the worst shape a failure can take. A float rather than an `httpx.Timeout`, deliberately: litellm expands a float across all four httpx phases, so on a streaming call it becomes the maximum gap *between chunks*, not a cap on total generation. A long answer streaming steadily is never cut off; a stalled one dies. Default 600s via `HEADROOM_UPSTREAM_TIMEOUT`; 0, negative, and junk fall back to the default rather than meaning "no timeout". **Keep the consistency re-count off the event loop.** It ran `tokenizer.count_messages` twice directly on the loop. Since Claude counting moved to a real BPE that is CPU-bound work stalling every other in-flight request — ~1s on a 2.3 MB body, with `/healthz` gaps tracking body size. Offloaded via `asyncio.to_thread` on the same tokenizer instance, so reported values are unchanged. (#2810) **Survive a re-parse MemoryError.** `MemoryError` is not a `ValueError`, so on 1M-context payloads the byte-faithful forwarder's verification re-parse escaped the handler and aborted an otherwise-fine request — 14 aborts across 8 days of reporter logs. (#2768) ## Performance All four are measured, not guessed. Each degrades with something a short benchmark does not vary: uptime, content shape, or process age. | fix | before | after | |---|---|---| | Cost-record walk per request (at 100k records) | 13.6 ms | bounded by model count | | JSON-block scan, JS-style object logs (1200 lines) | 4643 ms | 183 ms | | JSON-block scan, truncated JSONL | 3737 ms | 116 ms | | Lazy imports inside user requests | multi-second | paid at startup | | `count_text` (80% of local CPU) | — | memoised | Two worth calling out: - **The cost walk degrades with proxy *uptime*, not load.** A freshly started proxy pays ~0.01 ms; a month-old one pays 4–13 ms on every request, on the event loop, holding the metrics lock. Deliberately not a TTL cache over `stats()`: those values feed `check_budget()` when `--budget` is set, and a stale reading under-enforces the budget. The fix is to stop computing what the caller discards. - **The JSON-block memo is built only *after* a scan fails to balance.** That ordering is load-bearing, not an optimisation — caching from the start made pretty-printed JSON ~2x slower, since content that balances on the first scan has nothing to reuse and just pays the per-line dict traffic. Still a constant-factor fix, not an asymptotic one. ## Tests +1202 lines, 20 files. Each fix is pinned by a test that fails on the unmodified code: the re-count test asserts no `count_messages` pass runs with a live event loop in its thread; the re-parse test drives a `MemoryError` through the real request path and expects a 200; `totals()` equality with `stats()` is asserted across model counts, request volumes, and both pricing branches. The timeout test is structural rather than a mock — the failure mode is a dispatch path someone adds later without a guard, which mocking the existing four cannot catch. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
c07da992dd
|
Per-request backend selection for routing extensions (#2809)
## The gap
Headroom picks its egress backend **once**, at startup:
`create_proxy_backend` returns a single `Backend` (or `None` for the
direct Anthropic path) and every request goes through it. That is the
right shape for *"run this whole proxy against Bedrock instead of
Anthropic"* and the wrong shape for *"this request is cheaper on a
different provider than the last one."*
`ModelRouter` already lets an extension change `body["model"]` per
request — but only within the protocol the request arrived in, because a
model id alone cannot move a request to another provider.
So an extension can currently **decide** something Headroom has no way
to **carry out**. This adds the missing half.
## The seam
An extension publishes a decision on the request state:
```python
request.state.headroom_route = SimpleNamespace(
model="moonshot/kimi-k2", # required
provider="moonshot", # optional; inferred from the model id if absent
reason="cheaper at this prefix length",
)
```
Headroom resolves a `LiteLLMBackend` for that provider — which is where
translation already lives — and serves **that one request** from it.
Nothing in core names any particular extension; the field is duck-typed,
so an extension does not import Headroom to talk to Headroom.
## Absent means unchanged
This is the property the tests are built around, and the reason this
should be safe to merge.
With nothing published, every path is what it was before. Advice that is
**absent, malformed, names an unknown provider, names a native provider,
or fails to build** all resolve to `self.anthropic_backend` — including
when that is `None`, which is the direct-API path and must survive. A
routing preference can never take traffic down.
## Coverage
| path | |
|---|---|
| `/v1/messages` | non-streaming + streaming |
| `/v1/chat/completions` | non-streaming + streaming |
| Responses API | untouched — does not use the backend abstraction |
Streaming is the one that matters. The resolver rewrites
`body["model"]`, so had `_stream_response_bedrock` kept reading
`self.anthropic_backend`, every streamed routed request would have sent
a foreign model id to Anthropic. Both streaming helpers now take an
optional `backend`, defaulting to the configured one.
## Details worth review
- **Validate the provider name before building.** `LiteLLMBackend`
accepts *any* provider string — the registry falls through to a generic
pass-through config — so a typo silently builds a backend that only
fails later, at request time, with an error pointing nowhere near the
typo. `_known_provider()` checks against `litellm.provider_list` first.
- **Cache per provider, and cache the failures too**, or a broken
provider name costs a construction attempt on every request. (Bedrock
construction calls out to AWS to enumerate inference profiles — it is
not free.)
- **`backend_owns_translation` now asks the per-request backend.** It
decides whether Headroom or the backend owns the `max_tokens` /
`max_completion_tokens` spelling; asking `self.anthropic_backend` would
answer "Headroom does" for a request about to be served by a translating
backend that does.
- **`_route_resolver` lives in `route_advice.py`, not on a handler
mixin.** Two mixins need it, and reaching across sibling mixins only
works by accident of how `HeadroomProxy` composes them.
## Tests
`tests/test_route_advice.py` — 20 tests, most of them asserting the
absent-means-unchanged property from a different angle.
Local runs: 20/20 on the new file; **1102 passed, 1 failed** on `-k
"openai or chat_completions or ccr"`, and **414 passed, 0 failed** on
`-k "stream or bedrock or route_advice"`. The single failure is
`test_realignment_live_multi_turn::test_ccr_marker_round_trip_live`,
which fails identically on this branch's merge-base — verified by
checking out `59314cff~1` and re-running it.
Note for anyone reproducing: `pytest-asyncio` is a declared dev
dependency but was missing from my venv, which made every `async def
test_` in the repo fail. Worth checking before diagnosing a large
failure count.
## Docs
`docs/content/docs/pipeline-extensions.mdx` gains a section on the
contract, next to the existing `x-headroom-base-url` one.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
0237cbffbb
|
fix(proxy): enable tool search by default and repair poisoned transcripts (#2807)
## Description Server-side tool search poisons the Claude Code transcript: once the proxy injects deferral and the model runs one search, Anthropic's `server_tool_use` + `tool_search_tool_result` pair lives in the message history forever. Upstream validates **every `tool_reference` in that history against the *current* request's `tools` array** — and Claude Code replays one transcript across requests with wildly different tools arrays (main loop: hundreds of tools; prompt-type Stop hook evaluator, `/compact`, other side-requests: a handful). Every one of those side-requests 400s with `Tool reference 'X' not found in available tools`. This PR keeps tool search **on** — it's the whole point of the feature, and the default `coding` savings profile already turned it on at proxy startup — and instead repairs the transcript per request, statelessly. The issue author's preferred fix (never inject for Claude Code clients) would disable the feature for its main audience. A session-sticky approach was also considered and rejected: it needs session state, it can't re-add ~500 tool definitions to a 5-tool side-request without erasing the savings, and it can't heal transcripts already poisoned before the upgrade. Closes #2805 ## 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 - **`headroom/proxy/helpers.py`** — new `strip_unsupported_tool_search_blocks(messages, tools)`. Builds the set of names this request can resolve, drops any `tool_search_tool_result` whose `tool_reference` entries aren't all resolvable (or when no search tool is present at all), and drops the paired `server_tool_use` by `tool_use_id`. Other server tools (`web_search`, code execution) are untouched. Turns left with zero content blocks are removed rather than forwarded empty. Copy-on-write: returns the **original** `messages` object by identity when nothing was removed. - **`headroom/proxy/handlers/anthropic.py`** — runs the repair right after the injection block, so the tool just injected counts as present and the main loop is a no-op with a byte-identical prefix. Deliberately **not** gated on `HEADROOM_TOOL_SEARCH`, so transcripts poisoned before an upgrade (or before someone sets the flag to `0`) still recover. Logs and tags `router:tool_search_repair:Nblocks` when it fires. - **`headroom/proxy/handlers/anthropic.py`** — `HEADROOM_TOOL_SEARCH` now defaults to `1`. This matches the posture `seed_proxy_env_defaults()` already established for the default `coding` profile; the flip only affects entry points that never seeded. - **`docs/content/docs/proxy.mdx`** — documents on-by-default plus `HEADROOM_TOOL_SEARCH=0` as the opt-out. - **`tests/test_issue_746_tool_search.py`** — 6 tests covering the repair. ### Answering the issue's open question > we could not determine what enables it — `/proc/<pid>/environ` shows no `HEADROOM_TOOL_SEARCH` `seed_proxy_env_defaults()` calls `os.environ.setdefault("HEADROOM_TOOL_SEARCH", "1")` at proxy startup because the default savings profile is `coding`, which has `tool_search=True` (`headroom/agent_savings.py`). In-process mutation of `os.environ` never appears in the process's environ snapshot, which is why the flag looked unset. ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [x] Type checking passes (`mypy headroom`) - [x] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text $ python -m pytest tests/test_issue_746_tool_search.py -q 45 passed, 1 warning in 1.56s $ python -m pytest tests/test_*anthropic*.py tests/test_*tool*.py -q 4 failed, 459 passed, 2 skipped, 7 warnings in 27.40s # the 4 failures are in tests/test_bedrock_tool_result_cache_and_streaming_stats.py # and reproduce identically on this branch's merge-base with the changes stashed: # 4 failed, 9 passed, 5 warnings in 3.02s $ ruff check headroom/proxy/helpers.py headroom/proxy/handlers/anthropic.py tests/test_issue_746_tool_search.py All checks passed! $ ruff format --check <same three files> 3 files already formatted $ mypy --python-version 3.12 headroom/proxy/helpers.py headroom/proxy/handlers/anthropic.py Success: no issues found in 2 source files # --python-version 3.12 only to skip a pre-existing numpy-stub syntax error that # the repo's python_version = "3.10" setting triggers on this machine. ``` New tests: | Test | Asserts | |---|---| | `test_repair_drops_blocks_the_hook_evaluator_cannot_resolve` | small tools array → both blocks dropped, surrounding assistant text survives | | `test_repair_is_noop_on_the_main_loop` | search tool + referenced tool present → `removed == 0` and `messages is transcript` (prefix cache untouched) | | `test_repair_drops_a_turn_left_with_no_blocks` | a turn that was *only* the search round-trip is removed, not forwarded empty | | `test_repair_leaves_other_server_tools_alone` | `web_search` `server_tool_use` blocks survive | | `test_repair_is_idempotent` | second pass over a repaired transcript removes nothing | | `test_repair_strips_search_history_when_only_the_tool_is_missing` | references resolvable but no search tool in the array → still stripped | ## Real Behavior Proof - **Environment:** macOS 25.4.0, Python 3.12 venv, live `api.anthropic.com`, `claude-sonnet-4-6`, local proxy on `127.0.0.1:8799` built from this branch. - **Exact command / steps:** one request body — a poisoned transcript (`server_tool_use` + `tool_search_tool_result` referencing `AskUserQuestion`) with a **1-tool** `tools` array (`Read`), exactly the shape a Claude Code side-request replays — sent twice: once straight to `https://api.anthropic.com`, once to the proxy. ```text $ python /tmp/hr-2805-repro.py https://api.anthropic.com HTTP 400 {"type": "invalid_request_error", "message": "Tool reference 'AskUserQuestion' not found in available tools"} $ python /tmp/hr-2805-repro.py http://127.0.0.1:8799 HTTP 200 content: [{"type": "text", "text": "OK"}] ``` - **Observed result:** the exact 400 from the issue reproduces against upstream; the identical body through the proxy returns 200. The proxy's savings event for that request records `before: 133, after: 32, saved: 101` tokens — the two dropped blocks. The one-tool array is below `_TOOL_SEARCH_MIN_TOOLS = 12`, so no injection ran; the repair alone is what made the request valid. - **Not tested:** a full end-to-end Claude Code session with a real Stop hook (the synthetic replay above is the same request shape the hook evaluator produces); non-Anthropic providers, which don't have server-side tool search. ## 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 did **not** edit `CHANGELOG.md` ## Screenshots (if applicable) N/A — proxy-side behavior, covered by the command output above. ## Additional Notes - **Cache cost is zero on the hot path.** The repair only rewrites requests whose transcripts reference tools they don't carry — request families that were 400ing anyway. The main loop takes the identity path and its prefix stays byte-identical. - **Out of scope, spotted while here:** `run-all-plugins.sh` exports `HEADROOM_TOOL_SEARCH_MIN_TOOLS=5`, but nothing in Python reads it — `_TOOL_SEARCH_MIN_TOOLS` is a hardcoded `12`. Worth a follow-up. |
||
|
|
0fd0b996a4
|
deps: bump next from 16.2.10 to 16.3.0 in /docs (#2750)
Bumps [next](https://github.com/vercel/next.js) from 16.2.10 to 16.3.0. <details> <summary>Release notes</summary> <p><em>Sourced from <a href="https://github.com/vercel/next.js/releases">next's releases</a>.</em></p> <blockquote> <h2>v16.3.0</h2> <h3>Core Changes</h3> <ul> <li>Update vendored lodash to 4.17.23 to fix CVE-2025-13465: <a href="https://redirect.github.com/vercel/next.js/issues/91558">#91558</a></li> <li>Fix invalid HTML response for route-level RSC requests in deployment adapter: <a href="https://redirect.github.com/vercel/next.js/issues/91541">#91541</a></li> <li>Normalize encoded dynamic placeholders in app routes: <a href="https://redirect.github.com/vercel/next.js/issues/91603">#91603</a></li> <li>Fix(pages-router): restore Content-Length and ETag for /_next/data/ JSON responses: <a href="https://redirect.github.com/vercel/next.js/issues/90304">#90304</a></li> <li>Update tokio from 1.43.0 to 1.47.3: <a href="https://redirect.github.com/vercel/next.js/issues/90945">#90945</a></li> <li>[turbopack] Simplify snapshotting logic: <a href="https://redirect.github.com/vercel/next.js/issues/91178">#91178</a></li> <li>Turbopack: enable server HMR for app route handlers: <a href="https://redirect.github.com/vercel/next.js/issues/91466">#91466</a></li> <li>turbo-tasks-backend: batch find_and_schedule_dirty using for_each_task_meta: <a href="https://redirect.github.com/vercel/next.js/issues/91497">#91497</a></li> <li>[turbopack] Use bail! instead of panic! for duplicate module ident error: <a href="https://redirect.github.com/vercel/next.js/issues/91636">#91636</a></li> <li>Skip loadBindings() Lightning CSS check during next start: <a href="https://redirect.github.com/vercel/next.js/issues/91538">#91538</a></li> <li>turbo-tasks-backend: batch schedule dirty tasks in aggregation_update: <a href="https://redirect.github.com/vercel/next.js/issues/91461">#91461</a></li> <li>Turbopack: Add importModule() support to webpack loaders: <a href="https://redirect.github.com/vercel/next.js/issues/89630">#89630</a></li> <li>turbo-persistence: fix mmap page alignment and improve error context in MetaFile::open_internal: <a href="https://redirect.github.com/vercel/next.js/issues/91640">#91640</a></li> <li>turbopack-css: demote recoverable CSS parse warnings to Warning severity: <a href="https://redirect.github.com/vercel/next.js/issues/91524">#91524</a></li> <li>feat(node-streams): add config flag, define-env, and env precedence test: <a href="https://redirect.github.com/vercel/next.js/issues/90427">#90427</a></li> <li>Rename /_next/webpack-hmr to /_next/hmr: <a href="https://redirect.github.com/vercel/next.js/issues/91415">#91415</a></li> <li>Add per-slot error attribution for instant validation using slot markers and config depth preference: <a href="https://redirect.github.com/vercel/next.js/issues/91610">#91610</a></li> <li>Handle encoded params further: <a href="https://redirect.github.com/vercel/next.js/issues/91627">#91627</a></li> <li>[turbopack] Respect <code>{eval:true}</code> in worker_threads constructors: <a href="https://redirect.github.com/vercel/next.js/issues/91666">#91666</a></li> <li>Fix missing route in otel spans without base-server: <a href="https://redirect.github.com/vercel/next.js/issues/91665">#91665</a></li> <li>[turbopack] Optimize compaction cpu usage: <a href="https://redirect.github.com/vercel/next.js/issues/91468">#91468</a></li> <li>Fix layout segment optimization: move app-page imports to server-utility transition: <a href="https://redirect.github.com/vercel/next.js/issues/91701">#91701</a></li> <li>Fix server actions in standalone mode with <code>cacheComponents</code>: <a href="https://redirect.github.com/vercel/next.js/issues/91711">#91711</a></li> <li>turbo-persistence: remove Unmergeable mmap advice: <a href="https://redirect.github.com/vercel/next.js/issues/91713">#91713</a></li> <li>turbopack: move "compact database" tracing span to backend layer: <a href="https://redirect.github.com/vercel/next.js/issues/91693">#91693</a></li> <li>Turbopack: lazy require metadata and handle TLA: <a href="https://redirect.github.com/vercel/next.js/issues/91705">#91705</a></li> <li>Fix adapter outputs for dynamic metadata routes: <a href="https://redirect.github.com/vercel/next.js/issues/91680">#91680</a></li> <li>Turbopack: fix webpack loader runner layer: <a href="https://redirect.github.com/vercel/next.js/issues/91727">#91727</a></li> <li>[turbopack] Remove incorrect debug_assert in try_read_task_cell: <a href="https://redirect.github.com/vercel/next.js/issues/91699">#91699</a></li> <li>Add module count field to module graph tracing spans: <a href="https://redirect.github.com/vercel/next.js/issues/91697">#91697</a></li> <li>turbopack-cli: add --persistent-caching flag for filesystem-backed cache: <a href="https://redirect.github.com/vercel/next.js/issues/91657">#91657</a></li> <li>Turbopack: pull in updated vercel/nft tests: <a href="https://redirect.github.com/vercel/next.js/issues/91651">#91651</a></li> <li>[turbopack] Improve regressed build speed on cross-compiled MUSL: <a href="https://redirect.github.com/vercel/next.js/issues/91477">#91477</a></li> <li>[Segment Bundling] [Scaffolding] Ensure inlining hint correctness: <a href="https://redirect.github.com/vercel/next.js/issues/91320">#91320</a></li> <li>[Segment Bundling] [Scaffolding] Track which segments can be omitted from prefetch: <a href="https://redirect.github.com/vercel/next.js/issues/91438">#91438</a></li> <li>Avoid deprecated TS node10 moduleResolution defaults: <a href="https://redirect.github.com/vercel/next.js/issues/91847">#91847</a></li> <li>[turbopack] Rebuild the docker build scripts: <a href="https://redirect.github.com/vercel/next.js/issues/91799">#91799</a></li> <li>Fix TS6 baseUrl deprecation for extended tsconfig: <a href="https://redirect.github.com/vercel/next.js/issues/91855">#91855</a></li> <li>Add <code>next internal post-build</code> CLI command for Turbopack database compaction: <a href="https://redirect.github.com/vercel/next.js/issues/91336">#91336</a></li> <li>Turbopack: Define <code>Effect</code> as a trait instead of a closure: <a href="https://redirect.github.com/vercel/next.js/issues/89080">#89080</a></li> <li>Turbopack: Implement TraceRawVcs and NonLocalValue correctly for Effects: <a href="https://redirect.github.com/vercel/next.js/issues/89133">#89133</a></li> <li>turbo-tasks-backend: improve print_cache_item_size instrumentation: <a href="https://redirect.github.com/vercel/next.js/issues/91742">#91742</a></li> <li>Turbopack: switch from base40 to base38 hash encoding (remove ~ and . from charset): <a href="https://redirect.github.com/vercel/next.js/issues/91832">#91832</a></li> <li>Use charCodeAt for normalizePathTrailingSlash: <a href="https://redirect.github.com/vercel/next.js/issues/91380">#91380</a></li> <li>Turbopack: Only patch lockfile when bindings fails to load: <a href="https://redirect.github.com/vercel/next.js/issues/91379">#91379</a></li> <li>[create-next-app] Skip interactive prompts when CLI flags are provided: <a href="https://redirect.github.com/vercel/next.js/issues/91840">#91840</a></li> <li>[devtools] Make instant navs panel draggable: <a href="https://redirect.github.com/vercel/next.js/issues/91914">#91914</a></li> <li>[Segment Bundling] Bundle static prefetches based on size: <a href="https://redirect.github.com/vercel/next.js/issues/91439">#91439</a></li> </ul> <!-- raw HTML omitted --> </blockquote> <p>... (truncated)</p> </details> <details> <summary>Commits</summary> <ul> <li><a href=" |
||
|
|
56ee57be98
|
deps: bump brace-expansion from 5.0.7 to 5.0.9 in /docs (#2751)
Bumps [brace-expansion](https://github.com/juliangruber/brace-expansion) from 5.0.7 to 5.0.9. <details> <summary>Commits</summary> <ul> <li><a href=" |
||
|
|
6b63b623e0
|
docs(metrics): document OTLP metric export and Dynatrace ingest (#2785)
## Description
The proxy can already push its counters to any OTLP/HTTP endpoint via
`HEADROOM_OTEL_METRICS_*`, but the docs site only surfaced this as a
single row in the proxy env table (`proxy.mdx:287`). The endpoint,
header, service-name, and resource-attribute variables were documented
only in `wiki/metrics.md` — so an operator reading the Vercel docs had
no way to wire Headroom into their existing observability stack.
This adds that section, plus a Dynatrace subsection, because Dynatrace
has a silent failure mode that costs an afternoon to diagnose.
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
- `docs/content/docs/metrics.mdx` — new `### OpenTelemetry (OTLP)
Export` section after the Prometheus section: the
`headroom-ai[proxy,otel]` install, all seven `HEADROOM_OTEL_*` variables
in a table, the exported counter names (`headroom.proxy.tokens.saved` et
al.), the `curl /stats | jq .otel` verification, and the note that an
app-managed global meter provider is recorded into automatically.
- `docs/content/docs/metrics.mdx` — new `### Dynatrace` subsection:
copy-paste env block, `metrics.ingest` token scope, a `warn` Callout on
the delta-temporality requirement, the ActiveGate URL variant, the
Collector + `cumulativetodelta` alternative, and one paragraph
explaining that trace export needs `opentelemetry-instrument`
(Headroom's self-configured tracing targets Langfuse only).
- `docs/content/docs/proxy.mdx` — the `HEADROOM_OTEL_METRICS_ENABLED`
row now links to `/docs/metrics#opentelemetry-otlp-export`.
No code, config, or nav changes — the Observability nav slot already
points at `metrics.mdx`.
## Testing
- [ ] Unit tests pass (`pytest`)
- [ ] Linting passes (`ruff check .`)
- [ ] Type checking passes (`mypy headroom`)
- [ ] New tests added for new functionality
- [x] Manual testing performed
Docs-only change: no Python touched, so pytest/ruff/mypy have nothing to
cover here. `next build` was **not** run — `docs/node_modules` is absent
in this checkout, which would require a full `npm install`; Vercel's
preview build is the real gate. In its place I verified the MDX cannot
break the build by parsing for the two things that actually fail MDX v3
— unbalanced JSX and bare `<`/`{` in prose.
### Test Output
```text
$ python - <<'PY' # strip fenced + inline code, then scan prose for MDX hazards
...
PY
hazards: [(80, '<Tabs groupId="lang" items={[\'TypeScript\', \'Python\']}>'),
(125, '<Tabs groupId="lang" items={[\'Python\', \'Proxy\']}>')]
Callout balance: 1 open / 1 close
```
Both flagged lines are pre-existing `<Tabs>` JSX expressions, untouched
by this PR. The added prose introduces no bare `<` or `{` (every
`<env-id>` / `<activegate>` placeholder sits inside a code fence or
inline backticks). `type="warn"` is already used on three other pages,
and the anchor `#opentelemetry-otlp-export` matches the GitHub-slugger
form of the new heading.
## Real Behavior Proof
- **Environment:** macOS (darwin 25.4.0), repo `.venv`,
`opentelemetry-sdk` 1.44.0, `opentelemetry-exporter-otlp-proto-http`,
headroom @
|
||
|
|
3c10e8ff00
|
chore(docs): one documentation site, not two (#2784)
## Description The repo published **two** documentation sites from two source trees: ``` docs/ -> Next.js/Fumadocs -> headroom-docs.vercel.app <- canonical wiki/ -> MkDocs -> gh-pages branch -> github.io/headroom <- orphan ``` The Vercel site is what the README badge and **every** README deep link point at, and what `pyproject.toml` names as both `Homepage` and `Documentation`. The Pages site is referenced from **nowhere** in the repo — not README, not `pyproject`, not `CLAUDE.md`, not any docs page. I grepped for `github.io` and `gh-pages` across all of them and got zero hits. So it was costing work and causing breakage while nobody was reading it: - **Every documented change had to be written twice.** This session I wrote the same configuration content into `docs/content/docs/configuration.mdx` *and* `wiki/configuration.md`. That's the tax, and it compounds silently — the two drift and no one notices which is stale. - **It broke the Vercel deployment.** Each Pages deploy runs `mkdocs gh-deploy --force`, force-pushing `gh-pages`. Vercel's Git integration then tries to build that branch with Root Directory `docs`, which fails: *"The specified Root Directory `docs` does not exist"* — because `gh-pages` holds only the rendered site (`.nojekyll`, `404.html`, …). Timing was exact: ```text 23:06:44 main |
||
|
|
13a310a00d
|
feat(claude): support Claude Code in VS Code (#2752)
## Description Add first-class Headroom support for the official Claude Code extension in VS Code. The new wrapper starts the local proxy, configures the Claude Code user settings consumed by the embedded extension process, preserves authentication and model selection, and provides a conflict-safe reversible unwrap lifecycle. 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) - [x] Documentation update - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - Add `headroom wrap vscode-claude` and `headroom unwrap vscode-claude`. - Configure project-scoped `ANTHROPIC_BASE_URL` plus `ENABLE_TOOL_SEARCH=true` in Claude Code user settings while preserving existing values. - Respect `CLAUDE_CONFIG_DIR`, macOS/Linux home paths, Windows `USERPROFILE`, custom `--settings-file`, and `--no-configure`. - Add durable Headroom-owned restore state and refuse malformed settings or conflicting user edits. - Add unit, CLI, and Docker-harness e2e coverage for configuration, real proxy forwarding, and restoration. - Document setup, remote development, undo, and troubleshooting. ## 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_NO_SYNC=1 uv run pytest -q tests/test_provider_claude_vscode_config.py tests/test_cli/test_wrap_vscode_claude.py tests/test_cli/test_wrap_vscode.py tests/test_cli/test_wrap_claude_base_url.py tests/test_provider_copilot_vscode_config.py tests/test_copilot_auth.py 160 passed in 0.45s $ UV_NO_SYNC=1 uv run ruff check . All checks passed! $ UV_NO_SYNC=1 uv run mypy headroom Success: no issues found in 512 source files $ npm run build # from docs/ Compiled successfully; generated 155 static pages ``` ## Real Behavior Proof - Environment: macOS, Python 3.13 editable install, isolated temporary HOME and Claude settings, local mock Anthropic Messages upstream. - Exact command / steps: invoked the new `verify_vscode_claude_wrap` e2e function, which launched real `headroom wrap vscode-claude`, waited for proxy readiness, POSTed an Anthropic `/v1/messages` request through the generated project-scoped URL, stopped the wrapper, then ran `headroom unwrap vscode-claude`. - Observed result: HTTP 200 with the mock Claude response through Headroom; generated settings retained unrelated values and enabled tool deferral; unwrap restored the original Claude settings. - Not tested: real Anthropic account traffic or the full Docker image locally because Docker Desktop was unavailable. The same e2e function is wired into the existing Docker wrap CI job. ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Checklist - [x] My code follows the project 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 did **not** edit `CHANGELOG.md` — it is generated by release-please from my Conventional Commit PR title (a CI guard enforces this) ## Screenshots (if applicable) Not applicable; this adds CLI configuration and proxy routing without changing VS Code UI. ## Additional Notes The wrapper deliberately leaves the endpoint configured when stopped so requests fail closed instead of silently bypassing Headroom. `headroom unwrap vscode-claude` restores the exact prior managed values and preserves unrelated settings. --------- Co-authored-by: JD Davis <jd@jds-macbook-air.tail2a279.ts.net> |
||
|
|
6422a80a58
|
fix(compress): resolve the /v1/compress tokenizer per model, and document the real contract (#2743)
## Description
`/v1/compress` does no format conversion — callers send whichever wire
shape they already use — but the pipeline pinned **one provider's token
counter for the whole route**.
`OpenAITokenCounter.count_message` walks list content for `text` and
`image_url` only and has **no else branch**, so Anthropic content blocks
contributed literally zero. A 599-token `tool_result` scored 8. A
request that really removed 235 characters reported `tokens_saved: 0` —
so a caller gating on `tokens_saved > 0` concludes compression is broken
while it is working.
Prompted by a Kong integration question ("do you support the Anthropic
native format?"). The answer is that we already did — we just reported
zeros for it, and the docs said otherwise.
Closes #
## Type of Change
- [x] Bug fix (non-breaking change that fixes an issue)
- [x] Documentation update
## Changes Made
### Tokenizer resolution (no hardcoded lists)
Build the derived pipelines with `provider=None` so `TransformPipeline`
resolves the tokenizer from the **per-model registry**. Every registry
tokenizer derives from `BaseTokenizer`, whose `_count_content_parts`
ends in a serialize-and-count catch-all, which means:
- No block type counts as zero, and there is **no per-provider
block-type list to keep in sync**. An enumerated set was the first thing
I tried and it already missed `mcp_tool_result`,
`web_search_tool_result`, `document`, and `thinking`.
- Gemini / Mistral / DeepSeek / Kimi stop defaulting to a tiktoken count
when the registry already has a calibrated counter for them.
- Gateway aliases matching no vendor pattern still count correctly.
`mode="ccr"` now runs a derived pipeline too, for the same reason —
sharing `openai_pipeline` pinned its provider. Costs that mode its own
cold compression cache; correct metrics win.
### Tokenizer selection stays separate from context-limit resolution
Deliberately not welded together. `model_limit` feeds `context_pressure
-> min_ratio`, so letting a tokenizer decision pick the limit table
changes compression aggressiveness: `gpt-4-32k` answered by the
Anthropic table is **8,192 instead of 32,768**, a 4× under-estimate.
`test_tokenizer_choice_does_not_move_the_context_limit` pins the
independence.
### Docs, rewritten from the code
- **`proxy.mdx`** — the loopback-only default and **404-not-403**
behavior, previously undocumented *anywhere* in `docs/` despite shipping
in #2458 explicitly for gateway sidecars;
`HEADROOM_COMPRESS_ALLOW_REMOTE`; all four request fields; the whole
`config` object including every `mode` value and `frozen_message_count`;
`transforms_summary`; the 400/401/404/503 contract; and the timeout
fail-open shape (`compression_skipped` / `skip_reason`).
- **Corrected "never calls an LLM"** — accurate about *generative*
provider requests, misleading for a sidecar operator. Kompress (a
ModernBERT **encoder**, classification not generation) and Magika run
**in-process**, and `HEADROOM_KOMPRESS_ENDPOINT` offloads inference over
HTTP — **real egress**. Now stated explicitly, with
`HEADROOM_DISABLE_KOMPRESS=1` as the structural-only option.
- **Both wire formats documented as accepted**, and removed
`anthropic-sdk.mdx`'s claim that OpenAI format is "the compression
engine's native format" — the exact misconception that prompted this
work. The SDK's conversion is now framed as an SDK choice, not an API
requirement.
- **`litellm.mdx`** had no mention of the endpoint at all, despite the
code naming LiteLLM's guardrail as its primary consumer. Added the HTTP
deployment path, the `HEADROOM_COMPRESS_ALLOW_REMOTE` requirement, and
why to leave `config.mode` unset.
- **`index.mdx`** printed `compressionRatio * 100` labelled "Saved …%",
so a 77% saving displayed as **23%**. `api-reference.mdx` already
defined it correctly, so the docs contradicted each other.
- `openai-sdk.mdx`, `wiki/proxy.md`, `wiki/typescript-sdk.md` — same
corrections; dropped "any HTTP client", "Cloud", and a CacheAligner
claim (it is detector-only).
## 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
$ .venv/bin/ruff check headroom/ tests/ --exclude headroom/dashboard/templates
All checks passed!
$ .venv/bin/mypy headroom/
Success: no issues found in 511 source files
$ python -m pytest tests/test_compress_route_tokenizer_by_model.py \
tests/test_proxy_compress_endpoint.py tests/test_compress_api.py \
tests/test_platform_stabilization_functional.py tests/test_proxy_eager_preload_bind.py -q
99 passed, 2 warnings in 47.15s
```
Broader sweep (`-k "compress or litellm or gateway or guardrail"`):
**1625 passed, 4 failed** — all 4 pre-existing, verified by stashing
this diff and re-running on clean `main` (2 strands hook tests, 1 codex
WS semaphore-tail timing test, 1 unrelated local WIP test).
## Real Behavior Proof
- **Environment:** macOS 26.4 arm64, Python 3.12.6, repo `.venv`, branch
rebased on `upstream/main`.
**(1) Before → after, same request** (60-line grep payload in an
Anthropic `tool_result`):
| model | before | after |
| --- | --- | --- |
| `claude-sonnet-4-6` | `before=28 saved=0` | `before=1223 saved=58` |
| `bedrock/anthropic.claude-3-5-sonnet` | `saved=0` | `before=1037
saved=59` |
| `my-gateway/big-model` (alias) | `saved=0` | `before=1037 saved=59` |
| `gemini-2.5-pro` | `saved=0` | `before=1036 saved=59` |
| `gpt-4o` + OpenAI shape | `before=1225 saved=58` | `before=1225
saved=58` (unchanged) |
All three `config.mode` values verified for each. Response shape
preserved: `type=tool_result`, `tool_use_id` intact.
**(2) Counter-level root cause**, 6.8 KB body, `count_message()`:
```text
OpenAITokenCounter string-content -> 1406 tool_result block -> 5
registry (BaseTokenizer) claude tool_result=408 thinking=418 mcp_tool_result=421
web_search_tool_result=421 document=422
```
**(3) Every documented behavior asserted against the running app** — 13
checks, all PASS: 400s for missing `messages`/`model`, invalid
`config.mode`, and all four invalid `frozen_message_count` forms; 200
for valid ones; non-dict `config` ignored; bypass and empty-messages
omit `transforms_summary`; success returns exactly the 8 documented
keys.
- **Not tested:** the docs site was not built (`docs/node_modules`
absent) — MDX was checked for balanced `<Callout>` tags only, so a
reviewer with the site running should eyeball rendering. No live
gateway/Kong request; verification is via `TestClient` against the real
ASGI app.
- **Note:** `HEADROOM_DISABLE_KOMPRESS` is read into `ProxyConfig` at
`server.py:4919` and by the CLI, not by `create_app(ProxyConfig(...))`
directly — I confirmed `disable_kompress=True` does reach the derived
pipeline.
## Review Readiness
- [x] I have performed a self-review
- [x] This PR is ready for human review
## Checklist
- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [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 did **not** edit `CHANGELOG.md`
🤖 Generated with [Claude Code](https://claude.com/claude-code)
|
||
|
|
007446c73a
|
feat(copilot): proxy VS Code models transparently (#2687)
## Description Make Headroom a transparent proxy for VS Code GitHub Copilot. Users keep using Copilot's normal model picker—GPT-4.1, Claude Sonnet, Claude Opus, and other models in their entitlement—while Headroom silently forwards the selected model instead of registering or requiring a separate "Headroom" model. This also fixes GitHub's device OAuth exchange by sending form-encoded request bodies, matching the endpoint contract. ## 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 - Add `headroom wrap vscode` to start a Copilot-seeded subscription proxy and safely configure VS Code's shipped Copilot proxy override. - Add `headroom unwrap vscode` for reversible cleanup. - Preserve VS Code's selected model by changing only the proxy URL/auth override; no custom model is registered and no model preference is written. - Support stable VS Code settings locations on macOS, Windows, and Linux, plus `--settings-file` for Insiders, portable, and other installations. - Edit JSONC settings with a marker-owned block while preserving unrelated bytes, comments, ordering, and trailing commas. - Refuse malformed markers, invalid JSONC, or unmanaged existing Copilot overrides instead of overwriting user configuration. - Fix SIGINT cleanup so the managed settings block is removed and normal shutdown exits successfully. - Fix Copilot device OAuth start/poll requests to use `application/x-www-form-urlencoded`. - Add a compatibility matrix, setup/removal flow, credential behavior, remote-development guidance, enterprise notes, troubleshooting, and verification documentation. ## Testing - [ ] 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 $ .venv/bin/pytest -q tests/test_provider_copilot_vscode_config.py tests/test_cli/test_wrap_vscode.py tests/test_cli/test_wrap_helpers.py tests/test_copilot_auth.py tests/test_cli/test_copilot_auth.py tests/test_provider_copilot_wrap.py tests/test_cli/test_wrap_copilot.py tests/test_copilot_provider_label.py tests/test_copilot_subscription_smoke.py 244 passed in 0.59s $ .venv/bin/ruff check <changed Python files and tests> All checks passed! $ .venv/bin/mypy headroom/providers/copilot/vscode.py Success: no issues found in 1 source file $ cd docs && npm run types:check fumadocs-mdx && next typegen && tsc --noEmit # exited 0 $ git diff --check # exited 0 ``` The full 10,179-test suite was also sampled through approximately 83%, but was stopped because of its runtime. It exposed existing failures in `test_recover_codex.py`, `test_wrap_stale_marker.py`, and `test_proxy_health.py`; therefore the broad `pytest`, repository-wide Ruff, and repository-wide mypy boxes are intentionally not checked. ## Real Behavior Proof - Environment: macOS arm64, VS Code 1.131.0, built-in GitHub Copilot 0.59.0, Headroom 0.33.1-dev. - Exact command / steps: 1. Completed `headroom copilot login` with GitHub's device flow. 2. Ran `.venv/bin/headroom wrap vscode --port 8788`. 3. Confirmed VS Code retained its ordinary Copilot model catalog and made `GET /models` through Headroom with `GitHubCopilotChat/0.59.0` and `editor-version: vscode/1.131.0`. 4. Sent native Copilot `/p/headroom/chat/completions` requests through the same endpoint using `gpt-4.1`, `claude-sonnet-4.6`, and `claude-opus-4.7`. - Observed result: - All three completion requests returned HTTP 200. - GPT-4.1 resolved upstream to `gpt-4.1-2025-04-14`; Sonnet and Opus retained their exact selected IDs. - All returned the requested exact marker content. - VS Code's settings contained only the Headroom proxy URL and token auth override—no Headroom model or model-selection setting. - The proxy health endpoint remained ready with `openai_api_url` set to `https://api.githubcopilot.com`. - Not tested: - Physical Windows or Linux hosts (their path/config behavior is covered by unit tests). - WSL, dev containers, SSH remotes, VS Code Insiders, or enterprise Copilot deployments end-to-end. - Every model in the live Copilot catalog. - A fully submitted chat from VS Code's UI automation; the real extension's catalog request and native completion paths were verified separately. ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Checklist - [x] My code follows the project's style guidelines - [x] I have performed a self-review of my code - [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 targeted unit tests pass locally with my changes - [x] I did **not** edit `CHANGELOG.md` — it is generated by release-please from my Conventional Commit PR title (a CI guard enforces this) ## Screenshots (if applicable) Not applicable; this integration intentionally has no separate UI or model entry. ## Additional Notes The integration uses VS Code Copilot's shipped advanced/debug proxy endpoint seam. The managed settings block is deliberately narrow and reversible. Remote extension hosts may need their own reachable proxy/configuration as documented. --------- Co-authored-by: JerrettDavis <2610199+JerrettDavis@users.noreply.github.com> |
||
|
|
01df245252
|
fix(proxy/cost): mark estimated-basis budget records and add an enforcement policy (#2713) (#2725)
## Description
`CostTracker.check_budget()` is a hard spend control — the Anthropic
handler refuses the request with a 429 once the period budget is gone.
The ledger that control reads could not tell a measured dollar from a
guessed one.
When a provider response carries no input-token breakdown,
`record_tokens()` substitutes Headroom's own `tokens_sent` estimate for
the input count so input cost isn't silently dropped from the budget.
That fallback is the right call, but the resulting record was
byte-identical to a provider-measured one: no field, no log line, no
separation in `/stats`. `RequestOutcome.uncached_input_tokens` defaults
to `0`, so any route whose response omits usage lands on this branch in
production. An estimate can drift in either direction, so a budget check
could pass after real spend had already gone over — with nothing saying
the decision rested on an estimate.
This keeps the fallback and makes it visible, then lets operators decide
what an estimate is allowed to do to a hard limit.
Closes #2713
## Type of Change
- [x] Bug fix (non-breaking change that fixes an issue)
## Changes Made
- New `headroom/proxy/budget_basis_policy.py` (pure policy module,
matching the existing `*_policy.py` convention): the
`measured`/`estimated` basis constants, the `count`/`ignore`/`block`
policy values, and `resolve_estimated_basis_policy()` (explicit value →
`HEADROOM_BUDGET_ESTIMATED_BASIS` → `count`; an unknown value warns once
and falls back rather than failing proxy startup).
- `headroom/proxy/cost.py`: ledger entries are now `CostEntry(timestamp,
cost_usd, basis)` instead of a bare tuple; `record_tokens()` marks the
fallback branch `estimated` and logs one WARNING per model (deduped the
same way pricing warnings are, per #2504 — an unguarded warning on this
path fires once per request for a provider that never reports usage);
new `period_cost_breakdown()` and an optional `basis` filter on
`get_period_cost()`; new `budget_denial_detail()` builds the 429 body
where the ledger lives; `check_budget()` honors the policy while keeping
its `(allowed, remaining)` signature.
- `stats()` gains `budget_estimated_basis` (the active policy) and
`budget_basis` (the period split: `total_usd`, `measured_usd`,
`estimated_usd`, `estimated_pct`, `records`, `estimated_records`).
`merge_cost_stats()` already spreads `**cost_stats`, so both reach
`/stats["cost"]` with no extra plumbing.
- Operator knob wired through every config layer:
`ProxyConfig.budget_estimated_basis` (`models.py`), the Click
`--budget-estimated-basis` option with `envvar=` (`cli/proxy.py`), the
argparse `--budget-estimated-basis` flag (`server.py`, `default=None` so
the env var stays reachable), and a `SettingField` in the `Budget` group
(`settings_store.py`).
- `headroom/proxy/handlers/anthropic.py`: the 429 body now comes from
`budget_denial_detail()`, which names how much of the period's spend was
booked from an estimate and distinguishes "you overspent" from "I refuse
to enforce a hard limit on a guess".
- `headroom/cli/doctor.py`: the budget check stays **PASS** and appends
the estimated share (and the policy, when it isn't the default). No new
WARN state — a provider that never reports usage would otherwise sit at
a permanent WARN. Every new read is `.get()` + type-guarded so `doctor`
still works against an older running proxy.
- `docs/content/docs/metrics.mdx`: a "Measured vs Estimated Spend"
subsection with the `/stats` shape and the three policy values.
- Tests: new `tests/test_cost_budget_basis.py` (20 tests) plus 4 new
`doctor` tests; `tests/test_anthropic_pre_upstream_backpressure.py`'s
cost-tracker double gained `budget_denial_detail()` to match the
handler's duck-typed contract.
### Policy values
| `HEADROOM_BUDGET_ESTIMATED_BASIS` | Effect on the hard limit |
|---|---|
| `count` (default) | Unchanged behavior — estimated spend consumes the
budget. |
| `ignore` | Booked and reported, but only measured spend enforces. |
| `block` | Fail closed — refuse rather than enforce a hard limit on a
guess. |
Default enforcement is unchanged. `CHANGELOG.md` is untouched.
## Testing
- [x] Unit tests pass (`pytest`) — every test covering the changed
modules; see `Not tested` for this machine's pre-existing environment
failures
- [x] Linting passes (`ruff check .`)
- [x] Type checking passes (`mypy headroom`) — clean on every file this
PR touches
- [x] New tests added for new functionality
- [x] Manual testing performed
### Test Output
```text
$ python -m pytest tests/test_cost_budget_basis.py tests/test_cost_tracker_counterfactual.py tests/test_cost_pricing_warning_dedup.py -q
31 passed
$ python -m pytest tests/test_cli_doctor.py -q
72 passed
$ python -m pytest tests/test_anthropic_pre_upstream_backpressure.py -q
25 passed
$ python -m pytest tests/test_proxy_settings_endpoints.py tests/test_proxy/test_settings_store.py -q
50 passed
# full suite (see "Not tested" below for the excluded modules and the pre-existing failures)
$ python -m pytest -q
...
tests\test_cost_budget_basis.py .................... [ 25%]
tests\test_cost_pricing_warning_dedup.py ... [ 25%]
tests\test_cost_tracker_counterfactual.py ........ [ 25%]
...
217 failed, 8807 passed, 657 skipped, 5318 warnings, 59 errors in 716.30s (0:11:56)
# same failing files re-run on clean upstream/main with the change stashed -> identical count
$ git stash push -u -- headroom tests docs
$ python -m pytest tests/test_log_compressor.py tests/test_cache/test_client_integration.py \
tests/test_fsutil.py tests/test_savings_ledger.py tests/test_ccr_mcp_http.py \
tests/test_router_registry_dispatch.py tests/test_proxy_savings_history.py \
tests/test_text_compressors.py tests/test_builtin_compressor_adapters.py \
tests/test_cli_proxy_env.py -q
73 failed, 182 passed in 34.82s # 40+16+2+2+1+1+1+4+3+3 = 73, matching the run above
$ python -m mypy headroom --ignore-missing-imports --python-version 3.13
# 12 errors, all in release_version.py / ccr/mcp_server.py / memory/mcp_server.py
# (stale local `mcp` stubs) — none in any file this PR touches
$ python -m ruff check headroom/proxy/budget_basis_policy.py headroom/proxy/cost.py headroom/proxy/server.py headroom/proxy/models.py headroom/proxy/handlers/anthropic.py headroom/cli/proxy.py headroom/cli/doctor.py headroom/settings_store.py tests/test_cost_budget_basis.py tests/test_cli_doctor.py tests/test_anthropic_pre_upstream_backpressure.py
All checks passed!
$ python -m ruff format --check <same 11 files>
11 files already formatted
```
## Real Behavior Proof
- Environment: Windows 11, Python 3.13.11, branch
`fix/budget-estimated-basis-2713` off `upstream/main` @ `
|
||
|
|
232fb49c73
|
fix(proxy): route Codex Live voice through a dedicated /v1/live transport (#2709)
## Description Codex Live traffic currently reaches an unrouted WebSocket path and receives HTTP 403 before the proxy can contact an upstream. Closes #2653 ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) - [ ] New feature - [ ] Breaking change - [ ] Documentation update - [ ] Performance improvement - [ ] Code refactoring ## Changes Made - Add a dedicated `/v1/live` WebSocket route family and transparent transport. - Preserve subscription auth routing, account headers, origin policy, subprotocols, and text/binary frame bytes. - Propagate WebSocket close metadata and cancel relay tasks deterministically on every exit. - Keep Live outside the Responses parser, compression, memory injection, and Responses beta-header path. - Keep generic HTTP paths on the existing catch-all and document the Live aliases plus the derived-path override. - Add real-app route, relay, and loopback integration proof. - Add coverage for authorization fallback, defensive receive events, and cancellation cleanup in the Live relay. ## Testing The focused Live handshake, preservation suites, Ruff, format, and diff checks pass. The base comparison, Codex Desktop owner round trip, and ChatGPT backend acceptance of the derived `/backend-api/codex/live` path remain untested. - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check`) - [x] Type checking passes (`mypy headroom --ignore-missing-imports`) - [x] New tests added for the reported failure - [x] Manual loopback testing performed ### Test Output ```text uv run pytest tests/test_codex_live.py -q: 6 passed, 7 warnings in 11.03s uv run pytest tests/test_provider_proxy_routes.py tests/test_proxy_codex_route_aliases.py tests/test_provider_codex_endpoints.py tests/test_openai_codex_routing.py -q: 53 passed in 90.54s (0:01:30) uv run ruff check headroom/providers/codex/live.py headroom/providers/proxy_routes.py headroom/proxy/handlers/openai.py headroom/proxy/ws_headers.py tests/test_codex_live.py: All checks passed uv run ruff format --check headroom/providers/codex/live.py headroom/providers/proxy_routes.py headroom/proxy/handlers/openai.py headroom/proxy/ws_headers.py tests/test_codex_live.py: 5 files already formatted uv run mypy headroom --ignore-missing-imports: Success: no issues found in 508 source files git diff --check: pass ``` ## Real Behavior Proof The local WebSocket integration floor uses real uvicorn, a real WebSocket client, and a real loopback WebSocket upstream. The base 403 comparison was not run. Head observes HTTP 101 on every Live alias and a byte-identical binary frame relay. - Environment: Windows, CPython 3.13, the Headroom proxy test environment. - Exact command / steps: run the focused Live test against the local uvicorn proxy and loopback WebSocket upstream, then run the preservation suite listed in `Test Output`. - Observed result: all four Live aliases return HTTP 101, negotiate `codex.live.v1`, preserve text and binary frames, and pass the preservation suite. - Not tested: Codex Desktop Live session; ChatGPT backend acceptance of `/backend-api/codex/live`; the base 403 comparison. ## Review Readiness - Live has a separate transport and does not enter Responses handling. - Existing Responses and generic passthrough suites remain preservation gates. - No `CHANGELOG.md` or install/crate changes are included. - [x] I have performed a self-review - [x] This PR is ready for human review ## Checklist - [x] Closes #2653 - [x] Real loopback handshake and binary-frame proof required - [x] No audio payload logging - [x] No unqualified end-to-end claim ## Screenshots Not applicable. ## Additional Notes The upstream Live path is derived from the repository’s Codex URL formula and remains explicitly unconfirmed until owner evidence is available. |
||
|
|
6d5516dcb8
|
feat(code): add PHP support to CodeAwareCompressor (#2423)
## Description Adds PHP to `CodeAwareCompressor`, fixing #201. PHP was already *detected* as code (Magika labels in `headroom/compression/detector.py` include `php`, and the Rust `magika_detector.rs` lists it too) but there was no PHP `LangConfig`, so PHP content silently passed through uncompressed. This wires PHP through the tree-sitter compression path following the C# pattern (the most recently added, fully functional language — deliberately not the quarantined Perl path). A secondary detection bug is fixed along the way: PHP's `$variables` match Perl's prefilter regex, and the existing Perl-dominance guard in `detect_language` returned `UNKNOWN` for PHP files. An explicit `<?php` open tag — which no Perl source contains — now drops Perl from the candidate set before that guard runs. ## Type of Change - [ ] Bug fix - [x] New feature - [ ] Documentation update - [ ] Refactor - [ ] Other ## Changes Made - `headroom/transforms/code_compressor.py`: `CodeLanguage.PHP` + `phtml`/`php5`/`php7`/`php8` aliases; PHP `LangConfig` built from the actual tree-sitter-php grammar (node names verified by parsing samples): `namespace_use_declaration` imports, `function_definition`/`method_declaration` functions, `class_declaration`/`interface_declaration`/`trait_declaration` classes, `enum_declaration` types, `declaration_list` class bodies, `compound_statement` function bodies. `namespace_definition` maps to `package_node` so statement-scoped `namespace App;` hoists ahead of the `use` imports (required PHP ordering); the rare block-scoped `namespace A { }` form takes the same path and is preserved verbatim — valid output, just no compression inside the block. PHP prefilter regexes added; supported-languages error message updated; `<?php`-tag Perl disambiguation in `detect_language`. - `headroom/transforms/content_detector.py`: `php` entry in `_CODE_PATTERNS` so raw PHP classifies as `SOURCE_CODE` and reaches the code-aware route. - `tests/test_transforms/test_code_compressor.py`: new `TestPhpSupport` mirroring `TestCSharpSupport` — signatures preserved / bodies elided, `<?php` → `namespace` → `use` → declarations ordering, auto-detection despite the Perl sigil overlap, alias coercion, malformed passthrough. - `tests/test_code_compressor_language_alias.py`: `php` in the canonical list, `phtml` in the alias table. - `docs/content/docs/code-compression.mdx`: PHP added to the Tier 2 supported-languages row. No new dependency: `tree-sitter-language-pack` (the existing `[code]` extra) already ships the PHP grammar. No Rust changes needed. ## Testing - [x] New unit tests added and passing - [x] Full affected test suites pass locally **Test Output** ``` $ python -m pytest tests/test_transforms/test_code_compressor.py tests/test_code_compressor_language_alias.py -q ============================= 120 passed in 7.81s ============================= $ python -m pytest tests/test_transforms/ -q 3 failed, 443 passed # the 3 failures (kompress ONNX thread caps, kompress size gate, # text_crusher unicode parity) reproduce identically on a clean # upstream/main checkout in this environment — pre-existing local # ONNX runtime quirks, unrelated to this change $ ruff check . (0.15.17, CI-pinned) → All checks passed! | ruff format --check → clean $ mypy headroom/transforms/code_compressor.py headroom/transforms/content_detector.py --ignore-missing-imports Success: no issues found in 2 source files ``` ## Real Behavior Proof - Environment: Windows 11, Python 3.13, tree-sitter + tree-sitter-language-pack (<1.0) installed, branch `feat/201-php-code-compression` off `upstream/main`. - Exact command / steps: parsed PHP samples (namespaced class w/ methods, block-scoped namespace, mixed HTML+PHP) with `tree_sitter_language_pack.get_parser('php')` to verify every node name used in the config; then ran `CodeAwareCompressor().compress(php_code, language="php")` and `compress(php_code)` (auto-detection) on a 48-line realistic service class. - Observed result: explicit and auto-detected paths both return `language=CodeLanguage.PHP`, `compression_ratio=0.64`, `syntax_valid=True`; method bodies elided to `// [N lines omitted]` while `<?php`, `namespace`, `use` lines, class header, and all signatures are preserved verbatim in the original order. Before the detection fix, auto-detection returned `UNKNOWN` (Perl prefilter dominance) — reproduced and then verified fixed. - Not tested: exotic PHP shapes (heredoc-heavy code, attributes `#[...]` on methods, interleaved multi-`<?php ?>` HTML templates beyond the basic mixed case); these fall back to verbatim preservation via the uncaptured-node pass or malformed-passthrough, both of which are covered by tests for the simple cases. ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: JerrettDavis <mxjerrett@gmail.com> |
||
|
|
e0ce4b1d48
|
fix: remove rtk and lean-ctx CLI context tools (#2677)
## Description Removes both third-party CLI context tools — **rtk** and **lean-ctx** — and with them the context-tool selector itself. Headroom no longer downloads, installs or configures either one, and there is no replacement. The previous pass (#2344) gated only three entry points inside `headroom/cli/wrap.py`. That left the feature reachable in practice: | Gap | Effect | |---|---| | `scripts/install.sh:1544`, `install.ps1:1681` | Ran `rtk init --global --auto-patch` from bash/PowerShell, **bypassing the Python gate entirely** — `curl \| sh` still wrote a Claude Code `PreToolUse` hook regardless of `HEADROOM_RTK` | | `wrap.py` `_setup_context_tool_for_agent` | **`wrap openhands` was broken by default**: `rtk_required=True` met a gate returning `None` → `SystemExit(1)`. Invisible because all 8 openhands tests patched `_ensure_rtk_binary` to a fake path | | `proxy/helpers.py`, `subscription/tracker.py` | Proxy shelled out to `rtk gain` from `/stats`, the dashboard and `headroom perf`; the tracker polled it per contribution (`_RTK_WIRING_DEFAULT = "enabled"`) | | No cleanup path | Nothing removed artifacts an earlier default had installed, so a machine that once ran the old default kept rtk in the loop forever (#1669, #1955) | Also worth noting: the rtk binary download had **no SHA or signature verification** — only `rtk --version` as a smoke test. ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) - [ ] New feature (non-breaking change that adds functionality) - [x] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update - [ ] Performance improvement - [x] Code refactoring (no functional changes) ## Changes Made **Removed** — `headroom/rtk/` and `headroom/lean_ctx/` packages, `headroom/cli/wrap_rtk_metrics.py`, `_selected_context_tool` / `_setup_context_tool_for_agent` / `_VALID_CONTEXT_TOOLS`, the `--rtk` / `--no-rtk` / `--no-project-rtk` / `--keep-rtk` flags across all 18 wrap subcommands, `HEADROOM_RTK*`, the proxy-side `rtk gain` polling, the dashboard CLI-filtering panel (rows + all 8 `cliFiltering*` Alpine getters), `paths.rtk_path()` / `lean_ctx_path()`, the SDK path helpers, `benchmarks/rtk_loop_learn_eval.py`, and the `headroom/rtk/**` CI path filters. **Fails loudly, not silently** — `--context-tool` / `--no-context-tool` / `HEADROOM_CONTEXT_TOOL` are kept solely to error out. They live in shell profiles, aliases and CI jobs, and accepting them as a no-op would read as Headroom having quietly stopped working. The installers reject them too, which matters more than it looks: their arg parsers forward the first unknown flag **and everything after it** to the wrapped tool, so a leftover `--no-rtk` would have silently swallowed a following `--port` and then been ignored downstream. **New `headroom/context_tool_cleanup.py`** — deleting the code cannot help a machine that already ran the old default, since the hooks, binaries and injected guidance are durable on disk. `purge_context_tool_artifacts()` runs once per `wrap`/`unwrap` and removes the registered hook entries, the generated hook scripts, the Headroom-managed `~/.local/bin` symlinks, the vendored `~/.headroom/bin/{rtk,lean-ctx}` binaries, the `lean-ctx` MCP server entry and the marker-fenced instruction blocks. Deliberately conservative: idempotent, **skips** a malformed config rather than overwriting it, and only unlinks a symlink resolving inside Headroom's own bin dir so a user's own build is untouched. It reports on **stderr**, because `wrap/unwrap openclaw --prepare-only` emit machine-readable JSON on stdout as their entire contract. Skipped for `wrap selfheal` (runs from a SessionStart hook; must not race Claude Code's writer for `~/.claude.json`) and for `--help`, which must stay read-only. **Client-config hardening** (discovered while investigating a "corrupted Serena settings file" report) — `wrap.py` reset a settings file to `{}` when an existing file would not parse, then wrote that back. One hand-edited typo or a transient `EACCES`/`EINTR` on a valid file destroyed the user's `permissions`, `env` and `hooks`, on **every `headroom wrap claude`**. It now refuses to write. Separately, `fsutil.write_text` is now atomic (temp file + `fsync` + `os.replace`), fixing all 14 non-atomic client-config writes at once; it follows symlinks rather than replacing them (dotfile managers) and preserves an existing file's mode. **Deliberately kept** — `rtk` stays in the wrapper-peel list in `transforms/content_router.py`. It sits beside `sudo`/`env`/`timeout` as shell-command grammar, so `rtk cat f` is still classified as a file read for anyone running their own rtk install, which the purge intentionally leaves alone. ## 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 headroom/ tests/ e2e/ --exclude headroom/dashboard/templates All checks passed! $ ruff format --check headroom/ tests/ e2e/ --exclude headroom/dashboard/templates 1255 files already formatted $ mypy headroom/ Success: no issues found in 508 source files $ pytest tests/test_context_tool_cleanup.py -q 11 passed $ pytest tests/test_fsutil.py -q 12 passed $ pytest tests/test_cli/test_wrap_codex.py -q # 89 tests 89 passed in 431.68s $ pytest tests/test_cli/test_wrap_opencode.py -q 39 passed in 257.46s $ pytest tests/test_cli/test_wrap_helpers.py -q 45 passed $ pytest tests/test_paths.py -q 75 passed $ pytest tests/test_cli/test_unwrap_claude.py -q 14 passed $ pytest tests/test_proxy_savings_history.py -q 39 passed $ pytest tests/test_cli/test_wrap_copilot.py -q 27 passed $ pytest tests/test_cli/test_wrap_zcode.py -q 20 passed $ pytest tests/test_subscription_tracker.py -q 9 passed $ pytest tests/test_proxy_dashboard_stats_cache.py -q 5 passed, 1 skipped ``` Repo-wide grep for 14 removed symbols (`headroom.rtk`, `headroom.lean_ctx`, `_ensure_rtk_binary`, `_selected_context_tool`, `_get_context_tool_stats`, `rtk_path`, `lean_ctx_path`, `wrap_rtk_metrics`, `HEADROOM_RTK`, `cli_tokens_avoided`, `tokens_saved_rtk`, …) across `*.py`, `*.ts`, `*.sh`, `*.ps1`, `*.yml`, `*.html`: **zero hits**. Notable test changes: `test_wrap_openhands.py` no longer patches `_ensure_rtk_binary` and asserts `wrap openhands --prepare-only` exits 0 unpatched — the regression that was previously masked. `test_wrap_continue.py` and `test_wrap_hintfile_agents.py` were removed (every test drove RTK instruction injection). A new `test_subscription_tracker.py::test_load_state_written_before_cli_context_tools_were_removed` proves a pre-removal `subscription_state.json` still loads. ## Real Behavior Proof - **Environment:** macOS 15.4 (darwin 25.4.0), Python 3.12.6, Headroom @ this branch, real `~/.headroom` and `~/.claude` on the dev machine. - **Exact command / steps and observed result:** ```text # 1. Retired flag fails loudly instead of silently no-op'ing $ headroom wrap codex --prepare-only --context-tool rtk Error: CLI context tools (rtk, lean-ctx) have been removed from Headroom: they rewrote shell commands through a third-party binary Headroom no longer manages. Drop --context-tool / --no-context-tool and unset HEADROOM_CONTEXT_TOOL; `headroom wrap` uninstalls what they left behind on first run. $ HEADROOM_CONTEXT_TOOL=lean-ctx headroom wrap codex --prepare-only Error: CLI context tools (rtk, lean-ctx) have been removed from Headroom: ... # 2. install.sh rejects the retired flags (extracted parse_wrap_args harness) ['--no-rtk', '--port', '9999'] rc=1 ERROR: CLI context tools ... Drop --no-rtk ['--context-tool=rtk'] rc=1 ERROR: CLI context tools ... Drop --context-tool $ bash -n scripts/install.sh # syntax OK # 3. Purge ran against the real machine, which had all the orphaned artifacts $ python -c "from headroom.context_tool_cleanup import purge_context_tool_artifacts; ..." removed ~/.headroom/bin/lean-ctx (51 MB) removed ~/.headroom/bin/rtk (7.7 MB) removed ~/.local/bin/rtk (symlink into ~/.headroom/bin) removed ~/.claude/hooks/rtk-rewrite.sh removed 8 lean-ctx-* hook scripts # ~/.claude.json afterwards: 90 top-level keys, 19 projects, mcpServers unchanged # → ~59 MB reclaimed, no unrelated key touched # 4. stdout stays machine-readable while the purge reports (planted a fake artifact) $ headroom wrap openclaw --prepare-only --gateway-provider-id codex >out 2>err $ cat out {"enabled":true,"config":{"proxyPort":8787,...}} # parses as JSON $ cat err Retired CLI context tool cleanup: removed /Users/tcms/.headroom/bin/rtk # 5. --help is inert (planted artifact survives), a real run purges $ headroom wrap codex --help → artifact survived: CORRECT $ headroom wrap openclaw --prepare-only → purged: CORRECT # 6. MCP purge dry-run against a copy of the real 82 KB ~/.claude.json top-level keys 90 -> 90; projects 19 -> 19; LOST keys: none all content outside mcpServers byte-identical: True ``` Dashboard rendered via the Playwright test after the panel removal: "Token Savings" shows only `Proxy 0 (0.0%)` / `Of total wire: 36.86%`, and "Token Usage" reads Before Compression → Proxy Removed → After Compression with no "Filtered (this session)" row. Nothing below the removed panel broke. - **Not tested:** Windows and Linux (macOS only) — `install.ps1` is verified by brace-balance and inspection, not executed, since no `pwsh` is available locally. The wrap e2e suite (`e2e/wrap/run.py`) was updated but not run; it needs the Docker e2e image. `serena project index` interaction is exercised in the stacked base 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 - [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 did **not** edit `CHANGELOG.md` — it is generated by release-please from my Conventional Commit PR title (a CI guard enforces this) ## Additional Notes **Stacked on #2676** (`tejas/serena-config-bootstrap`) — please merge that first; this PR's base should then be retargeted to `main`, or it will read as containing that fix too. **Breaking-change migration for users:** - Drop `--rtk`, `--no-rtk`, `--no-project-rtk`, `--keep-rtk`, `--context-tool`, `--no-context-tool` from any alias, script or CI job, and unset `HEADROOM_RTK*` / `HEADROOM_CONTEXT_TOOL`. They now error rather than being ignored, so the failure is immediate and self-explaining. - Previously-installed artifacts are purged automatically on the next `wrap`/`unwrap`; no manual cleanup needed. - `headroom perf --json` no longer carries a `cli_filtering` key, and `/stats` no longer returns a `context_tool` section. **Docs:** `docs/rtk-architecture.md` deleted; RTK/lean-ctx removed from `README.md`, `docs/content/docs/{configuration,opencode,grok-build,docker-install,filesystem-contract}.mdx`, `docs/observability.md` and the matching `wiki/` pages. `REALIGNMENT/09-phase-G-rtk-observability.md` is marked SUPERSEDED rather than deleted, to keep the planning record. **Follow-ups not in scope:** `_emit_wrap_interrupted` was deleted as dead code — its only caller was the `except KeyboardInterrupt` guarding the binary download, so with no download there is nothing slow left to interrupt. |
||
|
|
f74d874777
|
fix(learn): detect the active OpenCode database (#2587)
## Description `headroom learn --agent opencode` can silently mine a frozen conversation corpus. `OpenCodePlugin` hardcodes `~/.local/share/opencode/opencode.db`, but source-built OpenCode writes `opencode-local.db` in the same directory. When both files exist, learn still succeeds against the stale packaged DB and ignores the live source-built corpus. This follows the report in https://github.com/headroomlabs-ai/headroom/issues/2581 and builds on the existing OpenCode learn path introduced in https://github.com/headroomlabs-ai/headroom/pull/559. This change keeps explicit constructor paths authoritative, honors `HEADROOM_OPENCODE_DB` when it is set, and otherwise selects the newest existing database between `opencode.db` and `opencode-local.db`, preferring canonical `opencode.db` on exact ties. It also updates the OpenCode learn docs line so the documented behavior matches the landed resolver. Closes #2581. The branch also carries one narrow CI repair requested during review: `headroom/cli/wrap.py` now binds the `unwrap claude` Click command back to `unwrap_claude` instead of the leak-warning helper, which restores the existing unwrap test surface and leaves the helper as an internal warning function. ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) - [ ] New feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - add a private OpenCode DB resolver in `headroom/learn/plugins/opencode.py` with precedence `db_path` then `HEADROOM_OPENCODE_DB` then newest existing default filename then canonical fallback - preserve canonical `opencode.db` for exact mtime ties and for canonical-only installs - add focused regression coverage for newer-local, explicit-path, canonical-only, equal-tie, missing-override, and end-to-end scanning cases - sync the OpenCode learn docs paragraph so it no longer claims `opencode.db` is the only supported default path - restore the `unwrap claude` Click command binding in `headroom/cli/wrap.py` and apply the repo formatter so the branch passes the existing unwrap test and lint gates ## Testing - [x] Unit tests pass (`uv run pytest tests/test_learn/test_opencode_scanner.py -q`) - [x] Linting passes (`uv run ruff check headroom/learn/plugins/opencode.py tests/test_learn/test_opencode_scanner.py`) - [x] Type checking passes (`uv run mypy headroom/learn/plugins/opencode.py`) - [x] New tests added for new functionality when applicable - [x] Manual testing performed ### Test Output ```text uv run pytest tests/test_learn/test_opencode_scanner.py -q -k "newer_local_database" 1 passed, 9 deselected in 0.26s uv run pytest tests/test_learn/test_opencode_scanner.py -q 10 passed in 0.50s uv run ruff check headroom/learn/plugins/opencode.py tests/test_learn/test_opencode_scanner.py All checks passed! uv run ruff format headroom/learn/plugins/opencode.py tests/test_learn/test_opencode_scanner.py --check 2 files already formatted uv run mypy headroom/learn/plugins/opencode.py Success: no issues found in 1 source file rg -n "opencode-local\.db|HEADROOM_OPENCODE_DB|opencode\.db" docs/content/docs/opencode.mdx 78:`headroom learn` supports OpenCode as a scan target. It reads past sessions from the newer of `~/.local/share/opencode/opencode-local.db` and `~/.local/share/opencode/opencode.db`, or from `HEADROOM_OPENCODE_DB` when you set an explicit override, and writes corrections to your project's `AGENTS.md`. uv run pytest tests/test_cli/test_unwrap_claude.py -q -k "removes_mcp_rtk_and_stops_proxy or preserves_user_managed_serena or removes_headroom_installed_serena or keep_flags_skip_cleanup or restores_all_base_url_modes or stops_claude_owned_persistent_deployment or reports_ambiguous_same_port_persistent_deployment or warns_about_same_port_inherited_env or ignores_malformed_inherited_env_port" 9 passed, 5 deselected in 0.40s uv run ruff check . All checks passed! uv run ruff format --check . 1340 files already formatted ``` ## Real Behavior Proof - Environment: temporary SQLite databases exercised through the production `OpenCodePlugin()` constructor - Exact command / steps: run `uv run pytest tests/test_learn/test_opencode_scanner.py -q -k "newer_local_database"` against `origin/main` with the new regression test overlaid, then run the same command and the full `uv run pytest tests/test_learn/test_opencode_scanner.py -q` suite on the branch head - Observed result: the base reproduction fails with `AssertionError: assert 'Canonical' == 'Local'`, proving current main still selects the stale canonical DB; the branch head passes the reproduction row and the full 10-test scanner suite - Not tested: live user OpenCode corpus ## 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 Headroom generates release notes from conventional commits. - The automatic chooser is intentionally limited to the two known default filenames, `opencode.db` and `opencode-local.db`. Other layouts can use `HEADROOM_OPENCODE_DB`. - The fix stays inside `headroom/learn/plugins/opencode.py`; no provider-neutral learn or pipeline code changes are planned. |
||
|
|
58555c5be0
|
docs(configuration): document cold-prefix hook flags + bound the TTL observation log (#2557)
## Description Follow-up to #2555. Documents the cold-prefix hook / reasoning-compaction / cache-TTL-learner flags (what to set for what, and whether each can be on by default), and makes two small safety fixes so the learning seam is production-ready and free when off. ## Type of Change - [x] Documentation update - [x] Performance improvement (learning seam is now free when disabled) ## Changes Made - **docs/content/docs/configuration.mdx** — env-var table rows for `HEADROOM_THINKING_COMPACT` (+`_KEEP_LAST`), `HEADROOM_COLD_RECOMPACT`, `HEADROOM_DEDUPE`, `HEADROOM_CACHE_TTL_LEARN`, `HEADROOM_KOMPRESS_ENDPOINT`, plus a **Cold-prefix hook & reasoning compaction** section: what to set for what, how cold detection reads the real TTL (CC config vs learned), and a per-flag "can this be on by default?" analysis. - **docs/content/docs/cache-optimization.mdx** — a cold-prefix recompaction section linking to the flags. - **headroom/cache/ttl_observations.py** — the observation log is now size-bounded (single-backup rotation) and respects `HEADROOM_STATELESS`. - **headroom/proxy/handlers/openai.py** — the extra `classify_cache_miss` attribution is gated behind `observations_enabled()` so it costs nothing when learning is off. Everything remains **off by default**. ## Testing - [x] Linting passes (`ruff check`) - [x] Type checking passes (`mypy`) - [x] Manual testing performed (module self-check) ### Test Output ```text $ ruff check headroom/cache/ttl_observations.py headroom/proxy/handlers/openai.py All checks passed! $ mypy headroom/cache/ttl_observations.py headroom/proxy/handlers/openai.py Success: no issues found in 2 source files $ python headroom/cache/ttl_observations.py ttl_observations self-check OK ``` ## Real Behavior Proof - Environment: local repo, Python 3.12 venv. - Exact command / steps: ran the module self-check (covers gated-off no-write, gated-on write, learned-table read with model→provider fallback) and ruff+mypy. - Observed result: self-check passes; when `HEADROOM_CACHE_TTL_LEARN` is unset no file is written; when `HEADROOM_STATELESS` is truthy no file is written; the observation log rotates to `.1` past the size cap. - Not tested: live multi-turn provider run (unchanged from #2555, which carried the live Kimi/CC proofs); docs render is Markdown/MDX 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] My changes generate no new warnings - [x] New and existing checks pass locally with my changes - [x] I did **not** edit `CHANGELOG.md` ## Additional Notes - Default-on stance (in the docs): `THINKING_COMPACT` stays opt-in (rewrites model inputs); `COLD_RECOMPACT` is a candidate to default for Claude Code once TTL detection is field-validated; `CACHE_TTL_LEARN` is the safest to default on (observation-only, bounded, stateless-aware) — kept opt-in for now. |
||
|
|
5d23a0aec2
|
refactor(wrap): retire tokensave; Serena is the code-memory MCP (#2499)
## Description
`tokensave` was a **downloaded third-party Rust binary**
(`aovestdipaperino/tokensave`) that `headroom wrap` registered as a
code-graph MCP server. This removes it entirely and standardises on
**Serena** as the code-memory MCP — which was already the default in
`wrap`. Serena runs on demand via `uvx`, so Headroom no longer downloads
or executes a binary of its own for code memory.
The change is a *removal + safe transition*, not a behaviour flip:
Serena was already the default, so existing users move over
automatically. This PR also folds in a small README repositioning
(Headroom = the proxy; Serena is the recommended companion; RTK/lean-ctx
are third-party tools we don't control), since it's the same
tooling-stack story.
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
- [x] Code refactoring (no functional changes)
## Changes Made
- **Removed the external tool:** `headroom/graph/tokensave_installer.py`
(the download-and-execute path), `build_tokensave_spec`, and the
`_ensure_tokensave_binary` / `_index_tokensave_project` /
`_setup_tokensave_mcp` helpers; dropped the dead `_setup_code_graph`.
- **`--code-memory`** now offers `serena` (default) or `none` — the
`tokensave` choice is gone. The strands `HeadroomBundle` uses Serena as
its (default-on) code-memory MCP.
- **Tombstone / transition (ledger-verified):** `headroom wrap` **and**
`headroom unwrap` remove a previously Headroom-installed `tokensave` MCP
entry so upgrading users stop launching it, and print that the leftover
`~/.local/bin/tokensave` binary and `.tokensave/` folders are safe to
delete. A user-managed `tokensave` entry is left untouched. Mirrors the
existing `codebase-memory-mcp` retirement.
- **Graceful for existing users:** `HEADROOM_CODE_MEMORY=tokensave` and
`--no-tokensave` resolve to Serena instead of erroring; `--no-serena`
now means "no code memory". **No state migration needed** — both tools'
indexes are regenerable caches of the source, so Serena simply
re-indexes.
- **Docs/README:** replaced the "tokensave binary trust model" section
with a Serena note + an "Upgrading from tokensave?" callout; retired the
RTK "first-class part of our stack" framing.
- **Tests:** deleted the tokensave-only test files
(`test_graph_tokensave.py`, `test_cli/test_tokensave_helpers.py`,
`test_cli/test_tokensave_setup.py`), rewrote `test_wrap_code_memory.py`
for the new resolver/dispatch, fixed a codex test that patched a removed
symbol.
Net: **+102 / −1187 lines.**
## Testing
- [x] Unit tests pass (`pytest`) — targeted to the affected areas
- [x] Linting passes (`ruff check`)
- [x] Type checking passes (`mypy`)
- [x] New tests added for new functionality
- [x] Manual testing performed
### Test Output
```text
$ ruff check headroom/cli/wrap.py headroom/mcp_registry/ headroom/integrations/strands/ tests/test_wrap_code_memory.py tests/test_cli/conftest.py tests/test_cli/test_wrap_codex.py
All checks passed!
$ mypy headroom/cli/wrap.py headroom/mcp_registry headroom/integrations/strands/bundle.py
Success: no issues found in 12 source files
$ pytest tests/test_wrap_code_memory.py tests/test_cli/test_wrap_codex.py \
tests/test_cli/test_wrap_claude_vertex_proxy_env.py \
tests/test_cli/test_wrap_claude_finally_unbound.py -q
======================== 120 passed in 97.86s (0:01:37) ========================
$ pytest tests/test_cli --collect-only -q
========================= 713 tests collected in 1.82s ========================= # no import errors after symbol removal
```
## Real Behavior Proof
- **Environment:** macOS (darwin), Python 3.12.6, local `.venv`, on
branch `tejas/remove-tokensave`.
- **Exact command / steps:**
- `python -c "from headroom.integrations.strands.bundle import
HeadroomBundle; b=HeadroomBundle(enable_headroom_mcp=False,
enable_serena_mcp=False); print(len(b.tools))"` → confirms the module
imports after `build_tokensave_spec` removal (the import that my change
would otherwise break).
- CliRunner-driven `wrap codex --prepare-only` (in `test_wrap_codex.py`)
writes `[mcp_servers.serena]` (with `command = "uvx"`, `"--context",
"codex"`) to the codex config and **no** tokensave entry.
- `_resolve_code_memory` unit tests confirm: default → `serena`;
`HEADROOM_CODE_MEMORY=tokensave` → `serena`; `--no-serena` → `none`;
`--code-memory bogus` → `ClickException`.
- **Observed result:** import OK (`tools: 0`); Serena registered,
tokensave absent; resolver behaves as above; 120/120 tests pass.
- **Not tested:** a live `headroom wrap` against a real agent on a
machine with a *previously-installed* tokensave MCP entry — the
tombstone-removal path is covered by unit tests with a fake
registrar/ledger, not an end-to-end run.
## Review Readiness
- [x] I have performed a self-review
- [x] This PR is ready for human review
## Checklist
- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [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 did **not** edit `CHANGELOG.md` — it is generated by
release-please from my Conventional Commit PR title
## Additional Notes
- `--no-tokensave` / `--serena` / `--no-serena` are retained as hidden,
deprecated flags (no-ops or mapped) so existing scripts don't break.
- `--code-graph` is unchanged — it's the proxy's live file-watcher flag
and was never the tokensave MCP; only the dead tokensave hook behind it
was removed.
- `CHANGELOG.md` intentionally left untouched.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
|
||
|
|
f0975b8de0
|
docs: add troubleshooting entry for uv build cache errors (#2490)
## Description Adds a troubleshooting section for a uv build error macOS users hit installing headroom-ai via uv: `src does not appear to be a Python project` (typically surfacing on `litellm` or `cryptography`) or `Unknown wheel data type: .DS_Store`. Root cause is uv build/wheel cache corruption on the user's machine, not a Headroom dependency pin. Also cross-references the existing `ast-grep-cli>=0.30.0,!=0.44.1` pin, which already excludes the compromised 0.44.1 build reported in the same issue. Closes #2476 ## 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 "uv build errors: 'src does not appear to be a Python project'" subsection under Installation Issues in `docs/content/docs/troubleshooting.mdx`, with symptom, cause, and `uv cache clean` fix. - Cross-referenced the already-shipped `ast-grep-cli` version pin for the 0.44.1 supply-chain issue. ## 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 N/A — docs-only change, no code paths touched. No pytest/ruff/mypy relevant. ``` ## Real Behavior Proof - Environment: N/A — markdown documentation edit only, no runtime behavior changed. - Exact command / steps: Read the modified `docs/content/docs/troubleshooting.mdx` section against the rendered structure of adjacent entries (Windows Defender / ast-grep-cli section) to confirm heading level, code fences, and link formatting match. - Observed result: New subsection renders consistently with surrounding Installation Issues entries (same `###` heading depth, Symptom/Cause/Fix structure, fenced code blocks). - Not tested: Live docs site build/preview (`cd docs && npm run dev`) was not run in this environment. ## 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 — N/A, prose docs - [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 - [x] I did **not** edit `CHANGELOG.md` ## Screenshots (if applicable) N/A ## Additional Notes Docs-only change; no source code touched. `docs && npm run dev` not run locally in this environment — flagging for maintainer to preview if desired before merge. Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
5a0a5a79cd
|
docs: sync Vercel docs with current code and add in-depth proxy config (#2475)
## Description Bring the published docs (headroom-docs.vercel.app) back in line with the current codebase. The docs described an older architecture, advertised user/community statistics the code no longer supports (the telemetry beacon was removed), shipped code samples that raise on import, and lacked an in-depth treatment of proxy-mode configuration. Docs-only change — no `headroom/` source touched. ## Type of Change - [x] Documentation update ## Changes Made - **Remove user/community stats.** The anonymous telemetry beacon was removed from the code and `HEADROOM_TELEMETRY` is now local-only, but the docs still advertised aggregate "instances worldwide" figures — which were hardcoded/fabricated. Deleted `community-savings.mdx` (+ nav entry), the community/live stat widgets and their components (`community-charts`, `community-stats-header`, `live-stats`, `stats`, `lib/telemetry`, and a second fabricated `LiveStats` in `marketing.tsx`), and the `## Production Telemetry` section in `benchmarks.mdx`. Reframed all telemetry wording as local-only. - **Correct the architecture docs.** Rewrote `architecture.mdx` to the real pipeline (interceptor → CacheAligner *off-by-default* → ContentRouter; Rust `_core`; CCR on by default). Dropped the removed 3-stage / Context Manager / RollingWindow model. Fixed `how-compression-works.mdx` (3-stage framing, dead LLMLingua reference, wrong compressor class names) and added an off-by-default note to `cache-optimization.mdx`. - **Fix broken code samples** (verified against source): `TextCompressor`→`TextCrusher` + real `SearchCompressorConfig` fields (`text-and-logs`), `MemoryCategory`→plain string (`memory`), `unload_tree_sitter` import path (`code-compression`). - **In-depth proxy configuration.** Added a "Configuration in depth" section to `proxy.mdx` (Kompress, CCR/lossless, file-read handling, reliability, tool-search/MCP, cost-aware routing, observability, security/networking, performance). Fixed the `HEADROOM_MODE` default (`token`→`cache`) in three pages and removed a duplicate `HEADROOM_TELEMETRY` row. - **Nav + links.** Un-orphaned `crewai`/`autogen` in the sidebar; normalized `chopratejas`→`headroomlabs-ai` repo/GHCR links (kept the real HF model id `chopratejas/technique-router`); `litellm-vertex`→`vertex_ai`. ## Testing - [ ] Unit tests pass (`pytest`) — N/A, no `headroom/` code changed - [ ] Linting passes (`ruff check .`) — N/A, no Python changed - [ ] Type checking passes (`mypy headroom`) — N/A, no Python changed - [x] Manual testing performed (static docs validation; output below) ### Test Output ```text -- dangling refs to deleted components/pages (expect empty) -- (none) -- meta.json valid -- pages: 60 | community-savings present: false | crewai: true | autogen: true -- Callout balance (open == close) -- docs/content/docs/proxy.mdx open=6 close=6 docs/content/docs/cache-optimization.mdx open=1 close=1 ``` ## Real Behavior Proof - Environment: docs are static MDX (Fumadocs/Next.js); no runtime behavior. Corrections were checked against `headroom/` source. - Exact command / steps: grepped for references to deleted components/pages; validated `meta.json` parses and no longer contains `community-savings`; confirmed `<Callout>` open/close balance and frontmatter on every edited page; verified every corrected API name/field/import against the source modules (`text_crusher.py`, `search_compressor.py`, `memory/__init__.py`, `code_compressor.py`). - Observed result: no dangling references; nav valid; balanced JSX; corrected code samples match the real importable API. - Not tested: full `next build` / `npm run types:check` — `docs/node_modules` is not installed in this environment. Recommend a Vercel preview deploy (or `cd docs && npm i && npm run types:check`) as the merge gate. ## 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 — 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 — N/A (docs-only) - [x] New and existing unit tests pass locally with my changes — N/A, no code changed - [x] I did **not** edit `CHANGELOG.md` ## Additional Notes - Docs-only; no `headroom/` package code touched, so the pytest/ruff/mypy items are N/A. - The full Next.js build was not run locally (deps not installed) — a Vercel preview is the recommended gate. - Org normalization assumes `headroomlabs-ai` is canonical (matches CI/GHCR + the newer docs). If `chopratejas/headroom` is still the canonical **public** repo, revert the `docs/lib/*.ts` + install/docker link changes. - Heads-up: a separate `docs` branch exists on the remote — if the Vercel docs site deploys from `docs` rather than `main`, retarget this PR there. |
||
|
|
961866ba7c
|
deps: bump the npm-minor-patch group across 3 directories with 7 updates (#2276)
Bumps the npm-minor-patch group with 6 updates in the /docs directory: | Package | From | To | | --- | --- | --- | | [fumadocs-core](https://github.com/fuma-nama/fumadocs) | `16.11.1` | `16.11.5` | | [fumadocs-mdx](https://github.com/fuma-nama/fumadocs) | `15.1.0` | `15.2.0` | | [fumadocs-ui](https://github.com/fuma-nama/fumadocs) | `16.11.1` | `16.11.5` | | [@anthropic-ai/sdk](https://github.com/anthropics/anthropic-sdk-typescript) | `0.106.0` | `0.111.0` | | [openai](https://github.com/openai/openai-node) | `6.33.0` | `6.47.0` | | [postcss](https://github.com/postcss/postcss) | `8.5.16` | `8.5.19` | Bumps the npm-minor-patch group with 1 update in the /plugins/opencode directory: @opencode-ai/plugin. Bumps the npm-minor-patch group with 1 update in the /sdk/typescript directory: [@anthropic-ai/sdk](https://github.com/anthropics/anthropic-sdk-typescript). Updates `fumadocs-core` from 16.11.1 to 16.11.5 <details> <summary>Commits</summary> <ul> <li><a href=" |
||
|
|
45a5a33b33
|
docs(proxy): document Vertex AI backend setup, env vars, aliases, native passthrough (#2422)
## Description Documents the Vertex AI proxy backend properly, fixing #2393. Following the docs verbatim (`pip install "headroom-ai[proxy]"` + `headroom proxy --backend vertex_ai`) currently fails with `vertexai import failed`, and the LiteLLM-specific `VERTEXAI_PROJECT`/`VERTEXAI_LOCATION` env vars are documented nowhere — risking requests silently resolving against the ADC default quota project and billing the wrong GCP project. All documented behavior was verified against source: alias normalization in `headroom/providers/registry.py` (`vertex`/`google-vertex`/`googlevertex` → `vertex_ai`), the always-registered native publisher passthrough routes in `headroom/providers/proxy_routes.py`, and `pyproject.toml` (no extra pulls in `google-cloud-aiplatform`). ## Type of Change - [ ] Bug fix - [ ] New feature - [x] Documentation update - [ ] Refactor - [ ] Other ## Changes Made - `docs/content/docs/proxy.mdx`: new **Google Vertex AI** subsection under Cloud providers — `google-cloud-aiplatform>=1.38` requirement (not in any extra or Docker image), `VERTEXAI_PROJECT`/`VERTEXAI_LOCATION` env vars with a warning about silent ADC quota-project fallback and their distinction from the standard `GOOGLE_CLOUD_PROJECT`/`GOOGLE_CLOUD_LOCATION` vars, backend name alias equivalence (`vertex_ai` / `vertex` / `google-vertex` / `googlevertex` / `litellm-vertex` / `litellm-vertex_ai`), and cross-links to the Claude Code on Vertex page and the LiteLLM callback page. - `docs/content/docs/proxy.mdx`: new **Native Vertex passthrough routes** subsection documenting the unconditionally registered `/{api_version}/projects/{project}/locations/{location}/publishers/{publisher}/models/{model}:*` routes and the `publisher=google` (Gemini handler) vs `publisher=anthropic` (LiteLLM-Vertex path) branching. - `docs/content/docs/installation.mdx`: added `VERTEXAI_PROJECT` and `VERTEXAI_LOCATION` rows to the LLM provider keys table, plus a pointer to the new Vertex section for the SDK dependency. - `docs/content/docs/litellm.mdx`: cross-reference callout distinguishing the LiteLLM callback integration from the proxy's `litellm-*` backends (issue gap #5). ## Testing - [x] Docs build passes locally **Test Output** ``` $ npm run build # docs/ — same as CI validate-nextjs ✓ Static + SSG pages generated (exit code 0), /docs/proxy, /docs/installation, /docs/litellm prerendered $ mkdocs build # same as CI validate-mkdocs INFO - Documentation built in 8.32 seconds ``` ## Real Behavior Proof - Environment: Windows 11, Node 20, npm 10, Python 3.13, mkdocs-material (latest), branch `docs/2393-vertex-ai-backend` off `upstream/main`. - Exact command / steps: `cd docs && npm ci && npm run build`; `mkdocs build` from repo root; manually re-verified each documented claim against `headroom/providers/registry.py` (alias normalization), `headroom/providers/proxy_routes.py` (publisher passthrough routes), and `pyproject.toml` `[project.optional-dependencies]` (no vertex SDK in any extra). - Observed result: Both docs builds succeed; new sections render with valid internal anchors (`/docs/proxy#google-vertex-ai`, `/docs/proxy#cloud-providers`, `/docs/claude-code-vertex`, `/docs/litellm`). - Not tested: Live end-to-end Vertex AI request through the proxy (no GCP project available); error messages and env-var behavior are taken from the issue reporter's verified reproduction on v0.32.0 and cross-checked against LiteLLM's Vertex provider docs. ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
9cba64d89e
|
docs(troubleshooting): explain cache-mode default showing ~0 compression savings on the dashboard (#2248) (#2424)
## Description Users upgrading 0.27.0 → 0.31.0 report that the dashboard's compression / "Tokens Saved" figures drop to ~0 and conclude Headroom stopped working. The #2248 reporter ran the same prompt on both versions and captured the telltale detail: **0.31.0 actually spent fewer total tokens than 0.27.0, despite showing 0 saved.** This is a default-mode change, not a regression. 0.31.0 ships the `coding` savings profile as the out-of-box default (`headroom/agent_savings.py`: `DEFAULT_PROFILE = "coding"`), and `coding` sets `proxy_mode="cache"`. Cache mode freezes the provider prefix and compresses only the newest turn *delta* — deliberately, to avoid busting the prompt cache — so the **compression** number is small while savings shift to **cheaper prefix-cache reads**. On a short prompt there's little delta to compress, so the compression tile reads ~0 even as real cost drops. The reference behavior is already documented in the proxy docs' [Savings profiles](/docs/proxy#savings-profiles) section, but there was no discoverable troubleshooting entry connecting the alarming "0 saved after upgrade" symptom to this cause — so it gets filed as a bug. Closes #2248 ## 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/content/docs/troubleshooting.mdx` only — a new `### Dashboard shows 0 compressed/saved tokens after upgrading to 0.31.0` subsection appended to the existing `## No Token Savings` section: - **Symptom** — compression figures ~0 after upgrade, while total spend is flat or lower (so users can match it by search). - **Cause** — the `coding`/cache-mode default and why delta-only compression makes the compression tile small. - **Where the savings show up** — the **Prefix Cache Impact** panel and **Compression vs Cache** tile, which reflect cache-read savings; the headline "Tokens Saved" tile counts compression only and understates the benefit in cache mode. - **How to get 0.27.0-style numbers back** — `--mode token`, or `HEADROOM_SAVINGS_PROFILE=balanced` / `agent-90`, with the explicit trade-off that token mode raises visible compression but can reduce prefix-cache hits. Placed under the existing `## No Token Savings` heading (which covers the separate SDK/library case: audit mode, sub-threshold tool outputs) rather than rewriting it. Cross-links to the existing Savings-profiles reference instead of restating the profile table, keeping one source of truth. No code change. ## 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 Docs-only; verification is fact cross-check against source plus MDX sanity: ```text $ grep -n 'DEFAULT_PROFILE = \|proxy_mode="cache"' headroom/agent_savings.py 18:DEFAULT_PROFILE = "coding" 173: proxy_mode="cache", # delta-only compression at ~0 prefix-cache busts $ grep -n "_estimate_cache_savings_usd" headroom/proxy/savings_tracker.py 248:def _estimate_cache_savings_usd(model: str, cache_read_tokens: int) -> float: $ grep -c "Prefix Cache Impact" headroom/dashboard/templates/dashboard.html # 2 $ grep -c "Compression vs Cache" headroom/dashboard/templates/dashboard.html # 1 $ grep -n "### Savings profiles" docs/content/docs/proxy.mdx 94:### Savings profiles # cross-link target for /docs/proxy#savings-profiles # placement: new "### Dashboard shows 0 compressed..." (line 145) sits between # "## No Token Savings" (89) and "## Claude Code context window..." (166) # MDX sanity: code fences balance (even count) ``` ## Real Behavior Proof - **Environment:** Docs source verified against the current `main` base (`718c8dc5`). - **Exact command / steps:** Issue #2248 contains a complete reproduction — the same prompt run under 0.27.0 and 0.31.0 via `headroom wrap claude --dangerously-skip-permissions` (Sonnet 5, same files, same Claude Code version, reproduced on macOS and Debian 12), with dashboard screenshots showing savings on 0.27.0 and ~0 on 0.31.0. Every claim in the new section is verified against the tree with the greps above: the `coding` default and its `proxy_mode="cache"`, the cache-read savings estimator, and both dashboard panel/tile labels users are pointed to. - **Observed result:** The documented cause matches the code — the compression tile legitimately reads ~0 in cache mode while cache-read savings accrue in the Prefix Cache Impact panel, which explains the reporter's own observation that 0.31.0 spent *fewer* tokens while showing 0 saved. - **Not tested:** I did not re-run a live 0.27.0-vs-0.31.0 dashboard comparison (that requires installing an old release and generating real provider traffic); the reporter's reproduction with screenshots already establishes the symptom, and the cause is verified in source. No local Fumadocs site build was run, so the section is validated by MDX syntax checks rather than a rendered preview. ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Checklist - [x] My code follows the project's style guidelines - [x] I have performed a self-review of my code - [ ] I have commented my code, particularly in hard-to-understand areas - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] I have updated the CHANGELOG.md if applicable ## Screenshots (if applicable) N/A (troubleshooting prose addition). ## Additional Notes - Test/tests-added and CHANGELOG checklist items are N/A — documentation-only change, kept to a single file (matching the merged #2031 and #2237 precedent). - If maintainers would rather resolve this in the UI than the docs, an alternative is a dashboard hint shown when mode is `cache` and compression savings are ~0 (pointing at the Prefix Cache Impact panel). That touches `dashboard.html` and has UX implications, so it's intentionally not attempted here. - This is the second report rooted in the cache-mode default (following the confusion behind #2031), which is why it's framed as a searchable troubleshooting entry rather than another reference-section edit. |
||
|
|
5424e99a65
|
Clarify uv tool install path on macOS (#1196)
## Description Clarifies the recommended install path for the Headroom CLI on macOS Apple Silicon and Linux. The docs now prefer `uv tool install --python 3.13 "headroom-ai[all]"` for host-level CLI use, keep `pip install` scoped to Python project environments, and call out absolute executable paths for MCP clients that do not inherit interactive shell `PATH`. ## 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 `uv tool install --python 3.13` guidance to the README, docs install page, quickstarts, and wiki install pages. - Documented `uv tool update-shell` for shells that cannot find the installed `headroom` command. - Clarified absolute MCP server command paths for clients that do not inherit the interactive shell `PATH`. - Pointed Intel macOS users at the Docker-native install path until native wheel support lands. ## Testing Describe the tests you ran to verify your changes: - [ ] Unit tests pass (`pytest`) - not run; docs-only change. - [ ] Linting passes (`ruff check .`) - not run; docs-only change. - [ ] Type checking passes (`mypy headroom`) - not run; docs-only change. - [ ] New tests added for new functionality - not applicable. - [x] Manual testing performed - [x] `git diff --check upstream/main...HEAD` ## Real Behavior Proof ```bash $ git diff --check upstream/main...HEAD # exits 0; no whitespace errors ``` `npm --prefix docs run types:check` was also attempted. It regenerated MDX and route types successfully, then failed in existing docs app code because `@/lib/...` imports cannot resolve from files such as `app/(home)/layout.tsx`, `app/api/search/route.ts`, and `components/button.tsx`. This PR only changes `README.md`, `docs/content/docs/installation.mdx`, `docs/content/docs/quickstart.mdx`, and `wiki/*.md` files. ## Review Readiness - [x] Draft PR; docs wording and install-path accuracy are ready for review. - [x] No code or runtime files changed. - [x] Known docs type-check blocker is documented above. ## Test Output ```bash $ git diff --check upstream/main...HEAD # no output ``` ```text $ npm --prefix docs run types:check [MDX] generated files ✓ Types generated successfully app/(home)/layout.tsx(2,29): error TS2307: Cannot find module @/lib/layout.shared or its corresponding type declarations. ... components/button.tsx(4,20): error TS2307: Cannot find module @/lib/cn or its corresponding type declarations. ``` ## 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 - not applicable; docs-only change. - [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 - not applicable; docs-only change. - [ ] New and existing unit tests pass locally with my changes - not run; docs-only change. - [ ] I have updated the CHANGELOG.md if applicable - not applicable. ## Screenshots (if applicable) Not applicable. ## Additional Notes The PR remains a draft while docs verification is limited by the existing docs app `@/lib/*` resolution issue. --------- Co-authored-by: JerrettDavis <mxjerrett@gmail.com> |
||
|
|
420dc9077b
|
feat(grok-build): add Grok Build wrap command and MCP integration (#1629)
## Description
Adds first-class Grok Build support to Headroom so Grok CLI sessions can
route through the local proxy for context compression and savings
tracking.
This PR introduces `headroom wrap grok-build` / `headroom unwrap
grok-build`, a `grok_build` provider slice, Grok MCP registrar support,
and install/telemetry wiring so Grok traffic is attributed correctly in
the proxy and dashboard.
Review follow-up (`9368c413`): when users already own
`[model.grok-build]` in `~/.grok/config.toml`, wrap rewrites `base_url`
in that table in place instead of appending a duplicate header (invalid
TOML).
## Type of Change
- [x] New feature (non-breaking change that adds functionality)
## Changes Made
- Added `headroom/providers/grok_build/` with runtime helpers,
reversible `~/.grok/config.toml` injection, and install env builders.
- Added `headroom wrap grok-build` and `headroom unwrap grok-build` CLI
commands.
- Added `GrokRegistrar` for Headroom MCP registration in Grok config.
- Wired `grok_build` into install planner/registry, agent savings,
telemetry, and proxy client detection (`grok/` user agent).
- **Review fix:** rewrite `base_url` inside an existing user-owned
`[model.grok-build]` table in place (`# was: …` metadata).
- Added regression tests + docs (`grok-build.mdx`, `proxy.mdx`) and
CHANGELOG entry.
## Testing
- [x] Unit tests pass (`pytest`)
- [x] New tests added for new functionality
- [x] Manual testing performed
### Test Output
```text
$ pytest -q tests/test_provider_grok_build.py tests/test_mcp_registry/test_grok_registrar.py
============================== 12 passed in 1.13s ==============================
```
See **Screenshots** below for terminal captures (pytest, review-fix
in-place rewrite, proxy `/readyz`, unwrap).
## Real Behavior Proof
- Environment: macOS, Python 3.11.12 venv, feat/grok-build @ `
|
||
|
|
e8bff1cfe3
|
feat: add CrewAI and AutoGen tool compression integrations (#1384)
## Description Add CrewAI and AutoGen tool compression integrations, following the same patterns as the existing LangChain agent integration (`HeadroomToolWrapper` / `wrap_tools_with_headroom`). Both delegate compression to `compress_tool_result()` from the MCP integration, with per-tool metrics tracking via `ToolCompressionMetrics` / `ToolMetricsCollector`. Closes #1379 ## Type of Change - [x] New feature (non-breaking change that adds functionality) ## Changes Made - Add `headroom/integrations/crewai/` — `HeadroomToolWrapper` subclasses CrewAI `BaseTool`, wraps `_run()` with compression - Add `headroom/integrations/autogen/` — `HeadroomToolWrapper` wraps AutoGen `FunctionTool` (sync and async) with compression - Wire both into `headroom/integrations/__init__.py` with aliased re-exports (avoids name collision with LangChain's `HeadroomToolWrapper`) - Add `[crewai]` and `[autogen]` optional dependency extras to `pyproject.toml` - Add 24 unit tests (12 per framework) under `tests/test_integrations/` - Add `.mdx` doc pages for both frameworks under `docs/content/docs/` - Update `CHANGELOG.md` with entries under `### Added` ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [ ] Type checking passes (`mypy headroom`) - [x] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text $ ruff check headroom/integrations/crewai headroom/integrations/autogen tests/test_integrations/crewai tests/test_integrations/autogen All checks passed! $ pytest tests/test_integrations/autogen -v 12 passed $ pytest tests/test_integrations/crewai -v 12 passed ``` ## Real Behavior Proof - Environment: Windows 11, Python 3.11, crewai 1.14.7, autogen-agentchat 0.7.5 - Exact command / steps: Ran standalone adapter demos and benchmark runner across 4 task types - Observed result: | Task | Tokens (raw) | Tokens (compressed) | Savings | |------|-------------|-------------------|---------| | Inventory JSON (80 items) | 5,044 | 1,532 | 69.6% | | Server logs (150 lines) | 8,712 | 314 | 96.4% | | Analytics query (100 rows) | 10,762 | 10,762 | 0% | | API docs (20 endpoints) | 8,043 | 8,043 | 0% | Compression results are identical across CrewAI and AutoGen — expected since both route through the same `compress_tool_result()` pipeline. - Not tested: Full end-to-end with a live LLM agent loop (demos test the compression pipeline standalone). LangGraph not included — headroom already has `headroom/integrations/langchain/langgraph.py`. ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Checklist - [x] My code follows the project's style guidelines - [x] I have performed a self-review of my code - [x] I have commented my code, particularly in hard-to-understand areas - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [x] I have updated the CHANGELOG.md if applicable ## Additional Notes - LangGraph integration is intentionally excluded — headroom already has one at `headroom/integrations/langchain/langgraph.py` - Re-exports in `__init__.py` are aliased (`CrewAIToolWrapper`, `AutoGenToolWrapper`) to avoid collision with the existing LangChain `HeadroomToolWrapper` - Both integrations follow the exact same conventions as the existing LangChain agents module: optional dep guard, `compress_tool_result()` delegation, metrics with 1000-entry cap, Google-style docstrings - `mypy` not checked due to Rust build dependency (`maturin`) that requires Application Control policy changes on this machine --------- Co-authored-by: Sneha27feb <sroy27.ai@gmail.com> |
||
|
|
4cbd5da673
|
feat(proxy): opt-in compression for catch-all passthrough routes (#1699)
## Description Requests whose path doesn't match a built-in API route fall through to `handle_passthrough`, which forwarded the body verbatim — bypassing ContentRouter/Kompress/TOIN entirely. Wrapper-proxy setups that front Headroom on custom paths (e.g. `/api/codex-proxy/<key>/v1/responses`) got zero compression on coding-agent traffic and hit context-limit 400s in long sessions. This adds an opt-in flag that routes OpenAI Responses-shaped passthrough bodies through the same compression path the native `/v1/responses` handler uses. Closes #1546 ## 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 `ProxyConfig.compress_passthrough` (default `False`) + `--compress-passthrough` CLI flag + `HEADROOM_COMPRESS_PASSTHROUGH=1` env. - `handle_passthrough`: when enabled, POST requests whose path ends in `/responses` with an OpenAI Responses-shaped body are compressed via the existing `_compress_openai_responses_payload_in_executor` before forwarding; stale `Content-Length` is dropped so httpx recomputes it. - New `_maybe_compress_passthrough_responses` helper — fail-open: non-JSON, non-Responses payloads, unmodified results, and any compressor error forward the original body unchanged. - Documented the flag in `docs/content/docs/proxy.mdx`. ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [ ] Type checking passes (`mypy headroom`) - [x] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text $ .venv/bin/python -m pytest tests/test_compress_passthrough.py -q collected 6 items tests/test_compress_passthrough.py ...... [100%] ============================== 6 passed in 0.35s =============================== $ .venv/bin/ruff check headroom/proxy/handlers/openai.py headroom/proxy/models.py headroom/proxy/server.py tests/test_compress_passthrough.py All checks passed! ``` ## Real Behavior Proof - Environment: macOS arm64, Python 3.14, repo `.venv`. - Exact command / steps: `.venv/bin/python -m pytest tests/test_compress_passthrough.py -q` — covers a Responses-shaped body being compressed, non-JSON passthrough, non-Responses (`messages`) payload untouched, unmodified-result short-circuit, compressor-error fail-open, and `ProxyConfig().compress_passthrough is False` default. Plus import smoke: `ProxyConfig(compress_passthrough=True)`, server/handler modules import, helper present. - Observed result: 6 passed; flag defaults off; enabled path reuses the native Responses compressor and never raises out to the request. - Not tested: live end-to-end through a real second proxy to a real upstream (no external wrapper proxy / upstream credentials in sandbox); the compression call is the same one `/v1/responses` already exercises in CI. ## 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 Scoped to OpenAI Responses-shaped bodies (the reporter's exact case). Anthropic `/messages` and OpenAI `/chat/completions` passthrough compression are natural follow-ups — deliberately left out to keep this change focused and fail-safe. CHANGELOG is release-managed, left unchecked. |
||
|
|
dec60de976
|
fix(codex): preserve wrapped sessions and recover state (#2160)
## Description Closes #2159. Codex wrappers currently launch against a disposable `CODEX_HOME`, so session state created during a wrapped run can disappear when that temporary directory is removed. This change launches Codex against its durable home, keeps proxy routing process-local, and adds recovery for retained temporary homes and pinned recovery sources. ## 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 - Launch Codex against its durable `CODEX_HOME` and apply routing through process-local config overrides after the actual proxy port is resolved. - Preserve custom provider identity and reject providers that cannot be redirected safely. - Detect dangling temporary Codex homes before interactive wraps and offer recovery. - Add `headroom recover codex` with automatic discovery, repeatable `--source`, preview, confirmation, retained backups, and rollback on failure. - Search Python's temp root, `$TMPDIR`, `/tmp`, `/private/tmp`, and macOS `/private/var/folders/*/*/T` for retained `headroom-codex-home-*` directories. - Reuse `source-pinned/` copies left by interrupted or failed recovery attempts after the original temporary home has disappeared. - Report deleted temporary homes still referenced by SQLite rollout paths without treating paths pasted into prompts or errors as filesystem evidence. - Audit the durable thread index, rollout files, and history when no source remains, including indexed chat counts and history-only orphan records. - Normalize legacy localhost `headroom` providers in both SQLite thread rows and rollout `session_meta`, including retries after an earlier broken recovery, while preserving user-defined remote providers named `headroom`. - Merge compatible config, JSONL, rollout, SQLite, credential, and regular-file state without propagating deletions or runtime artifacts. - Rewrite recovered thread rollout paths to the durable home and restore legacy Headroom thread providers to the active provider. - Validate SQLite schemas, SQLx migration checksums, integrity, and foreign keys, and quarantine malformed JSONL. - Preserve failed targets with an atomic rename before rollback, avoiding recursive-deletion races with live SQLite runtime files. - Document discovery, migration, retained backups, rollback behavior, and the limits of deleted-source recovery. ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check`) - [x] Type checking passes (`mypy`) - [x] New tests added for new functionality - [x] Manual testing performed in isolated Docker containers ### Test Output ```text $ uv run pytest tests/test_cli/test_wrap_codex.py tests/test_cli/test_recover_codex.py -q 122 passed $ uv run ruff check headroom/cli/recover.py headroom/providers/codex/recovery.py tests/test_cli/test_recover_codex.py All checks passed! $ uv run ruff format --check headroom/cli/recover.py headroom/providers/codex/recovery.py tests/test_cli/test_recover_codex.py 3 files already formatted $ uv run mypy headroom/cli/recover.py headroom/providers/codex/recovery.py Success: no issues found in 2 source files ``` All validation ran in `ghcr.io/astral-sh/uv:python3.12-bookworm` against a writable disposable copy of a read-only source mount. Codex was not installed or launched, and no real user Codex state was read or modified. The tests cover multi-root discovery, deleted-reference reporting, retained pinned-source recovery, durable SQLite path relocation, SQLite and rollout provider normalization, idempotent repair after an earlier broken recovery, remote provider preservation, unrelated dangling target rows, backup retention, atomic rollback, malformed-state quarantine, SQLite validation, and Windows-safe handle closure. The repository shim E2E was not launched locally because this recovery work intentionally avoids launching Codex. Upstream CI exercises wrapper E2E in isolated environments. ## Real Behavior Proof - Environment: `ghcr.io/astral-sh/uv:python3.12-bookworm`, Python 3.12, a writable disposable checkout copied from a read-only source mount, at head ` |
||
|
|
57e8dcb425
|
feat(proxy): add opt-in cost-aware model router (#1706) (#2205)
## Description Adds an optional, configuration driven model router (closes #1706). With `HEADROOM_MODEL_ROUTER_ENABLED` set, ordered rules in `HEADROOM_MODEL_ROUTES` rewrite the upstream model by estimated input size and tool presence, complementary to content compression, for example sending small, tool-free requests to a cheaper model. First matching rule wins and every decision is logged with a reason. Off by default so behavior is unchanged, skipped under `x-headroom-bypass`/passthrough, and wired on the Anthropic `/v1/messages` path. Malformed rules fail open, so a bad rule is skipped rather than silently widened. Closes #1706 ## 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/proxy/model_router.py`: new `ModelRouter` component (ordered rules, first-match decision with reason, fail-open env parsing, tokenizer-free input estimate). - `headroom/proxy/models.py` + `headroom/proxy/server.py`: `ProxyConfig.model_router` field, env loader (`HEADROOM_MODEL_ROUTER_ENABLED` / `HEADROOM_MODEL_ROUTES`), and proxy wiring. - `headroom/proxy/handlers/anthropic.py`: apply routing on `/v1/messages` after the bypass gate, tracked as a body mutation. - Tests, docs (`configuration.mdx`), and a CHANGELOG entry. ## 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 -q tests/test_proxy/test_model_router.py tests/test_proxy/test_model_router_wiring.py 36 passed, 1 warning $ ruff check . All checks passed! $ mypy headroom --ignore-missing-imports Success: no issues found in 477 source files ``` ## Real Behavior Proof - Environment: local, macOS, Python 3.12, headroom `.venv`, upstream mocked (no live provider call). - Exact command / steps: enable the router via `ProxyConfig(model_router=...)`, POST `/v1/messages` through `TestClient` with a rule routing low-risk requests to a cheaper model; repeat with header `x-headroom-bypass: true`. - Observed result: the forwarded upstream body model is rewritten from `claude-sonnet-4-6` to `claude-haiku-4-5` when the router is enabled, and is left unchanged under bypass (see `tests/test_proxy/test_model_router_wiring.py`). - Not tested: the OpenAI and Gemini handler paths (this PR wires the Anthropic path only); no live provider request (upstream is mocked). ## 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 Happy to adjust the interface or scope (for example OpenAI and Gemini parity) if you'd prefer a different shape. --------- Co-authored-by: JerrettDavis <mxjerrett@gmail.com> Co-authored-by: reneleonhardt <65483435+reneleonhardt@users.noreply.github.com> |
||
|
|
17d60dce1f
|
docs(troubleshooting): document Windows Defender ast-grep-cli false positive + workarounds (#2200) (#2237)
## Description On Windows, `uv tool install "headroom-ai[all]"` (and `pip install`) fails while installing the `ast-grep-cli` wheel because Windows Defender quarantines the bundled `sg.exe` as `Trojan:Win64/Lazy!MTB` (`os error 225`). This is a **known upstream false positive** in the `ast-grep-cli` wheel ([ast-grep/ast-grep#2799](https://github.com/ast-grep/ast-grep/issues/2799)), not a Headroom-introduced problem — but because `ast-grep-cli` is a base dependency, the local install path is blocked on affected Windows machines. The issue (#2200) explicitly asks: "At minimum, please document a supported workaround." This adds a troubleshooting entry with safest-first workarounds. `ast-grep` is used only for optional AST-based Read-output outlining and Headroom degrades gracefully without it, so the impact is purely the install-time quarantine. Closes #2200 ## 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/content/docs/troubleshooting.mdx` only — a new `### Windows: Defender blocks ast-grep-cli (sg.exe) during install` subsection under the existing `## Installation Issues` section, following the file's `**Symptom**` / `**Cause**` / workarounds pattern: - **Symptom** — the exact `uv tool install` failure text (`os error 225`, `Trojan:Win64/Lazy!MTB`, `sg.exe`) so users match it by search. - **Cause** — known upstream `ast-grep-cli` wheel false positive (linked); base dependency so it hits `[proxy]` too; `ast-grep` is optional at runtime and Headroom runs without it. - **Workarounds, safest first:** (1) run the proxy in Docker (no local wheel → no AV trigger); (2) restore `sg.exe` from Defender quarantine and retry (no persistent change); (3) a temporary, *scoped* Defender exclusion for `uv tool dir` during install, framed as a known false positive with a caution not to disable Defender wholesale; (4) report the false positive to Microsoft for a durable signature fix. Explicitly out of scope: making `ast-grep-cli` optional (a dependency-policy change requiring maintainer justification per CONTRIBUTING). No code change. ## 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 Docs-only; verification is fact cross-check + MDX sanity: ```text $ grep -n "ast-grep-cli>=" pyproject.toml 60: "ast-grep-cli>=0.30.0", # AST-aware code slicing (CodeCompressor); binary wheel # → confirms ast-grep-cli is a base dependency (affects [proxy] too) $ sed -n '6,7p' headroom/proxy/interceptors/astgrep.py followed by an elided body marker. Falls back to the original text if ast-grep isn't available, the extension isn't supported, or there are fewer # → confirms graceful degradation: Headroom runs without a working sg.exe $ uv tool dir C:\Users\<user>\AppData\Roaming\uv\tools # → the directory the scoped-exclusion workaround targets (via `uv tool dir`, not a hardcoded path) # MDX sanity: balanced code fences (even count), well-formed headings, links close. ``` ## Real Behavior Proof - **Environment:** Windows 11 (the affected platform), the docs source inspected against the current `main` base. - **Exact command / steps:** Issue #2200 contains a complete, exact reproduction (command `uv tool install "headroom-ai[all]"`, the `os error 225` / `Trojan:Win64/Lazy!MTB` failure on `sg.exe`, `ast-grep-cli 0.44.1`, `uv 0.11.16`, Windows 11). The documented facts are verified against the tree: base-dependency declaration (`pyproject.toml:60`) and graceful degradation (`headroom/proxy/interceptors/astgrep.py:6-7`). The `uv tool dir` command used in the exclusion workaround resolves correctly on this machine. - **Observed result:** The troubleshooting note accurately describes the failure and gives valid Windows/Defender workarounds, ordered safest-first. - **Not tested:** I deliberately did **not** run `uv tool install "headroom-ai[all]"` to force a live Defender quarantine — doing so is disruptive (it can quarantine real files and pulls the full dependency set) and machine-specific. The reproduction in the issue is complete and corroborated by the upstream ast-grep report. ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Checklist - [x] My code follows the project's style guidelines - [x] I have performed a self-review of my code - [ ] I have commented my code, particularly in hard-to-understand areas - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] I have updated the CHANGELOG.md if applicable ## Screenshots (if applicable) N/A (troubleshooting prose addition). ## Additional Notes - Test/tests-added and CHANGELOG checklist items are N/A — documentation-only change (kept to a single file, matching the merged #2031 precedent). - The durable fix for the underlying false positive belongs upstream (ast-grep) and/or with Microsoft's signature update; this PR documents supported workarounds in the meantime, as the issue requested. - Making `ast-grep-cli` an optional dependency would remove the install blocker at the source, but that's a dependency-policy change for maintainers to weigh (the interceptor already tolerates its absence) — intentionally not attempted here. |
||
|
|
996c1174a8
|
feat(proxy): show real upstream provider on dashboard for OpenAI-compatible endpoints (#1594)
## Description When the proxy runs against a custom OpenAI-compatible endpoint via `--openai-api-url` (OpenRouter, Groq, Together, Azure OpenAI, …), the dashboard always showed the provider as **OpenAI**, because the OpenAI handler records every request with `provider="openai"`. This detects well-known upstreams from the `--openai-api-url` host and adds a `--provider-name` override that takes precedence (the issue's option 3). The label is resolved only where the dashboard/stats payload is built — the internal provider key stays `openai`, so pricing and request formatting are unaffected. | Upstream URL | Provider shown | |--------------|----------------| | `https://api.openai.com/v1` | OpenAI | | `https://openrouter.ai/api/v1` | OpenRouter | | `https://api.groq.com/openai/v1` | Groq | | `https://api.together.xyz/v1` | Together AI | | `https://<resource>.openai.azure.com/` | Azure OpenAI | Unknown hosts keep the `openai` label unless `--provider-name` is set. Closes #1533 ## Type of Change - [x] New feature (non-breaking change that adds functionality) ## Changes Made - `helpers.py`: `classify_openai_upstream()` (host → display name) + `resolve_display_provider()` (precedence: `--provider-name` > host detection > raw provider; only relabels `openai`). - `models.py`: `ProxyConfig.provider_name`. - `cli/proxy.py`: `--provider-name` flag, threaded into `ProxyConfig`. - `server.py`: relabel at the four dashboard/stats display sites (recent requests, transformations feed, `requests.by_provider`, agent-usage breakdown) via the resolver / `_remap_provider_counts`. Stored logs and metrics keys are untouched. - `docs/content/docs/proxy.mdx`: document `--provider-name`. ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check`) - [x] New tests added ### Test Output ```text $ pytest tests/test_provider_display_classification.py tests/test_dashboard_agent_usage.py -q 16 passed 13 passed $ ruff check headroom/proxy/helpers.py headroom/proxy/server.py headroom/proxy/models.py headroom/cli/proxy.py All checks passed! ``` ## Real Behavior Proof - Environment: repo branch `feat/1533-upstream-provider-classify` @ HEAD, local `.venv` (Python 3) - Exact command / steps: ran the helpers directly from the venv — `python -c "from headroom.proxy.helpers import classify_openai_upstream, resolve_display_provider; print(classify_openai_upstream('https://openrouter.ai/api/v1')); print(resolve_display_provider('openai', openai_api_url='https://openrouter.ai/api/v1')); print(resolve_display_provider('openai', openai_api_url='https://openrouter.ai/api/v1', provider_name='Groq')); print(resolve_display_provider('anthropic'))"` - Observed result: host detection relabels `openai` → `OpenRouter`, `--provider-name` overrides detection (`Groq`), and the `anthropic` label (plus the `openai` pricing key) is unchanged. Full output below: ```text classify openrouter -> OpenRouter resolve openai+openrouter url -> OpenRouter override provider-name -> Groq anthropic untouched -> anthropic ``` - Not tested: live dashboard render against a real OpenRouter key (the payload-builder logic is covered by the unit tests above). ## 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 - [x] I have added tests that prove my fix is effective Co-authored-by: JerrettDavis <mxjerrett@gmail.com> |
||
|
|
560ffae103
|
feat(deploy): Add turnkey deploy command (#1404)
## Description Adds `headroom deploy` as the turnkey, zero-config local deployment entrypoint. The command chooses the most capable deployment path it can verify on the current host, configures detected tools through the existing persistent-install machinery, starts the proxy, and preserves the existing rollback behavior if an update fails. The selection order favors performance first: NVIDIA Docker GPU passthrough when `nvidia-smi` and Docker's NVIDIA runtime are available, then plain Docker, then native scheduled recovery, then a detached Python runtime fallback. ## 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 - [x] Performance improvement - [x] Code refactoring (no functional changes) ## Changes Made - Added the top-level `headroom deploy` command and reused the existing install manifest/apply/start/rollback path. - Added conservative runtime selection for GPU Docker, plain Docker, native schedulers, and detached Python fallback. - Added Docker runtime support for manifest-driven `--gpus all` passthrough. - Added tests for Docker selection, GPU Docker selection, detached fallback, GPU command rendering, and subprocess wrapper compliance. - Updated README and persistent-install docs to present the turnkey deployment flow and performance-first GPU behavior. - Allowed documented `opencode` targets through `headroom install apply --target`. ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check`) - [x] Formatting passes (`ruff format --check`) - [x] Type checking passes in local pre-commit and CI - [x] 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_cli/test_subprocess_utf8_encoding.py tests/test_cli/test_install_cli.py tests/test_install/test_runtime.py::test_build_runtime_command_for_docker_includes_gpu_passthrough tests/test_install/test_planner.py -q 47 passed in 1.57s uvx --from ruff==0.15.17 ruff check headroom/cli/install.py headroom/install/runtime.py tests/test_cli/test_install_cli.py tests/test_install/test_runtime.py --output-format concise All checks passed! uvx --from ruff==0.15.17 ruff format --check headroom/cli/install.py headroom/install/runtime.py tests/test_cli/test_install_cli.py tests/test_install/test_runtime.py 4 files already formatted ``` GitHub checks are green on the current head. ## Real Behavior Proof - Environment: Windows local worktree `C:\git\headroom`, Python 3.13 via `uv`; GitHub Actions Ubuntu/macOS/Windows runners for full PR CI. - Exact command / steps: Ran the focused deploy/install tests above, checked the touched Python files with the CI-pinned Ruff version, and confirmed the current PR head is mergeable with green GitHub checks. - Observed result: The deploy command, runtime selection, Docker GPU command rendering, install CLI behavior, and subprocess encoding coverage all pass locally; the branch is no longer conflicted. - Not tested: Actual RTX 4090 hardware passthrough on a physical NVIDIA workstation; the PR tests conservative detection and Docker command rendering without requiring GPU hardware in CI. ## 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 - CLI/runtime behavior only. ## Additional Notes CHANGELOG update is not included because this is an unreleased feature PR and the repository's release tooling owns release notes from conventional commits. |
||
|
|
6bdc8c44a3
|
docs(metrics): ship an importable Grafana dashboard (#2168)
## Description
<!-- Briefly explain the change and why it is needed. -->
The metrics docs describe the `headroom_*` Prometheus metric family and
suggest example Grafana panels, but ship no importable dashboard — users
have to build one by hand. This adds a ready-to-import Grafana dashboard
built **only** on documented metric names (`headroom_requests_total`,
`headroom_tokens_saved_total`, `headroom_tokens_input_total`, and the
`headroom_overhead_ms_*` millisecond summary), and links it from the
**Grafana Dashboard** section of `docs/content/docs/metrics.mdx`.
This is a docs/examples-only addition — no source code changes.
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
- Added `examples/grafana/headroom-dashboard.json` — a ready-to-import
Grafana dashboard (7 panels, uid `headroom-compression`) built entirely
on Headroom's documented `/metrics` names. Panels cover tokens saved,
input tokens, request rate, average processing overhead
(`headroom_overhead_ms_sum` / `headroom_overhead_ms_count` with
min/max), tokens-saved/sec, and request rate by pool. It uses **no
histograms** (the proxy emits none). The `pool`/`source` template
variables use regex matchers (`=~`) so they are optional and match
series without those labels.
- Updated `docs/content/docs/metrics.mdx` — linked the new dashboard
from the **Grafana Dashboard** section with import instructions, keeping
the existing ad-hoc PromQL query table alongside it.
## Testing
<!-- Check what you actually ran, then paste the real command output
below. -->
- [ ] Unit tests pass (`pytest`)
- [ ] Linting passes (`ruff check .`)
- [ ] Type checking passes (`mypy headroom`)
- [ ] New tests added for new functionality
- [x] Manual testing performed
Docs/examples-only change, manually verified: the dashboard JSON is
well-formed and every PromQL query references only the documented
`headroom_*` metric names from `docs/content/docs/metrics.mdx`.
### Test Output
```text
$ python3 -c "import json; d=json.load(open('examples/grafana/headroom-dashboard.json')); print('valid JSON,', len(d['panels']), 'panels, uid', d['uid'])"
valid JSON, 7 panels, uid headroom-compression
```
PromQL queries used by the panels (all against documented `headroom_*`
metrics):
```text
sum(headroom_tokens_saved_total{pool=~"$pool", hook=~"$hook"})
sum(headroom_tokens_input_total{pool=~"$pool", hook=~"$hook"})
sum(rate(headroom_requests_total{pool=~"$pool", hook=~"$hook"}[$__rate_interval]))
sum(rate(headroom_overhead_ms_sum{pool=~"$pool", hook=~"$hook"}[$__rate_interval])) / clamp_min(sum(rate(headroom_overhead_ms_count{pool=~"$pool", hook=~"$hook"}[$__rate_interval])), 1)
sum(rate(headroom_tokens_saved_total{pool=~"$pool", hook=~"$hook"}[$__rate_interval])) by (pool)
max(headroom_overhead_ms_max{pool=~"$pool", hook=~"$hook"})
min(headroom_overhead_ms_min{pool=~"$pool", hook=~"$hook"})
sum(rate(headroom_requests_total{pool=~"$pool", hook=~"$hook"}[$__rate_interval])) by (pool)
```
## Real Behavior Proof
- Environment: local checkout of the PR branch; Python 3 for JSON
validation.
- Exact command / steps: ran the JSON-validation command above (see Test
Output) — parses cleanly, reports 7 panels and uid
`headroom-compression`; then read every panel target and confirmed each
PromQL query references only metric names documented in
`docs/content/docs/metrics.mdx` (`headroom_requests_total`,
`headroom_tokens_saved_total`, `headroom_tokens_input_total`,
`headroom_overhead_ms_{sum,count,min,max}`). No histogram metrics are
referenced.
- Observed result: JSON is valid and importable via Grafana's
**Dashboards → New → Import → Upload**; no datasource UID is hard-coded,
so the importer prompts for a Prometheus datasource. Queries match the
documented metric family.
- Not tested: a full live Grafana import against a running proxy
scraping real `/metrics` was not performed in CI. Verification was
limited to JSON validity and query/metric-name correctness against the
documented metrics.
## Review Readiness
- [x] I have performed a self-review
- [x] This PR is ready for human review
Additive docs/examples only — no source code, tests, or runtime behavior
changed.
## Checklist
- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] I have updated the CHANGELOG.md if applicable
## Screenshots (if applicable)
N/A — dashboard is imported from JSON; see the PromQL and panel list
above.
## Additional Notes
<!-- Mention any N/A checklist items, tradeoffs, follow-ups, or
maintainer context. -->
Test-related checklist items are N/A: this is an additive docs/examples
change with no application code, so `pytest`/`mypy`/`ruff` and new unit
tests do not apply. The dashboard JSON was validated and its queries
checked against the documented metric names instead.
---------
Co-authored-by: Tejas Chopra <chopratejas@gmail.com>
|
||
|
|
f9f3162d38
|
docs(proxy): document HEADROOM_SAVINGS_PROFILE and correct --mode default (#2031) (#2040)
## Description
`HEADROOM_SAVINGS_PROFILE` is an implemented env var
(`headroom/agent_savings.py`) that selects a named profile bundling
Headroom's whole compression posture (proxy mode, keep-ratio, which
messages are compressed, `force_kompress`, etc.) at proxy startup. It
was entirely undocumented — `grep` over `docs/` found zero mentions.
Related, the proxy docs were **misleading about the default optimization
mode**: `docs/content/docs/proxy.mdx` stated `--mode` defaults to
`token`, but the code default is `cache`:
```python
# headroom/cli/proxy.py — the Click option has no default
@click.option("--mode", default=None, ...)
# ... mode resolution (default is CACHE):
effective_mode = normalize_proxy_mode(mode or os.environ.get("HEADROOM_MODE") or PROXY_MODE_CACHE)
```
A bare `headroom proxy` (no `--mode`, no `HEADROOM_MODE`) runs in
**cache** mode, and the default `coding` savings profile also sets
`proxy_mode="cache"` — which is exactly what the issue reporter found
confusing.
This documents `HEADROOM_SAVINGS_PROFILE` and corrects the `--mode`
default rows so the doc is accurate and internally consistent.
Closes #2031
## 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/content/docs/proxy.mdx` only:
- Corrected the `--mode` default in the Core-options table and the
Context-management table (`token` → `cache`), each pointing to the new
Savings profiles section for the reason.
- Added a `### Savings profiles` section documenting: the
`HEADROOM_SAVINGS_PROFILE` env var; a table of the four built-in
profiles (`coding` default, `balanced` fallback, `agent-90`, `general`)
with target savings, mode, and `force_kompress`; the unset→`coding`
default; the unknown-value→`balanced` warning-and-fallback (proxy never
fails to start); and the mode precedence (explicit `--mode` >
`HEADROOM_MODE` seeded by a profile > `cache` default), with an example.
No code change. Every documented value is pinned to
`headroom/agent_savings.py` (profile definitions) and
`headroom/cli/proxy.py` (default-mode resolution).
## Testing
- [x] Unit tests not run; docs-only source verification performed
- [x] Linting not run; docs-only MDX/source verification performed
- [x] Type checking not applicable; no Python code changed
- [x] New tests not applicable; documentation-only correction
- [x] Manual testing performed
### Test Output
Docs-only change; verification is cross-checking every documented value
against the source of truth:
```text
$ grep -n "DEFAULT_PROFILE = \|FALLBACK_PROFILE = " headroom/agent_savings.py
14:FALLBACK_PROFILE = "balanced"
18:DEFAULT_PROFILE = "coding"
# profile modes / knobs (agent_savings.py):
# coding → proxy_mode="cache", force_kompress=False, target_ratio=None (emergent)
# balanced → proxy_mode="token", force_kompress=False, target_ratio=0.30
# agent-90 → proxy_mode="token", force_kompress=True, target_ratio=0.10
# general → proxy_mode="token", force_kompress=False, target_ratio=None (emergent)
$ grep -n "effective_mode\|PROXY_MODE_CACHE" headroom/cli/proxy.py
# effective_mode = normalize_proxy_mode(mode or os.environ.get("HEADROOM_MODE") or PROXY_MODE_CACHE)
# → confirms the real default optimization mode is cache, not token
```
MDX sanity: code fences balance (even count) and the `### Savings
profiles` heading slugifies to `#savings-profiles`, matching the two
in-page anchor links added to the mode rows.
## Real Behavior Proof
- **Environment:** Windows 11; docs source inspected against the working
tree at the current `main` base.
- **Exact command / steps:** Each documented fact is grounded in code —
profile names, modes, `force_kompress`, and target ratios come from
`headroom/agent_savings.py:_PROFILES`; the default profile (`coding`)
from the `os.environ.get("HEADROOM_SAVINGS_PROFILE") or "coding"` reads
in `headroom/cli/proxy.py` and `headroom/proxy/server.py`; the `cache`
default mode from `headroom/cli/proxy.py`'s `mode or HEADROOM_MODE or
PROXY_MODE_CACHE`; the unknown-value fallback from
`get_agent_savings_profile` (`agent_savings.py`).
- **Observed result:** The new section's table and prose match those
sources exactly, and the previously-wrong `--mode` default rows now
state `cache`.
- **Not tested:** A live render of the Fumadocs/Next.js docs site (no
local docs build run here) — the change is MDX-syntax-valid (balanced
fences, well-formed table, standard heading-anchor slug).
## 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] Code comments not applicable; documentation-only change
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] Tests not applicable; docs-only facts verified against source
- [x] New and existing unit tests pass locally with my changes
- [x] CHANGELOG not applicable; documentation-only correction
## Screenshots (if applicable)
N/A (docs prose/table addition; a rendered screenshot can be added if
the docs site is built for preview).
## Additional Notes
- Test/tests-added checklist items are N/A — this is a
documentation-only change.
- Out of scope (intentionally): the `--mode` Click **help text** in
`headroom/cli/proxy.py` also says "default: token" and is likewise
inaccurate, but correcting Python help text is a code change beyond this
docs issue — noted as a possible follow-up.
---------
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
|
||
|
|
4ea96a417c
|
feat(mcp): add streamable HTTP MCP transport (#1773)
## Description `headroom mcp serve` only exposed stdio, which blocked MCP clients that require a Streamable HTTP endpoint. This PR adds an explicit HTTP transport mode around the existing Headroom MCP server while keeping stdio as the default and keeping tool registration single-sourced. Closes #1346. ## Type of Change - [ ] Bug fix (non-breaking change that fixes an issue) - [x] New feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [x] Documentation update - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - Add `headroom mcp serve --transport http` with host, port, and path options. - Serve `headroom_compress`, `headroom_retrieve`, and `headroom_stats` through the same MCP server instance used by stdio. - Keep `headroom mcp serve` defaulting to stdio for current Claude Code and local MCP host configs. - Update MCP docs for stdio and HTTP setup without implying the proxy automatically owns `/mcp`. - Keep the scope clean, rebased, and covered by focused tests. ## Testing - [x] Unit tests pass (`uv run pytest tests/test_ccr_mcp_http.py tests/test_cli/test_mcp.py -q`) - [x] Linting passes (`uv run ruff check headroom/cli/mcp.py headroom/ccr/mcp_server.py headroom/ccr/mcp_http.py tests/test_ccr_mcp_http.py tests/test_cli/test_mcp.py`) - [x] Type checking passes (`uv run mypy headroom --ignore-missing-imports`) - [x] New tests added for new functionality when applicable - [x] Manual testing performed ### Test Output ```text uv run pytest tests/test_ccr_mcp_http.py tests/test_cli/test_mcp.py -q 20 passed in 0.53s uv run ruff check headroom/cli/mcp.py headroom/ccr/mcp_server.py headroom/ccr/mcp_http.py tests/test_ccr_mcp_http.py tests/test_cli/test_mcp.py All checks passed! uv run ruff format headroom/cli/mcp.py headroom/ccr/mcp_server.py headroom/ccr/mcp_http.py tests/test_ccr_mcp_http.py tests/test_cli/test_mcp.py --check 5 files already formatted uv run mypy headroom --ignore-missing-imports Success: no issues found in 407 source files ``` ## Real Behavior Proof - Environment: Local Python environment with Headroom dev dependencies and MCP extra installed. - Exact command / steps: Start `headroom mcp serve --transport http --host 127.0.0.1 --port <test-port> --path /mcp`, then perform an MCP SDK Streamable HTTP initialize/list-tools exchange. - Observed result: The HTTP transport initializes and lists the existing Headroom MCP tools; `headroom mcp serve` without `--transport` still selects stdio, and mixed-case `--transport HTTP` routes to the HTTP transport. - Not tested: live validation against external MCP hosts ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Additional Notes `CHANGELOG.md` is not edited because this repository generates changelog entries from conventional commits. Full-suite validation is left to CI. |
||
|
|
c46cd8f950
|
fix(core): load ONNX Runtime dynamically so headroom._core imports on non-AVX2 x86-64 (#1715)
## Description
`import headroom._core` dies with SIGILL (`Illegal instruction`) on
x86-64 CPUs without AVX2 (Pentium N4200, Celeron N4500, AMD FX 8350 —
all reported on the issue). The repo sets no `RUSTFLAGS`/`target-cpu`
anywhere, so first-party Rust code is baseline x86-64; the AVX2 code
comes from Microsoft's prebuilt ONNX Runtime, statically linked into the
extension by fastembed's `ort-download-binaries-rustls-tls` feature on
non-Windows targets. Because it is statically linked, its code is mapped
and initialized when the extension module loads — **before** the runtime
AVX2 guard from #1162 can run, which is why that fix helped Magika init
but not the import-time crash.
Fix, mirroring what Windows already does for its own reasons (DirectML
link libs): build with `ort-load-dynamic` on every platform, so ONNX
Runtime is only `dlopen`'d at first use, where the #1162 AVX2 guard
falls back to the non-ONNX detection tiers on unsupported CPUs. Since
both target blocks became identical, they are collapsed into one
platform-independent `fastembed` dependency.
To keep Magika/fastembed working out of the box on Linux/macOS, the
existing `ORT_DYLIB_PATH` auto-pin (`headroom/_ort.py`, previously
Windows-only) now resolves the pip `onnxruntime` package's shared
library on all platforms (`onnxruntime.dll` / `libonnxruntime.so*` /
`libonnxruntime*.dylib`). The pip `onnxruntime` CPU wheels use runtime
CPU dispatch, so they also work on pre-AVX2 machines — non-AVX2 users
get working ML detection instead of a crash. Without the `onnxruntime`
package, ML detection degrades gracefully to the non-ONNX tiers exactly
as it already does on Windows.
Fixes #1278
## 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
- `crates/headroom-core/Cargo.toml`: replaced the per-target `fastembed`
blocks (`ort-download-binaries-rustls-tls` on non-Windows,
`ort-load-dynamic` on Windows) with a single platform-independent
dependency on `ort-load-dynamic`, with a comment documenting both the
DirectML and the AVX2/#1278 rationale.
- `Cargo.lock`: regenerated — `ort-sys` drops its static-download
dependencies (`hmac-sha256`, `lzma-rust2`, `ureq`); no version bumps.
- `headroom/_ort.py`: `ORT_DYLIB_PATH` auto-pin extended from
Windows-only to all platforms via a small `_find_dylib` helper that
resolves the platform's shared-library name inside the pip `onnxruntime`
package.
- `tests/test_transforms/test_ort_dylib.py`: replaced the obsolete
`test_noop_on_non_windows` with Linux (versioned `.so`) and macOS
(`.dylib`) pin tests; module docstring updated.
- `docs/content/docs/configuration.mdx`: `ORT_DYLIB_PATH` row updated
from Windows-only wording to the cross-platform behavior.
## 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
$ cargo fmt --all -- --check && cargo clippy --workspace -- -D warnings && cargo test -p headroom-core --lib
clean
test result: 844 passed; 0 failed; 1 ignored
$ python -m pytest tests/test_transforms/test_ort_dylib.py -q
8 passed
$ ruff check headroom/_ort.py tests/test_transforms/test_ort_dylib.py
All checks passed!
```
## Real Behavior Proof
- Environment: Windows 11 (AVX2-capable — the SIGILL itself is not
reproducible on this machine), Python 3.13, Rust 1.95.0, local checkout
branched from `upstream/main` (
|
||
|
|
e9e9cd55b7
|
feat(mcp): publish canonical server.json (#1510)
## Description Headroom can launch its MCP server, but did not publish a canonical `server.json` that registries and MCP hosts can consume directly. This PR adds a shared descriptor builder, commits a root `server.json`, parity-tests that artifact against the builder and existing runtime spec, and updates docs so registry authors do not need to reconstruct `headroom mcp serve` from prose. Closes #929. ## 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 a shared `server_json.py` descriptor builder for Headroom MCP publication metadata. - Published a canonical root `server.json` and parity-tested it against the builder. - Encoded the publishable uvx contract as `headroom-ai[mcp]` plus `headroom mcp serve`. - Updated README and MCP docs to point registry authors at the canonical descriptor. - Added the README ownership marker used by MCP Registry verification. - Kept existing registrars and `headroom mcp install` behavior unchanged. ## Testing - [x] Unit tests pass - [x] Linting passes - [x] Type checking passes - [x] New tests added for new functionality when applicable - [x] Manual testing performed ### Test Output ```text Focused registry/server-json tests and reviewer approval were completed on this PR before the governance body cleanup. The current body update is documentation-only metadata for PR governance. ``` ## Real Behavior Proof - Environment: Headroom development checkout with MCP test dependencies. - Exact command / steps: Inspected the generated `server.json` contract and parity coverage against the descriptor builder and runtime MCP spec. - Observed result: The committed descriptor matches the builder/runtime contract and advertises the intended `headroom-ai[mcp]` / `headroom mcp serve` launch path. - Not tested: live publication to third-party registries ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Additional Notes This body was normalized by a maintainer after approval so the governance parser reflects the already-reviewed PR state. |
||
|
|
b0fa84e84d
|
fix: add Vercel deploy config and workflow for docs site (#1739)
## Description The Vercel docs site at headroom-docs.vercel.app had no automated deployment pipeline, so newly added pages (persistent-installs, savings) return 404 despite existing in the repo and building correctly locally. Closes #1730 ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) - [ ] New feature (non-breaking function added) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - Add docs/vercel.json with explicit Next.js project config (framework, build/install commands) - Add deploy-vercel job to .github/workflows/docs.yml to auto-deploy on pushes to main touching docs/** ## 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 ``` Local build verification: cd docs && npm ci && npm run build Build succeeded - persistent-installs and savings pages generated at .next/server/app/docs/persistent-installs.html and .next/server/app/docs/savings.html ``` ## Real Behavior Proof - Environment: Linux x86_64, Node.js 20 - Exact command / steps: 1. cd docs && npm ci && npm run build 2. Checked .next/server/app/docs/ for generated HTML artifacts 3. Verified source.getPage(["persistent-installs"]) returns page object - Observed result: Both pages build and render correctly locally - Not tested: Live Vercel deployment requires maintainer secrets (VERCEL_TOKEN, VERCEL_ORG_ID, VERCEL_PROJECT_ID) ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Additional Notes Requires three repo secrets: VERCEL_TOKEN, VERCEL_ORG_ID, VERCEL_PROJECT_ID. |
||
|
|
e6df6ea470
|
docs: qualify CCR auto-resolution support for Gemini (#2044)
## Description Headroom's CCR docs describe automatic response handling as universal, but the current code only wires that continuation path for Anthropic and OpenAI-compatible handlers. This updates the docs to describe the real Gemini behavior today, including the native Gemini gap and the reported `MALFORMED_FUNCTION_CALL` risk on Gemini OpenAI-compatible round-2 continuations. Refs #2041 ## Type of Change - [x] Documentation update - [ ] Bug fix (non-breaking change that fixes an issue) - [ ] New feature (non-breaking change that adds functionality) - [ ] Breaking change - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - Narrow CCR response-handler claims to the providers that currently implement them. - Add a Gemini-specific note covering native-handler limits and the reported round-2 continuation failure. ## Testing - [x] Unit tests pass - [ ] Linting passes - [ ] Type checking passes - [ ] New tests added for new functionality when applicable - [x] Manual testing performed ### Test Output ```text uv run --no-sync pytest tests/test_ccr_response_handler.py tests/test_ccr_response_handler_extra.py -q ============================= test session starts ============================= platform win32 -- Python 3.12.13, pytest-9.0.3, pluggy-1.6.0 rootdir: D:\Repos\headroom-pr-2041-gemini-ccr-docs configfile: pyproject.toml plugins: anyio-4.12.1, langsmith-0.9.3, asyncio-1.3.0, cov-7.0.0 asyncio: mode=Mode.AUTO, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function collected 42 items tests\test_ccr_response_handler.py ............................... [ 73%] tests\test_ccr_response_handler_extra.py ........... [100%] ============================= 42 passed in 0.91s ============================== ``` ## Real Behavior Proof - Environment: Windows, Python 3.12.13, docs-only change with no live Gemini provider call - Exact command / steps: `uv run --no-sync pytest tests/test_ccr_response_handler.py tests/test_ccr_response_handler_extra.py -q` - Observed result: All 42 CCR response-handler tests pass, confirming the existing Anthropic/OpenAI-compatible continuation behavior is unchanged by the docs update - Not tested: a live Gemini round-2 continuation request ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review Co-authored-by: Tejas Chopra <chopratejas@gmail.com> |
||
|
|
81ddbd47d5
|
docs: document Claude VSCode deferred-tool rendering caveat (#2045)
## Description Headroom already documents why `ENABLE_TOOL_SEARCH=true` matters for Claude Code through a custom `ANTHROPIC_BASE_URL`, but it does not document the current VSCode extension rendering failure on the deferred-tool content blocks that setting can surface. This adds a narrow docs warning and workaround for the VSCode path without changing the CLI default that still helps the main Claude Code flow. Refs #2028 ## Type of Change - [x] Documentation update - [ ] Bug fix (non-breaking change that fixes an issue) - [ ] New feature (non-breaking change that adds functionality) - [ ] Breaking change - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - Document the Claude Code VSCode extension `unsupported content type` failure mode. - Explain when to set `ENABLE_TOOL_SEARCH=false` as a workaround. - Keep the existing default guidance for Claude CLI users unchanged. ## Testing - [x] Unit tests pass - [ ] Linting passes - [ ] Type checking passes - [ ] New tests added for new functionality when applicable - [x] Manual testing performed ### Test Output ```text uv run --no-sync pytest tests/test_cli_doctor.py -q ============================= test session starts ============================= platform win32 -- Python 3.12.13, pytest-9.0.3, pluggy-1.6.0 rootdir: D:\Repos\headroom-pr-2028-claude-vscode-tool-search-docs configfile: pyproject.toml plugins: anyio-4.12.1, langsmith-0.9.3, asyncio-1.3.0, cov-7.0.0 asyncio: mode=Mode.AUTO, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function collected 51 items tests\test_cli_doctor.py ............................................... [ 92%] .... [100%] ============================= 51 passed in 0.67s ============================== ``` ## Real Behavior Proof - Environment: Windows, Python 3.12.13, docs-only change with no LLM provider involved - Exact command / steps: `uv run --no-sync pytest tests/test_cli_doctor.py -q` - Observed result: All 51 `test_cli_doctor.py` tests pass, confirming the existing `headroom doctor` CLI behavior is unchanged by the new VSCode troubleshooting docs - Not tested: live rendering in the Claude Code VSCode extension ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Additional Notes The extension renderer bug is upstream. This PR only makes the current Headroom behavior explicit and gives users the supported workaround. |
||
|
|
d2fb562709
|
docs(proxy): document savings profiles section (#2091)
## Description Closes #2031 Add a new "Savings profiles" section to the proxy documentation, covering the four built-in profiles (`coding`, `agent-90`, `balanced`, `general`), their key parameters and use cases, how profiles override CLI flags like `--mode`, and how to extend them with env overrides. ## Type of Change - [ ] Bug fix (non-breaking) - [ ] New feature (non-breaking) - [ ] Breaking change - [x] Documentation update ## Changes Made - `docs/content/docs/proxy.mdx`: Added "Savings profiles" section between the CLI options callout and API endpoints, documenting: - How to switch profiles via `HEADROOM_SAVINGS_PROFILE` - Table of 4 built-in profiles with their key params - Detailed description of each profile's behavior - How `proxy_mode` overrides `--mode` CLI flag - Extending profiles with individual env overrides - Pointer to `headroom/agent_savings.py` for custom profiles ## Testing - [x] Verified doc builds and renders correctly - [x] Confirmed only doc file changed ``` $ git diff upstream/main...HEAD --name-only docs/content/docs/proxy.mdx $ grep -c "Savings profiles" docs/content/docs/proxy.mdx 1 ``` ## Real Behavior Proof - Environment: headroom main branch - Exact command / steps: `git diff upstream/main...HEAD --name-only` - Observed result: `docs/content/docs/proxy.mdx` (one file, doc-only change) - Not tested: N/A ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review --------- Co-authored-by: lennney <lennney@users.noreply.github.com> Co-authored-by: JD Davis <mxjerrett@gmail.com> |
||
|
|
1d2b76e72e
|
fix: harden persistent install startup (#1851)
## Description Hardens persistent install startup and proxy compression behavior for issue #1843. Repeated `headroom install start` / scheduled ensure calls no longer spawn duplicate runtimes by default, and `/v1/compress` now fails open on compression timeout instead of returning a 503. The PR also adds a machine-readable platform feature matrix and app-level stabilization tests for health, compression functionality, timeout behavior, and matrix evidence. Refs #1843 ## 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 - [x] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - Wrapped direct persistent deployment starts with the existing profile-local runtime start lock. - Made `headroom install start` idempotent when the deployment is already healthy. - Added wedged-runtime handling: if a PID is running but `/readyz` does not recover inside the grace window, stop it before starting again. - Kept `install agent ensure` inside the already-held lock while delegating to the shared start helper. - Changed `/v1/compress` timeout behavior from `503 compression_timeout` to fail-open `200` with original messages, `compression_skipped: true`, and `skip_reason: compression_timeout`. - Added `tests/test_platform_stabilization_functional.py` covering real FastAPI health/compression routes, successful compression metrics, timeout fail-open speed, and a real JSON tool payload that reduces tokens. - Added `docs/platform-feature-matrix.json` and `docs/platform-stabilization.md` for Linux/macOS/Windows hardening coverage and known gaps. - Strengthened matrix tests so cited local test/workflow paths must exist. ## 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 # Clean source tree without copied Rust extension: functional module is skipped locally, as CI copies _core from the built wheel. > python -m pytest tests/test_install tests/test_cli/test_install_cli.py tests/test_platform_feature_matrix.py tests/test_platform_stabilization_functional.py -q collected 120 items / 1 skipped 119 passed, 2 skipped in 17.08s > python -m ruff check headroom/proxy/handlers/openai.py tests/test_platform_stabilization_functional.py tests/test_platform_feature_matrix.py All checks passed! # Local Windows compiled-core proof: > python -m maturin build --profile ci --out dist-local Built wheel for abi3 Python >= 3.10 to dist-local\headroom_ai-0.29.0-cp310-abi3-win_amd64.whl # Copied _core.pyd from the wheel into headroom/ for local route execution, then: > python -m pytest tests/test_platform_stabilization_functional.py -q collected 4 items 4 passed in 6.71s > python -m pytest tests/test_install tests/test_cli/test_install_cli.py tests/test_platform_feature_matrix.py -q collected 120 items 119 passed, 1 skipped in 17.16s Commit hooks: Sync plugin versions.....................................................Passed check for merge conflicts................................................Passed ruff.....................................................................Passed ruff-format..............................................................Passed mypy.....................................................................Passed ``` ## Real Behavior Proof - Environment: Windows 11, PowerShell, Python 3.13.13, worktree `C:\git\headroom-stabilization` on branch `jd/cross-platform-stabilization`. - Exact command / steps: built the Windows wheel with `maturin`, extracted `_core.pyd`, ran the new FastAPI route tests and install/matrix tests listed above, then removed generated artifacts before committing. - Observed result: direct start paths now no-op when healthy, skip spawning when the start lock is contended, and stop a wedged runtime before restart. `/v1/compress` now returns original messages quickly on timeout instead of a 503. The real JSON tool-payload smoke test returns `tokens_before > tokens_after`, `tokens_saved > 0`, `compression_ratio < 1.0`, and non-empty transforms through the public route. - Not tested: full native Windows persistent process e2e remains blocked by the upstream CRT/wheel issue already documented in workflows and in the matrix. No real OS service was installed locally; service manager behavior is covered by argument-level unit tests. ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Checklist - [x] My code follows the project's style guidelines - [x] I have performed a self-review of my code - [x] I have commented my code, particularly in hard-to-understand areas - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] I have updated the CHANGELOG.md if applicable ## Screenshots (if applicable) N/A ## Additional Notes CHANGELOG is not updated because this is an unreleased hardening/test/documentation pass. The platform matrix intentionally records partial/blocked Windows/macOS e2e gaps instead of claiming full coverage where the repo cannot currently run it. |
||
|
|
abc557a5dc
|
[codex] Document local LLM prefill benchmarking (#1396)
## Summary - add a Local LLM Prefill Benchmark docs page for baseline-vs-optimized proxy testing - document the `--no-optimize` baseline, optimized rerun, dashboard comparison, and optional `--learn` condition - link the workflow from the proxy and benchmarks docs ## Context This captures the local-inference workflow shown in Joe Maddalone's June 2026 Headroom demo: Headroom can improve local model prompt-processing time by sending fewer prompt tokens, even when token cost is not the main concern. ## Validation - `npm --prefix docs run types:check` - `npm --prefix docs run build` ## Notes - This PR is independent from #1395, which covers Codex audit/maturation evidence. Co-authored-by: Robert Briscoe <robert@briscoe.dev> |
||
|
|
75fff43eca
|
deps: bump @types/node from 25.5.2 to 26.1.1 in /docs (#1683)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 25.5.2 to 26.1.1. <details> <summary>Commits</summary> <ul> <li>See full diff in <a href="https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node">compare view</a></li> </ul> </details> <br /> Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
e8b66a27e1
|
deps: bump fumadocs-typescript from 4.0.14 to 5.3.0 in /docs (#1684)
Bumps [fumadocs-typescript](https://github.com/fuma-nama/fumadocs) from 4.0.14 to 5.3.0. <details> <summary>Release notes</summary> <p><em>Sourced from <a href="https://github.com/fuma-nama/fumadocs/releases">fumadocs-typescript's releases</a>.</em></p> <blockquote> <h2>fumadocs-typescript@5.3.0</h2> <h3>Default to Base UI</h3> <p>Internal packages & templates now use Base UI rather than Radix UI.</p> <h2>fumadocs-typescript@5.2.7</h2> <h3>Migrate to <code>cnfast</code></h3> <p>Drop <code>tailwind-merge</code>.</p> </blockquote> </details> <details> <summary>Commits</summary> <ul> <li><a href=" |
||
|
|
bb2acf700a
|
fix(proxy): honor x-headroom-base-url on /v1/messages route (#1763)
## Description The Anthropic Messages route (`POST /v1/messages`) ignored the `x-headroom-base-url` per-request upstream override and unconditionally forwarded to `api.anthropic.com`. `handle_anthropic_messages` already accepts `upstream_base_url` (it builds the upstream URL via `build_copilot_upstream_url`), but the route never passed it. Clients that speak the Anthropic Messages wire format while authenticating against a non-Anthropic gateway (e.g. OpenCode Zen's "Go" tier) were forwarded to the real Anthropic API, which rejected the gateway key with `401 invalid x-api-key`. The route now reads and trims `x-headroom-base-url` and passes it through as `upstream_base_url`, mirroring the OpenAI-compatible routes and the generic passthrough route (`proxy_routes.py:996`). Closes #1760 ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) - [ ] New feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - `headroom/providers/proxy_routes.py`: the `/v1/messages` route reads `x-headroom-base-url`; when present it strips whitespace and a trailing slash and passes the value as `upstream_base_url` to `handle_anthropic_messages`. Absent or whitespace-only headers keep the previous default (`api.anthropic.com`). - `tests/test_proxy/test_anthropic_upstream_header.py`: new test module pinning the route contract (header present, absent, empty, whitespace-only, trimming + trailing-slash stripping). - `docs/content/docs/configuration.mdx`: new "Proxy upstream override (`x-headroom-base-url`)" subsection under Per-Request Overrides documenting the header across the OpenAI, Anthropic Messages, and passthrough routes. - `CHANGELOG.md`: `Unreleased > Fixed` entry for the `/v1/messages` override. ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [x] Type checking passes (`mypy headroom`) - [x] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text $ python -m pytest tests/test_proxy/ -k "anthropic or passthrough or bedrock" collected 140 items / 91 deselected / 49 selected tests/test_proxy/test_anthropic_upstream_header.py .... [ 65%] ... 49 passed, 91 deselected, 1 warning in 79.68s $ ruff check headroom/providers/proxy_routes.py tests/test_proxy/test_anthropic_upstream_header.py All checks passed! $ mypy headroom/providers/proxy_routes.py Success: no issues found in 1 source file ``` ## Real Behavior Proof Ran the actual `headroom proxy` against a local mock upstream (a tiny HTTP server on `127.0.0.1:9911` that logs the path it receives) to reproduce the issue's before/after. - Environment: local, macOS, Python 3.12; ran `headroom proxy --port 8799` against a local mock upstream (a tiny HTTP server on `127.0.0.1:9911` that logs the path it receives). - Exact command / steps: started the proxy and the mock upstream, then sent one `POST /v1/messages` **with** the override header and one **without** it (negative control), using these two `curl` commands. ```bash # WITH the override header — expect routing to the mock at 127.0.0.1:9911 curl http://127.0.0.1:8799/v1/messages \ -H "content-type: application/json" -H "anthropic-version: 2023-06-01" \ -H "x-headroom-base-url: http://127.0.0.1:9911" \ -H "x-api-key: zen-test-key" \ -d '{"model":"glm-5.2","max_tokens":16,"messages":[{"role":"user","content":"hi"}]}' # WITHOUT the override header — expect routing to the real api.anthropic.com curl http://127.0.0.1:8799/v1/messages \ -H "content-type: application/json" -H "anthropic-version: 2023-06-01" \ -H "x-api-key: sk-ant-fake" \ -d '{"model":"claude-3-5-sonnet-20241022","max_tokens":16,"messages":[{"role":"user","content":"hi"}]}' ``` - Observed result: with the header, the mock upstream logged `HIT path=/v1/messages x-api-key=zen-test-key` and the proxy returned `HTTP 200`, confirming the request was routed to `<x-headroom-base-url>/v1/messages` carrying the gateway key. Without the header, the request went to the real `api.anthropic.com` (returned `HTTP 401` with a genuine `request_id` and `{"type":"authentication_error","message":"invalid x-api-key"}`) and the mock received no additional hit — matching the pre-fix behavior in the issue. Also verified by TDD: the two override unit cases failed before the route change (`assert None == 'https://opencode.ai/zen/go'`) and passed after it; all 4 new cases and 49 related proxy tests are green. - Not tested: a request against the real OpenCode Zen gateway (no credentials); the gateway path is verified with a local mock upstream instead. ## 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 - Manual testing against the real OpenCode Zen gateway is N/A (no credentials); a local mock upstream is used instead to prove the routing (see Real Behavior Proof). - Scope is limited to `/v1/messages`. The related `/v1/messages/count_tokens` route uses a fixed passthrough target and is out of scope for this issue. |
||
|
|
361adcd1a0
|
fix(dashboard): distinguish unavailable RTK from zero stats in Docker (#1901)
## Description Dockerized Headroom shows `0` for RTK/context-tool dashboard figures whenever the `rtk` binary isn't reachable inside the proxy's runtime — indistinguishable from "genuinely nothing saved yet." The backend already computes this distinction (an `installed`/`available` flag on the context-tool stats payload) but it never reaches two of the JSON surfaces the dashboard reads from, and the dashboard template never checks the one surface that already has it. This PR threads that existing availability flag through to both surfaces and updates the dashboard to show a distinct "not installed" message instead of a bare `0`, plus a short Docker note so operators know `rtk` needs to be installed inside the container for those figures to populate at all. Closes #1831 ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) - [ ] New feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - `headroom/proxy/server.py`: reuse the existing context-tool `installed` flag as one `available` boolean, add it to `savings.by_layer.cli_filtering` in `/stats`, and add it to the curated `cli_filtering` block in `/stats-history`; corrected that endpoint's stale docstring claim that `cli_filtering` is `None` whenever RTK is absent. - `headroom/dashboard/templates/dashboard.html`: added `cliFilteringAvailable`/`historyCliFilteringAvailable` getters and used them to show a "not installed" message instead of `0` in the session view's Token Usage panel and Token Savings breakdown, and to keep the Historical tab's lifetime card hidden (its existing behavior) instead of showing a stale zero. - `docker-compose.yml` and `docker/docker-compose.native.yml`: added a one-line comment noting that `rtk` needs to be installed inside the container for CLI-filtering dashboard figures to populate. - `docs/content/docs/docker-install.mdx`: added a note to the existing Notes section about the same requirement. - Added focused pytest coverage for the new JSON field on both endpoints (installed, not-installed, and hard-failure cases) and a new Playwright spec covering the rendered not-installed / genuine-zero / Historical-tab states. ## Testing - [x] Unit tests pass (`uv run pytest tests/test_proxy_dashboard_stats_cache.py tests/test_proxy_savings_history.py -q`) - [x] Linting passes (`uv run ruff check .`) - [ ] Type checking passes (`uv run mypy headroom`) or explain N/A truthfully - [x] New tests added for new functionality when applicable - [x] Manual testing performed ### Test Output ```text uv run pytest tests/test_proxy_dashboard_stats_cache.py tests/test_proxy_savings_history.py -q 51 passed, 1 skipped, 1 failed uv run ruff check headroom/proxy/server.py tests/test_proxy_dashboard_stats_cache.py tests/test_proxy_savings_history.py tests/test_dashboard_context_tool_availability_playwright.py All checks passed! ``` The one failure (`test_savings_tracker_save_fsyncs_parent_directory`) is pre-existing and unrelated to this change; it reproduces identically on a clean `origin/main` checkout with this diff removed (Windows filesystem fsync behavior). ## Real Behavior Proof - Environment: Windows sandbox, Python (uv-managed), no live Docker container - Exact command / steps: `GET /stats` and `GET /stats-history` against a `TestClient` app with the context-tool stats source monkeypatched to a not-installed payload (mirrors the exact shape `_context_tool_zero_payload` produces when `rtk` is absent), then the same with an installed-but-zero payload - Observed result: `savings.by_layer.cli_filtering.available` and `/stats-history`'s `cli_filtering.available` are `False` for the not-installed payload and `True` for the installed-but-zero payload, matching the pre-existing `context_tool.available` field; the new Playwright spec exercises the corresponding dashboard rendering states and runs in CI's "Dashboard Playwright" check - Not tested: real rendering in a live browser against a live Docker container (this sandbox cannot run the CI-only Dashboard Playwright job locally); the fix is proved locally at the JSON-contract level and the rendering claim is proved by the contributed CI-executed Playwright spec ## 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 - [ ] New and existing unit tests pass locally with my changes - [x] I have updated the CHANGELOG.md if applicable ## Additional Notes CHANGELOG.md was intentionally left unchanged — release automation derives changelog entries from conventional commits per this repo's convention, and this is a dashboard/docs clarity fix rather than a new user-facing command or config option. Type checking was not re-run in isolation for this change; it's covered by the repo's CI lint job. |
||
|
|
8872bbc6a2
|
deps: bump the npm-minor-patch group across 4 directories with 18 updates (#1907)
Bumps the npm-minor-patch group with 12 updates in the /docs directory: | Package | From | To | | --- | --- | --- | | [fumadocs-core](https://github.com/fuma-nama/fumadocs) | `16.10.3` | `16.11.1` | | [fumadocs-mdx](https://github.com/fuma-nama/fumadocs) | `15.0.12` | `15.1.0` | | [fumadocs-twoslash](https://github.com/fuma-nama/fumadocs) | `3.1.15` | `3.3.0` | | [fumadocs-ui](https://github.com/fuma-nama/fumadocs) | `16.10.3` | `16.11.1` | | [next](https://github.com/vercel/next.js) | `16.2.6` | `16.2.10` | | [react](https://github.com/facebook/react/tree/HEAD/packages/react) | `19.2.4` | `19.2.7` | | [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) | `19.2.14` | `19.2.17` | | [react-dom](https://github.com/facebook/react/tree/HEAD/packages/react-dom) | `19.2.4` | `19.2.7` | | [recharts](https://github.com/recharts/recharts) | `3.8.1` | `3.9.2` | | [@tailwindcss/postcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-postcss) | `4.2.2` | `4.3.2` | | [@types/mdx](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/mdx) | `2.0.13` | `2.0.14` | | [postcss](https://github.com/postcss/postcss) | `8.5.15` | `8.5.16` | Bumps the npm-minor-patch group with 1 update in the /plugins/openclaw directory: [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest). Bumps the npm-minor-patch group with 2 updates in the /plugins/opencode directory: [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) and @opencode-ai/plugin. Bumps the npm-minor-patch group with 3 updates in the /sdk/typescript directory: [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest), [@anthropic-ai/sdk](https://github.com/anthropics/anthropic-sdk-typescript) and [dotenv](https://github.com/motdotla/dotenv). Updates `fumadocs-core` from 16.10.3 to 16.11.1 <details> <summary>Commits</summary> <ul> <li><a href=" |
||
|
|
c707de4691
|
docs: retire IntelligentContext from README and installation guide (#1445)
## Description Public README and installation guide still marketed **IntelligentContext** and score-based history dropping after PR-B1 retired those stages in favor of live-zone-only compression. This updates the two first-touch docs so new users see the current pipeline: compress fresh tool output and new turns only; frozen prefix preserved; history never dropped. Closes #1444 ## 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.md** — replace IntelligentContext marketing bullet with **live-zone compression** (new bytes only; frozen prefix preserved; history never dropped) - **README.md** — pipeline internals list current transforms and note IntelligentContext / RollingWindow retirement (PR-B1) - **docs/content/docs/installation.mdx** — core package description matches live-zone ContentRouter - **docs/content/docs/installation.mdx** — add PR-B1 retirement note for IntelligentContext / RollingWindow ## 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 $ rg -n 'IntelligentContext' README.md docs/content/docs/installation.mdx README.md:289:- **Transforms** do the work: ... (live-zone only; IntelligentContext and RollingWindow were retired in PR-B1). docs/content/docs/installation.mdx:31:> **Note:** IntelligentContext / RollingWindow ... were retired in PR-B1. $ rg -n 'live-zone|Live-zone' README.md docs/content/docs/installation.mdx README.md:274:- **Live-zone compression** — compresses only new bytes ... README.md:289:- **Transforms** do the work: ... (live-zone only; ...) docs/content/docs/installation.mdx:29:The core package includes ... live-zone ContentRouter compression. ``` ## Real Behavior Proof - Environment: macOS (darwin 25.5.0), branch `docs/retire-intelligentcontext-readme` in `/Users/bhavya/Desktop/Headroom-upstream` - Exact command / steps: `rg -n 'IntelligentContext' README.md docs/content/docs/installation.mdx` and `rg -n 'live-zone|Live-zone' README.md docs/content/docs/installation.mdx`; read updated README pipeline section and installation.mdx core-package blurb - Observed result: IntelligentContext appears only in retirement notes (not as an active feature); live-zone compression is the primary marketed behavior in README and installation guide - Not tested: Wiki pages (tracked as follow-up in #1444); docs site build (`npm run build` in docs/) ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Checklist - [x] My code follows the project's style guidelines - [x] I have performed a self-review of my code - [ ] I have commented my code, particularly in hard-to-understand areas - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] New and existing unit tests pass locally with my changes - [ ] I have updated the CHANGELOG.md if applicable ## Additional Notes Wiki still has extensive IntelligentContext docs — out of scope here; follow-up tracked in #1444. CHANGELOG N/A (docs-only, no release note required). Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: JerrettDavis <mxjerrett@gmail.com> |
||
|
|
800ad31ab6
|
docs(orchestration): guide repeated agent wakes with CCR (#1871)
## Description Repeated agent wakes rebuild the same expensive prompt sections while also carrying volatile memory digest content. This PR adds an agent-orchestration guide for applying Headroom to that shape: keep cacheable provider prefixes stable, use CCR and `headroom_retrieve` for lossless digest backing detail, and choose proxy, library, MCP, or proxy plus MCP integration based on where the orchestrator controls message assembly. It also corrects the cache optimization docs to match the current CacheAligner implementation: CacheAligner detects volatile system-prompt content and reports prefix metrics, but it does not rewrite messages. Refs #1256. ## 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 a repeated-wake agent orchestration guide covering stable prefix layout, volatile digest placement, real-wake measurement fields, and cache-hit expectations. - Documented CCR-backed digest curation with `headroom_retrieve`, including TTL sizing, local-first deployment, and which digest fields should stay verbatim. - Compared proxy, library, MCP, and proxy plus MCP integration modes for orchestrators that spawn agents. - Added digest field-routing guidance for ContentRouter, SmartCrusher, prose compression, CCR-backed backing detail, and verbatim instruction-bearing sections. - Updated CacheAligner docs so `cache-optimization`, `how-compression-works`, and `architecture` describe detector-only drift reporting instead of message rewriting. - Added the new guide to the docs navigation. ## Testing - [ ] Unit tests pass (N/A, docs-only change) - [ ] Linting passes (N/A, docs-only change) - [x] Type checking passes (`npm run types:check`) - [ ] New tests added for new functionality when applicable (N/A, docs-only change) - [x] Manual testing performed ### Test Output ```text cd docs && npm run types:check > headroom-docs@0.0.0 types:check > fumadocs-mdx && next typegen && tsc --noEmit [MDX] generated files in 6.344200000000001ms Generating route types... [MDX] generated files in 13.536699999999996ms ✓ Types generated successfully cd docs && npm run build > headroom-docs@0.0.0 build > next build [MDX] generated files in 103.85800000000006ms ▲ Next.js 16.2.6 (Turbopack) Creating an optimized production build ... ✓ Compiled successfully in 11.9s Running TypeScript ... Finished TypeScript in 2.1s ... Collecting page data using 12 workers ... Generating static pages using 12 workers (0/140) ... Generating static pages using 12 workers (35/140) Generating static pages using 12 workers (70/140) Generating static pages using 12 workers (105/140) ✓ Generating static pages using 12 workers (140/140) in 1272ms Finalizing page optimization ... Route (app) ┌ ○ / ├ ○ /_not-found ├ ƒ /api/search ├ ● /docs/[[...slug]] │ ├ /docs/agent-orchestration │ ├ /docs/agno │ ├ /docs/anthropic-sdk │ └ [+41 more paths] ├ ○ /llms-full.txt ├ ● /llms.mdx/docs/[[...slug]] │ ├ /llms.mdx/docs/agent-orchestration/content.md │ ├ /llms.mdx/docs/agno/content.md │ ├ /llms.mdx/docs/anthropic-sdk/content.md │ └ [+41 more paths] ├ ○ /llms.txt ├ ● /og/docs/[...slug] │ ├ /og/docs/agent-orchestration/image.png │ ├ /og/docs/agno/image.png │ ├ /og/docs/anthropic-sdk/image.png │ └ [+41 more paths] ├ ○ /robots.txt └ ○ /sitemap.xml ƒ Proxy (Middleware) ○ (Static) prerendered as static content ● (SSG) prerendered as static HTML (uses generateStaticParams) ƒ (Dynamic) server-rendered on demand The width(-1) and height(-1) of chart should be greater than 0, please check the style of container, or the props width(100%) and height(100%), or add a minWidth(0) or minHeight(0) or use aspect(undefined) to control the height and width. The width(-1) and height(-1) of chart should be greater than 0, please check the style of container, or the props width(100%) and height(100%), or add a minWidth(0) or minHeight(0) or use aspect(undefined) to control the height and width. rg -n "CacheAligner|headroom_retrieve|compression_strategy|HEADROOM_CCR_TTL_SECONDS|agent-orchestration|prefix drift" "docs\content\docs\agent-orchestration.mdx" "docs\content\docs\cache-optimization.mdx" "docs\content\docs\how-compression-works.mdx" "docs\content\docs\architecture.mdx" "docs\content\docs\meta.json" docs\content\docs\meta.json:20: "agent-orchestration", docs\content\docs\agent-orchestration.mdx:19:## CacheAligner is detector-only docs\content\docs\agent-orchestration.mdx:21:CacheAligner does not rewrite messages. It inspects the prefix, emits warnings for volatile content, and records observability data so callers can fix their own assembly logic. docs\content\docs\agent-orchestration.mdx:35:If CacheAligner warns about drift, keep the prefix stable in the caller. The transform is a detector, not a repair pass. docs\content\docs\agent-orchestration.mdx:68:- `headroom_retrieve` for on-demand recovery of stored originals docs\content\docs\agent-orchestration.mdx:69:- `HEADROOM_CCR_TTL_SECONDS` for sizing the local store lifetime docs\content\docs\agent-orchestration.mdx:70:- `compression_strategy` as the authoritative discriminator on stored CCR entries docs\content\docs\agent-orchestration.mdx:72:For routing decisions, the same rule in plain terms is: headroom_retrieve recovers originals, HEADROOM_CCR_TTL_SECONDS sizes the local lifetime, compression_strategy identifies the producing path, and shape inference is not the routing authority. docs\content\docs\agent-orchestration.mdx:74:When a stored original expires, regenerate the digest or re-read the source content. Do not infer routing from payload shape. Use the stored `compression_strategy` metadata to understand how the original was produced. docs\content\docs\agent-orchestration.mdx:104:| MCP | Agents need on-demand compression and retrieval tools | Best when `headroom_retrieve` should be available as a tool | docs\content\docs\agent-orchestration.mdx:122:- CacheAligner identifies drift, it does not repair prompt assembly. docs\content\docs\agent-orchestration.mdx:125:- Use `compression_strategy` to read stored CCR intent, not payload shape. docs\content\docs\architecture.mdx:112:When SmartCrusher compresses a tool output or Intelligent Context drops messages, the original content is stored in a local compression cache. If the LLM needs the full data, it can request retrieval via a `headroom_retrieve` tool call. This makes compression reversible. docs\content\docs\architecture.mdx:117:Retrieve: LLM calls headroom_retrieve("abc123") -> original 1000 items docs\content\docs\cache-optimization.mdx:6:LLM providers cache prompt prefixes to avoid reprocessing identical input on repeated calls. Headroom's **CacheAligner** is detector-only, so it surfaces prefix drift, reports observability data, and leaves message assembly to the caller. docs\content\docs\cache-optimization.mdx:8:## What CacheAligner reports docs\content\docs\cache-optimization.mdx:12:CacheAligner does not extract, move, normalize, reorder, strip, compress, or rewrite content. It detects volatile content and reports the stable prefix hash plus cache metrics so you can fix the prefix at the source: docs\content\docs\cache-optimization.mdx:45:CacheAligner tells you when the prefix changed, which is the only signal you need to keep OpenAI prefix caching effective. docs\content\docs\cache-optimization.mdx:67:Keep the stable prefix first, keep volatile content out of it, and treat CacheAligner warnings as a signal that the caller needs to move assembly logic. docs\content\docs\cache-optimization.mdx:69:CacheAligner surfaces prefix instability, provider caches reward byte-identical prefixes, and the caller owns the actual message layout. docs\content\docs\how-compression-works.mdx:14:│ CacheAligner │────>│ ContentRouter │ docs\content\docs\how-compression-works.mdx:16:│ Report │ │ Detect type & │ docs\content\docs\how-compression-works.mdx:17:│ prefix drift │ │ route to best │ docs\content\docs\how-compression-works.mdx:22:1. **CacheAligner** detects dynamic content (dates, user context) in your system prompt and reports prefix drift so the caller can keep the static prefix cacheable across requests. docs\content\docs\architecture.mdx:50:Detects dynamic content (dates, UUIDs, session tokens) in your system prompt and reports prefix metrics. Keep the stable prefix and live context separated in the caller so provider caches (Anthropic `cache_control`, OpenAI prefix caching) can hit on repeated calls. ``` ## Real Behavior Proof - Environment: Windows, local docs toolchain, no provider credentials required. - Exact command / steps: build the docs app and check the new docs page plus cache docs for the repeated-wake guidance, `headroom_retrieve`, CCR TTL, `compression_strategy`, and the nav entry. - Observed result: docs type generation and build completed successfully; the new `agent-orchestration` page is present in docs navigation; the edited docs describe CacheAligner as detector-only drift reporting. - Not tested: live Anthropic cache-hit billing, live Claude Code wake traffic, and CCR retrieval across multiple OS processes. The PR documents the measurement fields and local deployment constraints for those real-wake checks. ## 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 - [ ] 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 ## Additional Notes This is intentionally documentation-only. It does not add Orcha-specific runtime branches, change CCR behavior, or change provider routing. `CHANGELOG.md` is unchanged because package behavior and public APIs are unchanged. |
||
|
|
37a12dd833
|
[codex] docs: add pipeline extension recipe (#1758)
## Description Headroom already supports `headroom.pipeline_extension`, but request-normalization pattern was not documented. That leaves users guessing how to fix upstream quirks such as `content: null` tool-call payloads. Closes #1758 ## Type of Change - [x] Documentation update ## Changes Made - Added a `Pipeline Extensions` section to `configuration.mdx`. - Documented the `PRE_SEND` hook as the right place for request cleanup. - Included a minimal `NormalizeNullContent` example and entry-point registration. - Mentioned `x-headroom-base-url` as the per-request routing override. ## 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 Docs-only verification: - Reviewed docs diff for API names, hook names, and placement. - Confirmed example uses public `headroom.pipeline` contract and matches existing header-routing terminology. ``` ## Real Behavior Proof - Environment: GitHub PR diff review for docs-only extension recipe. - Exact command / steps: Compared new configuration docs against public pipeline-extension and per-request routing interfaces already exposed by Headroom. - Observed result: Docs now show concrete request-cleanup extension pattern without requiring a fork. - Not tested: Live extension package execution in this verification pass. ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Checklist - [x] My code follows the project's style guidelines - [x] I have performed a self-review of my code - [ ] I have commented my code, particularly in hard-to-understand areas - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] New and existing unit tests pass locally with my changes - [ ] I have updated the CHANGELOG.md if applicable Co-authored-by: Your Name <you@example.com> |