Commit graph

1553 commits

Author SHA1 Message Date
wzy-del
c365c7ff81
fix(proxy): only queue mid-turn messages for opt-in clients with explicit session header (#1951)
## Description

Mid-turn steering wrongly queues **concurrent independent streams**.
When two streaming `/v1/messages` requests share the same model + system
prompt and arrive concurrently (no `x-headroom-session-id` header), the
proxy misclassifies the second as a "mid-turn message", returns `202
{"event":"headroom_queued"}`, and never forwards it upstream. A standard
Anthropic SDK client that made a *streaming* call receives a non-SSE 202
→ empty event stream → `AssertionError` (`assert
self.__final_message_snapshot is not None` in
`anthropic/lib/streaming/_messages.py`), and fails after retries.

**Root cause.** Without an `x-headroom-session-id` header,
`_get_session_key()` falls back to `md5(model + system[:500])` (mirrors
`prefix_tracker.compute_session_id`). That key is intentionally coarse
and cannot distinguish genuinely concurrent, independent streams that
share a model + system prompt (e.g. a main conversation plus its
background / parallel requests), so the second stream hits `session_key
in self._active_streams` and gets queued.

A queued message is only ever drained back to the client via the custom
`headroom_pending_messages` SSE event, which a standard Anthropic SDK
does not understand — so mid-turn steering is effectively a private
protocol for clients that **opt in** via `x-headroom-session-id`. A
client that never sends the header can never participate in the queue;
for it, the 202 is simply a broken streaming response.

Note: "send a unique header per request" is **not** a workaround — the
same header also drives `prefix_tracker.compute_session_id()`, so
unique-per-stream ids break prompt caching while a shared id keeps
colliding.

Closes #1949

## 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 `StreamingMixin._should_queue_mid_turn()` helper that gates
mid-turn queuing behind an explicit `x-headroom-session-id` header.
- Header-less concurrent streams are now forwarded upstream normally;
only opt-in (header-bearing) callers can be queued.
- Prefix-tracker / cache-alignment behavior is untouched — the header
still drives `compute_session_id()` exactly as before.

## Testing

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

### Test Output

```text
$ pytest tests/test_mid_turn_steering.py
6 passed
```

New test `test_should_queue_only_with_explicit_session_header`: a
header-less concurrent stream must not queue; an explicit-header opt-in
must. All existing `test_mid_turn_steering.py` cases pass an explicit
header and are unaffected.

## Real Behavior Proof

- Environment: macOS, `headroom-ai` 0.30.0 (installed via `uv tool`),
proxy running `headroom proxy --port 8799 --no-http2 --mode cache`,
upstream = an Anthropic-compatible gateway. Client = Hermes Agent
(Anthropic SDK, streaming) driving a main conversation plus concurrent
background/parallel requests that share the same model + system prompt.
- Exact command / steps:
1. Reproduce on stock 0.30.0: concurrent streaming requests without
`x-headroom-session-id` → second stream returns `202
{"event":"headroom_queued"}` → client raises `AssertionError` in
`anthropic/lib/streaming/_messages.py`.
2. Correlate logs: count of `AssertionError` in the client error log vs
count of `202` in the proxy access log for the window — **48 == 48**,
timestamps line up 1:1.
3. Apply this patch to the running package, restart the proxy, re-run
the same concurrent workload.
- Observed result: after the fix, **0 × 202 / all requests 200**, no new
`AssertionError`, and `cache_hit_pct` stayed ~99% (prefix caching
intact). Header-bearing opt-in clients still queue mid-turn as before.
- Not tested: behavior under a client that deliberately sends a
*changing* `x-headroom-session-id` per request (out of scope —
documented as a caching anti-pattern, not a supported mode).

## Review Readiness

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

## Checklist

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

## Additional Notes

- Docs / CHANGELOG unchanged: this is a proxy-internal correctness fix
with no user-facing config surface.
- The fix is deliberately minimal and conservative — it only narrows
*when* queuing engages (explicit opt-in header), leaving the
prefix-tracker, cache-alignment, and body-rewrite paths byte-for-byte
identical.

---------

Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-13 00:52:41 -04:00
alex33d
71cbb6aaad
feat(proxy): extend output shaping to the OpenAI Responses path (Codex HTTP + WS) (#1943)
## Description

Output shaping (`HEADROOM_OUTPUT_SHAPER`) so far only runs on the
Anthropic `/v1/messages` path (`shape_request` is called only from
`handlers/anthropic.py`). Codex traffic over `/v1/responses` — HTTP and
WebSocket — is never shaped, so subscription Codex users get no
output-token reduction. On a fleet where Codex is the majority of
traffic, that's the largest unshaped output-token pool.

This ports both output-shaping levers to the OpenAI Responses format
with the same contracts as the Anthropic path, wired at the single
funnel all three call paths already share.

Closes #

## Type of Change

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

## Changes Made

- `output_shaper.py` — Responses-format counterparts of the existing
levers:
- `classify_responses_turn()`: structural turn classifier over the
`input` item list. The trailing run of tool-output items
(`function_call_output`, `custom_tool_call_output`,
`local_shell_call_output`, `computer_call_output`) is a mechanical
continuation; a trailing user message is a new ask. Error detection is
structural JSON fields only (`exit_code`/`success`/`error`, incl. the
common `{"output":…, "metadata":{…}}` nesting) — never prose — mirroring
the Anthropic `is_error` handling so error turns keep full effort.
- `apply_responses_verbosity_steering()`: appends the byte-stable
steering block to the tail of the `instructions` string. Idempotent per
level, replaced in place on level change — within a conversation every
shaped turn sends identical `instructions` bytes, so the provider prefix
cache stays hot after the first shaped turn (same contract as the
Anthropic system-tail append).
- `route_responses_effort()`: lowers an explicitly-present
`reasoning.effort` on mechanical continuations only. Never injects
`reasoning`, never raises an effort, leaves new asks and error
continuations untouched. Responses gets its own rank table (`minimal`
floor).
- `shape_responses_request()`: the `shape_request` counterpart (same
settings, labels, level-resolution contract).
- `output_savings.py` — `conversation_key_from_responses_body()`:
conversation-stable holdout key (model + first user input text) so whole
conversations land in one A/B arm.
- `handlers/openai.py` — `_shape_openai_responses_payload()` (module
helper, never raises) called inside
`_compress_openai_responses_payload_in_executor`'s closure — the single
funnel for HTTP `/v1/responses`, the WS first frame, and WS subsequent
frames. Runs before compression so the classifier sees the client's
input as sent; serialization stays off the event loop. Shaper labels
ride the existing transforms channel so `outcome.py record_from_labels`
feeds the output-savings ledger unchanged. The `modified` flag is forced
only when shaping actually mutated the payload — an unshaped control-arm
request never breaks byte-faithful forwarding.
- `tests/test_output_shaper_responses.py` — 40 tests covering the
classifier (incl. error sniff + prose-never-inspected), steering
(idempotency, level change, byte stability, non-string instructions),
effort routing (never-inject/never-raise, error/new-ask untouched),
conversation key stability, and the handler helper
(disabled/treatment/full-holdout arms).

Off by default; same env gates as the Anthropic path, all hot-reloadable
via `/admin/runtime-env`.

## Testing

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

### Test Output

```text
$ pytest tests/test_output_shaper.py tests/test_output_shaper_responses.py \
    tests/test_output_savings.py tests/test_verbosity_controller.py \
    tests/test_verbosity_learn.py tests/test_codex_openai_contract_parity.py \
    tests/test_codex_responses_waste_signals.py -q
154 passed

$ ruff check headroom/proxy/output_shaper.py headroom/proxy/output_savings.py \
    headroom/proxy/handlers/openai.py tests/test_output_shaper_responses.py
All checks passed!

$ mypy headroom/proxy/output_shaper.py headroom/proxy/output_savings.py --ignore-missing-imports
exit 0
```

## Real Behavior Proof

- Environment: macOS, Python 3.13, proxy run from this branch
(`PYTHONPATH=. headroom proxy --port 8790`, `HEADROOM_OUTPUT_SHAPER=1
HEADROOM_VERBOSITY_LEVEL=2`), real OpenAI upstream (fake API key —
shaping happens pre-upstream; upstream 401s prove the request went
through the full pipeline).
- Exact command / steps: POST three `/v1/responses` bodies — (a)
trailing `function_call_output` with `exit_code:0` (mechanical), (b)
plain user ask, (c) trailing `function_call_output` with `exit_code:1`
(error).
- Observed result: mechanical turn got `effort:high->low` + L2 steering;
new ask and error turns kept full effort with L2 steering only — proxy
request log `transforms_applied` below.

```text
(a) ["output_shaper:stratum:gpt|mechanical_continuation|xs|tools", "output_shaper:verbosity:L2", "output_shaper:effort:high->low"]
(b) ["output_shaper:stratum:gpt|new_user_ask|xs|notools",          "output_shaper:verbosity:L2"]
(c) ["output_shaper:stratum:gpt|error_continuation|xs|notools",    "output_shaper:verbosity:L2"]
```

Mechanical turn gets `reasoning.effort` high→low; new ask and error
continuation keep full effort; all three get the byte-stable L2 steering
on the `instructions` tail.
- Not tested: a live Codex WebSocket session end-to-end against the
ChatGPT backend (the WS paths share the exact executor funnel exercised
above);
`test_codex_ws_compression_scheduler.py::test_concurrent_compression_has_no_semaphore_tail`
fails in my env on a clean tree too (no compiled `headroom._core` in a
source checkout) — unrelated.

## Review Readiness

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

## Checklist

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

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-13 00:52:36 -04:00
Abhay Singh
1843346283
fix(proxy/vertex): route google-publisher requests to the request region (#2069)
## Description

The Vertex `publisher=google` routes forward to a **fixed** upstream
host, ignoring the
request's region. In `headroom/providers/proxy_routes.py`,
`vertex_generate_content`,
`vertex_stream_generate_content`, and `vertex_count_tokens` all do:

```python
del api_version, project, location        # <-- location discarded
if publisher == "google":
    return await proxy.handle_gemini_generate_content(
        request, model,
        _api_target(proxy, "vertex"),      # <-- single fixed host (default us-central1)
        "vertex:google",
    )
```

The sibling Anthropic `rawPredict` route already does this correctly —
it keeps `location` and
passes `_vertex_target_for_location(proxy, location)`, which derives the
regional host from the
path.

So a request to
`.../locations/europe-west1/publishers/google/models/gemini-2.0-flash:generateContent`
(with the proxy left at the default Vertex URL) is forwarded to
`https://us-central1-aiplatform.googleapis.com/...europe-west1...` — a
`us-central1` host serving a
`europe-west1` path. Vertex requires the host region to match the path
location, so it rejects the
request. `_vertex_target_for_location` and the region-aware Anthropic
routing landed together in
`0e059150`; the three google routes were the missed spot.

Closes: no issue filed — found while auditing Vertex routing.

## Fix

In all three `publisher == "google"` branches, keep `location` and pass
`_vertex_target_for_location(proxy, location)` instead of
`_api_target(proxy, "vertex")`. That
helper honors an operator-pinned non-default upstream (private gateway)
and otherwise derives the
host from the request's `location` (`global` → the unprefixed host).

## Type of Change

- [x] Bug fix (non-breaking change that fixes an issue)

## Changes Made

- `headroom/providers/proxy_routes.py`: region-aware host for the google
generateContent / streamGenerateContent / countTokens routes.
- `tests/test_vertex_claude_compression.py`: add route-level tests that
the google generateContent and countTokens routes forward a
`europe-west1` request to
`https://europe-west1-aiplatform.googleapis.com` (default config),
mirroring the existing anthropic-route test.

## Testing

- [x] New regression tests added
(`tests/test_vertex_claude_compression.py`)
- [x] Linting/formatting clean — run with the CI-pinned `ruff==0.15.17`
- [ ] Full `pytest` deferred to CI (local-OOM reason below).

```text
$ uvx ruff@0.15.17 check headroom/providers/proxy_routes.py tests/test_vertex_claude_compression.py
All checks passed!
```

## Real Behavior Proof

- Environment: Windows 11, Python 3.10, headroom from this branch.
Importing `headroom` pulls in the torch/transformers stack and a full
`pytest` gets OOM-killed on this box, so I verified the host-derivation
with a dependency-free script and left the full pytest to CI.
- Exact command / steps: ran a `europe-west1` request through the old
fixed `_api_target` host and the new `_vertex_target_for_location`, plus
the `us-central1`/`global`/operator-pinned cases.
- Observed result: the old path sends europe-west1 to the us-central1
host (rejected); the new path derives the correct region and still
honors a pinned upstream:

```text
europe-west1: OLD host=https://us-central1-aiplatform.googleapis.com
europe-west1: NEW host=https://europe-west1-aiplatform.googleapis.com
VERTEX REGION ROUTING FIX VERIFIED (old = fixed us-central1; new = per-request region)
```

- Not tested: a live GCP/Vertex round-trip (handlers stubbed, as the
existing tests do). The existing tests that pin a non-default
`vertex_api_url="https://vertex.test"` still pass, since
`_vertex_target_for_location` honors the pinned upstream. Full local
`pytest` deferred to CI (OOM, per 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 commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes — ran
lint + a standalone logic check; full pytest deferred to CI (local OOM,
disclosed above)
- [x] I have updated the CHANGELOG.md if applicable

## Additional Notes

- Reuses the in-file `_vertex_target_for_location` helper the anthropic
route already uses; no new dependencies. (The
non-`google`/non-`anthropic` publisher passthrough is still fixed-host —
a separate, lower-priority follow-up.)
- @JerrettDavis tagging you — non-`us-central1` Vertex Gemini requests
currently fail on a host/region mismatch; this brings the google routes
in line with the anthropic one you reviewed. Thanks!

Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-13 00:46:25 -04:00
Abhay Singh
19201e842f
fix(proxy/openai): respect explicit stream_options.include_usage (#2026)
## Description

On the direct OpenAI `/v1/chat/completions` streaming path, the handler
injects
`stream_options.include_usage = True` so it can count tokens from the
trailing usage chunk —
but it does so **unconditionally**, including flipping an explicit
client `include_usage: false`
to `true` (`headroom/proxy/handlers/openai.py`):

```python
if "stream_options" not in body:
    body["stream_options"] = {"include_usage": True}
elif isinstance(body.get("stream_options"), dict):
    body["stream_options"]["include_usage"] = True    # overrides an explicit `false`
```

When the client passed `stream_options: {"include_usage": false}` (or a
dict that set some
other key), the upstream is nevertheless asked for usage and appends a
terminal usage-only
frame:

```
data: {"id":...,"choices":[],"usage":{...}}
data: [DONE]
```

The extremely common client pattern `for chunk in stream:
chunk.choices[0].delta.content`
then raises `IndexError` on that empty-`choices` frame — for a usage
chunk the client
explicitly opted out of.

Closes: no issue filed — found while auditing the streaming
request-shaping.

## Fix

Only fill in `include_usage` when the client left the choice open — no
`stream_options` at all,
or a `stream_options` dict that doesn't mention `include_usage`. An
explicit `true`/`false` is
respected. Extracted into a small `_apply_stream_usage_option(body)`
helper (mirroring the
existing `_normalize_openai_max_tokens`) for a clean unit-test seam:

```python
stream_options = body.get("stream_options")
if stream_options is None:
    body["stream_options"] = {"include_usage": True}
elif isinstance(stream_options, dict) and "include_usage" not in stream_options:
    stream_options["include_usage"] = True
```

Scope note: this respects an explicit client choice, which is the
unambiguous defect. The
separate question of whether to strip the synthetic usage chunk when
Headroom injected the
option itself (the no-`stream_options` default, kept for token-counting)
touches the raw SSE
byte stream and is intentionally left out of this change.

## Type of Change

- [x] Bug fix (non-breaking change that fixes an issue)

## Changes Made

- `headroom/proxy/handlers/openai.py`: add
`_apply_stream_usage_option(body)` and call it from the streaming chat
path; it no longer overrides an explicit client `include_usage`.
- `tests/test_proxy/test_openai_stream_usage_option.py`: cover explicit
`false` (respected), explicit `true` (preserved), absent (injected), and
dict-without-key (filled in).

## Testing

- [x] New regression tests added
(`tests/test_proxy/test_openai_stream_usage_option.py`)
- [x] Linting/formatting clean — run with the CI-pinned `ruff==0.15.17`
- [ ] Full `pytest` deferred to CI (local-OOM reason below).

```text
$ uvx ruff@0.15.17 check headroom/proxy/handlers/openai.py tests/test_proxy/test_openai_stream_usage_option.py
All checks passed!
```

## Real Behavior Proof

- Environment: Windows 11, Python 3.10, headroom from this branch.
Importing `headroom` pulls in the torch/transformers stack and a full
`pytest` gets OOM-killed on this box, so I verified the decision logic
with a dependency-free script and left the full pytest to CI.
- Exact command / steps: ran a client body with `stream_options:
{include_usage: false}` (plus the explicit-true, absent, and
dict-without-key cases) through the old unconditional injection and the
new helper.
- Observed result: the old logic flips the client's `false` to `true`;
the new logic respects it:

```text
explicit false: OLD -> {'include_usage': True}   NEW -> {'include_usage': False}
INCLUDE_USAGE RESPECT-CLIENT FIX VERIFIED (old flips false->true; new respects false)
```

- Not tested: a full streaming round-trip through a live OpenAI upstream
(needs the heavy stack + a key). The fix is confined to the
request-shaping helper and the new tests drive it directly. Full local
`pytest` deferred to CI (OOM, per 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 commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes — ran
lint + a standalone logic check; full pytest deferred to CI (local OOM,
disclosed above)
- [x] I have updated the CHANGELOG.md if applicable

## Additional Notes

- Small, contained change plus a helper and tests; no new dependencies.
The backend-path injection (`test_backend_anyllm` /
`test_backend_streaming_cache_metrics`) is untouched — those pass an
explicit `include_usage: true`, which is preserved.
- @JerrettDavis tagging you — this one makes a client that sent
`include_usage: false` hit an `IndexError` on the usage chunk, so it
seemed worth surfacing. Thanks!

---------

Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-13 00:43:16 -04:00
JD Davis
0415dc8765
refactor(proxy): isolate output verbosity policy (#1963)
## Description

Extracts output verbosity steering text and sentinel replacement into a
pure `output_verbosity_policy` module. `output_shaper` continues to
mutate Anthropic/OpenAI request bodies, while the byte-stable steering
block and replacement rules now live behind deterministic, directly
tested policy functions.

Closes #

## Type of Change

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

## Changes Made

- Added `headroom.proxy.output_verbosity_policy` for steering sentinels,
level text, `steering_text`, and `replace_or_append_steering_block`.
- Updated `output_shaper` to delegate pure steering text/replacement
rules while preserving existing public imports and request mutation
behavior.
- Added direct policy tests for byte-stable steering text, append,
replacement, malformed sentinel handling, and idempotency.
- Included the current LiteLLM callback signature compatibility shim
required for repo-wide mypy on main-based slices.

## Testing

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

### Test Output

```text
python -m pytest tests/test_output_verbosity_policy.py tests/test_output_shaper.py tests/test_litellm_callback.py -q
58 passed in 6.23s

python -m ruff check .
All checks passed!

python -m ruff format --check .
1095 files already formatted

python -m mypy headroom --ignore-missing-imports
Success: no issues found in 409 source files

gitleaks protect --staged --no-banner --redact
no leaks found
```

## Real Behavior Proof

- Environment: Windows, Python 3.13.13, clean worktree based on
`headroomlabs/main`.
- Exact command / steps: targeted pytest, ruff, format check, repo-wide
mypy, staged gitleaks scan.
- Observed result: output verbosity policy/shaper/callback tests pass;
static checks pass; no staged secrets detected.
- Not tested: live provider calls; this slice preserves existing request
mutation behavior and only moves pure steering rules.

## Review Readiness

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

## Checklist

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

## Screenshots (if applicable)

N/A

## Additional Notes

Documentation and changelog updates are not applicable for this internal
architecture slice. PR-specific GHAS checks will be monitored after
opening.
2026-07-12 21:35:09 -07:00
JD Davis
f359f21424
refactor(proxy): extract beta header merge policy (#1993)
## Description

Extracts deterministic beta-header token parsing and merge rules from
`headroom.proxy.helpers` into a focused module. Existing helper names
remain available for Anthropic/OpenAI handlers and the session beta
tracker.

Closes #

## Type of Change

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

## Changes Made

- Added `headroom.proxy.beta_header_merge` for beta token splitting and
deterministic merge behavior.
- Re-exported the existing `merge_anthropic_beta` and
`merge_openai_beta` helper names from `helpers.py` for compatibility.
- Added direct unit tests for token splitting, ordering,
case-insensitive dedupe, empty required tokens, and provider wrappers.

## Testing

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

### Test Output

```text
python -m pytest tests/test_beta_header_merge.py tests/test_anthropic_beta_session_sticky.py tests/test_openai_beta_session_sticky.py
48 passed in 0.38s

python -m ruff check .
All checks passed!

python -m ruff format --check .
1069 files already formatted

python -m mypy headroom --ignore-missing-imports
Success: no issues found in 410 source files

gitleaks protect --staged --no-banner --redact
no leaks found
```

## Real Behavior Proof

- Environment: Windows, Python 3.13.13
- Exact command / steps: Ran focused beta merge tests, existing
Anthropic/OpenAI beta sticky suites, full ruff, format check, mypy, and
staged gitleaks scan.
- Observed result: Existing beta merge and tracker behavior remains
green while extracted merge rules are covered directly.
- Not tested: Full repository pytest suite locally; CI covers the
broader matrix.

## Review Readiness

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

## Checklist

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

## Screenshots (if applicable)

N/A

## Additional Notes

Documentation and changelog updates are not applicable for this internal
refactor. The default-branch Dependabot alerts reported during push are
pre-existing and unrelated to this PR.
2026-07-12 21:34:25 -07:00
Chester
d0ecc9a556
fix(memory): track MCP retrieval access (#2065)
## Description

Track successful native MCP `memory_search` retrievals in persistent
memory metadata. Returned memories now increment `access_count` and
update `last_accessed`, so MCP usage contributes to memory budget and
retention signals.

Closes #2061

## 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 an atomic, deduplicated `MemoryStore.record_access` operation.
- Expose access recording through `HierarchicalMemory` and
`LocalBackend`, invalidating stale cache entries.
- Record only the final active memories actually returned by MCP search.
- Fail open if usage metadata cannot be written.
- Add SQLite and MCP regression coverage.

## Testing

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

### Test Output

```text
pytest tests/test_memory --ignore=tests/test_memory/test_learn_flag.py -q
368 passed, 142 skipped, 158 warnings in 3.28s

pytest tests/test_memory/test_hierarchical.py tests/test_memory/test_mcp_server.py tests/test_memory/test_factory.py -q
40 passed, 53 skipped, 158 warnings in 0.75s
```

## Real Behavior Proof

- Environment: macOS, Python 3.13, SQLite memory store.
- Exact command / steps: save two memories; call `record_access` with
duplicate IDs plus a missing ID; read both rows; call it again for one
row.
- Observed result: each existing memory increments once per call,
duplicates do not double-count, missing IDs are ignored, and
`last_accessed` advances to the supplied timestamp.
- Not tested: the full repository suite and
`tests/test_memory/test_learn_flag.py`; the source checkout does not
include the compiled `headroom._core` Rust extension. Ruff and mypy were
not available in the local development environment; CI remains
authoritative for those 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 style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] I have updated the CHANGELOG.md if applicable

## Additional Notes

Documentation and changelog changes are not included because this is an
internal retrieval-metadata correction with no user-facing configuration
change. Access tracking is intentionally fail-open so a metadata write
failure cannot suppress a valid memory search result.

---------

Co-authored-by: xuyidiao <xuyidiao@bytedance.com>
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-12 19:46:13 -04:00
Abhay Singh
ec6e60ea3e
fix(proxy/anthropic): scope session id by top-level system prompt (#2070)
## Description

`SessionTrackerStore.compute_session_id`
(`headroom/cache/prefix_tracker.py`) computes a fallback
session id (when no `x-headroom-session-id` header is present) from
`model` + system-prompt text.
But it harvests system text **only** from `messages` entries with `role
== "system"`:

```python
for msg in messages:
    if msg.get("role") == "system":
        ...  # collect system text
system_content = json.dumps(system_parts, ...)
key = f"{model}:{system_content}"
```

Anthropic's `/v1/messages` carries the system prompt as a **top-level**
`body["system"]` field —
it never sends `role:"system"` entries inside `messages`. And
`x-headroom-session-id` is a
Headroom-internal header no client sends. So for every genuine Anthropic
request `system_parts`
is empty and the id collapses to `md5(f"{model}:[]")` — **every
conversation on the same model
shares one session id**, and therefore one `PrefixCacheTracker` and all
session-sticky state.

The colliding state cross-contaminates across conversations
(`anthropic.py:1052`):
- sticky `headroom_retrieve` / memory tools keyed purely on `session_id`
(no content guard) get
injected into another conversation's tool list — busting its tools cache
and adding tools its
  client never requested;
- sticky `anthropic-beta` header tokens leak across conversations;
- `frozen_message_count` and the per-session compression cache
cross-contaminate.

(The sibling `StreamingMixin._get_session_key` already reads
`body.get("system")` and its docstring
claims to mirror `compute_session_id` — which it did not.)

Closes: no issue filed — found while auditing the session/prefix
tracker.

## Fix

Add an optional `system` parameter to `compute_session_id` and fold its
text (a plain string or a
list of `{"type":"text"}` blocks) into the hash. The Anthropic handler
passes `body.get("system")`.
OpenAI callers don't pass it (defaults to `None`), so their behavior is
unchanged.

## Type of Change

- [x] Bug fix (non-breaking change that fixes an issue)

## Changes Made

- `headroom/cache/prefix_tracker.py`: `compute_session_id` accepts an
optional `system` and folds it into the id.
- `headroom/proxy/handlers/anthropic.py`: pass
`system=body.get("system")` when computing the session id.
- `tests/test_cache/test_prefix_tracker.py`: add
`test_compute_session_id_distinguishes_top_level_system` (distinct
systems → distinct ids; list-form == string-form; `system=None`
unchanged).

## Testing

- [x] New regression test added
(`tests/test_cache/test_prefix_tracker.py`)
- [x] Linting/formatting clean — run with the CI-pinned `ruff==0.15.17`
- [ ] Full `pytest` deferred to CI (local-OOM reason below).

```text
$ uvx ruff@0.15.17 check headroom/cache/prefix_tracker.py headroom/proxy/handlers/anthropic.py tests/test_cache/test_prefix_tracker.py
All checks passed!
```

## Real Behavior Proof

- Environment: Windows 11, Python 3.10, headroom from this branch.
Importing `headroom` pulls in the torch/transformers stack and a full
`pytest` gets OOM-killed on this box, so I verified the hash logic with
a dependency-free script and left the full pytest to CI.
- Exact command / steps: computed ids for two conversations with the
same model and messages but different top-level `system` prompts,
through the old (never-folds-system) and new logic.
- Observed result: the old logic collapses both to one id (the leak);
the new logic separates them, folds list-form system the same as
string-form, and leaves the `system=None` (OpenAI) path unchanged:

```text
OLD: A=97d8857ba27010bb  B=97d8857ba27010bb  same=True
NEW: A=1e838c0f6e3980a6  B=18ec49bfa8240852  same=False
SESSION-ID SYSTEM FIX VERIFIED (old collapses Anthropic convos; new separates them)
```

- Not tested: a full two-conversation proxy run asserting no sticky-tool
leakage (needs the heavy stack). The fix is confined to
`compute_session_id` + the one handler call site, and the new test
drives the method directly. Full local `pytest` deferred to CI (OOM, per
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 commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes — ran
lint + a standalone logic check; full pytest deferred to CI (local OOM,
disclosed above)
- [x] I have updated the CHANGELOG.md if applicable

## Additional Notes

- Backward-compatible: the new `system` parameter defaults to `None`, so
the OpenAI call sites (`openai.py`) need no change and their session ids
are identical.
- @JerrettDavis tagging you — this one lets one Anthropic conversation's
sticky tools/headers leak into another on the same model, so it seemed
worth surfacing. Thanks!
2026-07-12 19:40:58 -04:00
Abhay Singh
cbb775015e
fix(subscription/copilot): preserve remaining=0 for exhausted quota (#1997)
## Description

`parse_copilot_quota` reads each category's remaining count like this
(`headroom/subscription/copilot_quota.py`):

```python
remaining = raw.get("remaining") or raw.get("quota_remaining")
```

When a Copilot category is fully consumed, the `/copilot_internal/user`
API sends
`remaining: 0`. The `or` chain treats that legitimate `0` as falsy and —
since the real
per-category payload emits `remaining`, not the `quota_remaining` alias
— collapses it to
`None`:

```python
{"entitlement": 300, "remaining": 0}   # fully spent
# raw.get("remaining") -> 0 (falsy) -> raw.get("quota_remaining") -> None -> remaining = None
```

With `remaining = None`, the derived properties break:

- `CopilotQuotaCategory.used` (needs `remaining is not None`) → `None`
instead of `entitlement`
- `used_percent`, when the API also omits `percent_remaining` for that
category → `None`

`to_dict` then emits `remaining: None, used: None, used_percent: None`,
so the dashboard
renders a **100%-exhausted** quota as `used: -` and a **0% green** gauge
— telling the user
they have full quota left when they have none.

Only the `remaining` field has this falsy-zero bug;
`entitlement`/`percent_remaining` are
already parsed with a plain `.get()`, and `overage_count`'s `or 0` is
benign because `0` is
its intended default.

Closes: no issue filed — found while auditing the subscription/quota
parsing.

## Fix

Use an explicit `is None` check, matching how the sibling fields are
parsed:

```python
remaining = raw.get("remaining")
if remaining is None:
    remaining = raw.get("quota_remaining")
```

## Type of Change

- [x] Bug fix (non-breaking change that fixes an issue)

## Changes Made

- `headroom/subscription/copilot_quota.py`: parse `remaining` with an
explicit `is None` check so a legitimate `0` survives (alias fallback
only when the key is truly absent).
- `tests/test_copilot_quota.py`: add
`test_fully_exhausted_remaining_zero_is_preserved` (remaining `0` →
`used == entitlement`, `used_percent == 100`).

## Testing

- [x] New regression test added (`tests/test_copilot_quota.py`)
- [x] Linting passes (`ruff check`) and formatting is clean (`ruff
format --check`) — run with the CI-pinned `ruff==0.15.17`
- [ ] Full `pytest` deferred to CI (local-OOM reason below).

```text
$ uvx ruff@0.15.17 check headroom/subscription/copilot_quota.py tests/test_copilot_quota.py
All checks passed!
```

## Real Behavior Proof

- Environment: Windows 11, Python 3.10, headroom from this branch.
Importing `headroom` pulls in the torch/transformers stack and a full
`pytest` gets OOM-killed on this box, so I verified the parse +
`used`/`used_percent` logic with a dependency-free script and left the
full pytest to CI.
- Exact command / steps: ran a fully-exhausted category (`entitlement:
300, remaining: 0`, no alias/percent) through both the old `or`
expression and the new `is None` check, then through the
`used`/`used_percent` property logic.
- Observed result: the old path yields `remaining=None → used=None,
used_percent=None` (the misleading 0%/green); the new path preserves `0`
and reports 100%:

```text
OLD remaining: None  used=None  used_percent=None
NEW remaining: 0  used=300  used_percent=100.0
  -> OLD renders exhausted quota as unknown (0%/green); NEW shows 300/300 = 100%
OK alias fallback + normal values preserved
COPILOT QUOTA ZERO-REMAINING FIX VERIFIED
```

- Not tested: rendering the actual dashboard HTML (needs the running
app). The fix is confined to the parse function and the new test asserts
the parsed `used`/`used_percent`. Full local `pytest` deferred to CI
(OOM, per 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 commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes — ran
lint + a standalone logic check; full pytest deferred to CI (local OOM,
disclosed above)
- [x] I have updated the CHANGELOG.md if applicable

## Additional Notes

- One-line falsy-zero fix plus a test; no new dependencies.
- @JerrettDavis tagging you — small one, but it makes the Copilot
dashboard show a spent quota as 100% instead of a green 0%, so worth a
quick look when you have a moment.

Co-authored-by: JD Davis <mxjerrett@gmail.com>
2026-07-12 18:48:22 -04:00
gglucass
112d95b618
feat(proxy): report new-content-relative input savings rate in /stats (#2058)
## Description

The whole-request savings ratios in `/stats` (`proxy_savings_percent`,
`savings_percent`) divide by a per-request recount of the full
transcript: a session at turn 200 has had its history counted 200 times
into the denominator. Long-running cached sessions — 1M-context models
especially, since they never compact — therefore read as ~0% savings no
matter how well compression performs on content that actually newly
enters context.

Field example that motivated this: one day of 1M-context Claude Code
traffic saved 641K tokens against ~13.4M tokens of genuinely new content
(~4.8%), but displayed as 0.14% because the summed full-transcript
denominator was 475M.

This PR adds a new-content-relative rate alongside the existing fields:

- `tokens.new_input_tokens` — provider-billed non-cache-read input
(uncached + cache-write tokens, summed from response usage across
providers; the cache accumulators already track both).
- `tokens.new_input_savings_percent` — `saved / (new_input + saved)`.
Tokens Headroom removed never reached the provider, so they're added
back to form the baseline: "of the input that would have newly entered
context, what fraction did Headroom remove?"

Purely additive — no existing field changes, no new accumulators.

## Type of Change

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

## Changes Made

- `headroom/proxy/server.py`: compute `new_input_tokens` from
`prefix_cache_stats["totals"]` (already built for `/stats`) and emit the
two new fields in the `tokens` block. Rate is guarded on
`new_input_tokens > 0`: the cache accumulators only see requests with
cache activity, so a deployment with no cache metrics (e.g. Bedrock)
would otherwise divide savings by themselves and report ~100% — it
reports 0 instead.
- `tests/test_stats_new_input_savings_rate.py`: endpoint-level tests via
`TestClient(create_app(...))` — a long-cached-session request shows
9.09% new-content rate while `proxy_savings_percent` stays diluted at
0.5%; and the no-cache-usage-data case reports 0.
- `CHANGELOG.md`: Features 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
- [ ] Manual testing performed

### Test Output

```text
$ uv run --frozen --extra dev pytest tests/test_stats_new_input_savings_rate.py -v
tests/test_stats_new_input_savings_rate.py::test_stats_reports_new_input_savings_rate PASSED
tests/test_stats_new_input_savings_rate.py::test_stats_new_input_rate_is_zero_without_cache_usage_data PASSED
========================= 2 passed, 1 warning in 6.78s =========================

$ uv run --frozen --extra dev pytest tests/test_proxy_savings_history.py tests/test_dashboard_token_savings.py tests/test_proxy_cache_ttl_metrics.py
======================== 57 passed, 1 warning in 10.70s ========================

$ uv run --frozen --extra dev mypy headroom/proxy/server.py
Success: no issues found in 1 source file

$ ruff check headroom/proxy/server.py tests/test_stats_new_input_savings_rate.py
All checks passed!
$ ruff format --check headroom/proxy/server.py tests/test_stats_new_input_savings_rate.py
2 files already formatted
```

## Real Behavior Proof

- Environment: macOS 15 (darwin 24.6.0), Python 3.10 via `uv run
--frozen --extra dev`.
- Exact command / steps: `TestClient(create_app(config))`, record a
request shaped like a late turn of a long cached session
(`input_tokens=1_000_000, tokens_saved=5_000, cache_read=900_000,
cache_write=45_000, uncached=5_000`), then `GET /stats`.
- Observed result: `tokens.new_input_tokens == 50_000`,
`tokens.new_input_savings_percent == 9.09`, while
`proxy_savings_percent` stays `0.5` — the dilution the new field exists
to correct, reproduced side by side.
- Not tested: not run against a live proxy with real provider traffic;
`ruff`/`mypy` run scoped to the changed files rather than the whole
repo.

## Review Readiness

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

## Checklist

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

## Screenshots (if applicable)

N/A — JSON API addition; dashboard adoption can follow separately.

## Additional Notes

- No linked issue; companion to the nested tool_result image
token-counting fix (same investigation — that PR fixes the inflated
numerator/denominator counts, this one fixes the metric that divides by
transcript recounts).
- Caveat worth a reviewer's eye: the numerator (`tokens_saved_total`,
local tokenizer) and denominator (provider-reported usage) come from
different counters. They're on the same scale, but the rate is
honest-approximate rather than exact — comment in code says so.
- Deliberately did not change the dashboard headline or any existing
field semantics; consumers can opt into the new rate.

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

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-12 18:48:19 -04:00
Abhay Singh
18e5680be3
fix(install): only validate requested targets on the manual path (#1659)
## Description

`resolve_targets()` runs the provider-scope "unsupported targets"
validation
**before** it dispatches on `provider_mode`:

```python
if scope == ConfigScope.PROVIDER.value:
    unsupported = [t for t in requested if t and t not in valid]
    if unsupported:
        raise click.ClickException("Provider scope supports only ...; unsupported targets: ...")

if provider_mode == ALL:  return [t.value for t in valid_targets]   # ignores `requested`
if provider_mode == AUTO: ...                                        # ignores `requested`
# manual: filters `requested`
```

But `all` and `auto` never consult the requested target list — only the
manual
path does. So an unsupported entry that those modes would simply ignore
instead
makes the call raise. Concretely:

```
headroom install apply --scope provider --providers all --target cursor
→ ClickException: Provider scope supports only claude, codex, openclaw, and
  opencode; unsupported targets: cursor
```

...when it should just return the full provider set. (`--target` is a
click
`Choice` that accepts all 7 targets regardless of scope/mode, so this is
reachable from the CLI.) The user-scope equivalent,
`resolve_targets("all",
["cursor"])`, happily ignores `cursor` and returns all user targets — so
provider
scope is inconsistent with user scope for identical, ignored input.

Closes: no issue filed — found while auditing `install` target
resolution.

## Fix

Move the provider-scope validation so it runs only on the manual path
(the only
mode that reads `requested`). `all` returns the full provider set and
`auto`
returns detected/default targets, neither raising on an ignored
requested list.

## Type of Change

- [x] Bug fix (non-breaking change that fixes an issue)

## Changes Made

- `headroom/install/planner.py`: move the provider-scope "unsupported
targets" check below the `all`/`auto` dispatch, into the manual path.
- `tests/test_install/test_planner.py`: regression tests — `all` and
`auto` ignore an unsupported requested target under provider scope; the
manual path still rejects it.
- `CHANGELOG.md`: Bug Fixes entry under Unreleased.

## Testing

- [x] New tests added for the fixed behavior
(`tests/test_install/test_planner.py`)
- [x] Linting passes (`ruff check`) and formatting is clean (`ruff
format --check`)
- [ ] Full `pytest` deferred to CI (see Real Behavior Proof for the
local-OOM reason).

```text
$ uv run ruff check headroom/install/planner.py tests/test_install/test_planner.py
All checks passed!
```

## Real Behavior Proof

- Environment: Windows 11, Python 3.12.11, headroom built from this
branch. Importing `headroom` loads the torch/transformers stack and a
full `pytest` gets OOM-killed on this box, so I verified the control
flow with a dependency-free script (only stdlib) and left the full
pytest to CI.
- Exact command / steps: replicated `resolve_targets`'s control flow
(with the fix, `click.ClickException` stubbed, and stand-in target
lists) in a standalone script — no `headroom` import — and exercised
`all`/`auto`/`manual` under provider scope with an unsupported `cursor`
entry, plus the user-scope and manual-dedup regressions.
- Observed result: `all`/`auto` return the provider targets without
raising, `manual` still raises on the unsupported target, and the
pre-existing manual-dedup and user-scope behavior is unchanged:

```text
OK: all + [cursor] + provider -> provider set (no raise)
OK: auto + [cursor] + provider -> [claude, codex] (no raise)
OK: manual + [cursor] + provider -> raises (preserved)
OK: manual dedupe/filter + user-scope all unchanged
PLANNER LOGIC VERIFIED
```

- Not tested: a full `headroom install apply` end-to-end (would require
the heavy stack and a real deployment); the change is confined to pure
target-list resolution. Full local `pytest` deferred to CI (OOM, per
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 commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes — ran
lint + a standalone logic check; full pytest deferred to CI (local OOM,
disclosed above)
- [x] I have updated the CHANGELOG.md if applicable

## Additional Notes

- No new dependencies; a control-flow move plus tests.

---------

Co-authored-by: JD Davis <mxjerrett@gmail.com>
2026-07-12 16:30:31 -05:00
Kenneth Wong
560319cef4
fix(dashboard): serve per-request metadata to trusted-gateway peers (#1766)
## Description

The dashboard's per-request metadata — the `recent_requests` /
`request_logs` tail and the `config` block (which echoes upstream API
URLs + backend settings) — is gated to loopback callers via
`_request_is_loopback`. It requires **both** a loopback peer IP
(`request.client.host == 127.0.0.1`) and a loopback `Host` header.

When Headroom runs in a **bridge-network container** (Docker/podman, or
Apple Containerization / `mocker`), a browser on the host reaches the
proxy through the container gateway, so `request.client.host` is the
**gateway IP** (e.g. `172.18.0.1`, or `192.168.64.1` on macOS vmnet),
not `127.0.0.1`. `include_sensitive` is therefore `False`, and the
"Recent Requests" table renders empty even though the operator is
browsing locally at `http://127.0.0.1:8787/dashboard`.

`curl` from **inside** the container (real `127.0.0.1` peer) confirmed
the data is present and populated — only the host-browser path was being
stripped.

The fix treats a peer inside an operator-configured trusted-gateway CIDR
(`HEADROOM_PROXY_TRUSTED_GATEWAY_CIDRS` — the same allow-list already
used by `forwarded_headers.py` to sanitize `X-Forwarded-*`) as
loopback-equivalent, while **retaining the loopback `Host`-header gate
as the DNS-rebinding defence**. It is opt-in and empty by default, so
there is **no behavior change** unless the operator explicitly
allow-lists their container gateway.

Closes #

## Type of Change

- [x] Bug fix (non-breaking change that fixes an issue)

## Changes Made

- `headroom/proxy/server.py` — `_request_is_loopback` now: (1) always
enforces the loopback `Host`-header gate first; (2) returns `True` for a
genuine loopback peer; (3) additionally returns `True` for a peer inside
`HEADROOM_PROXY_TRUSTED_GATEWAY_CIDRS` via the existing
`peer_is_trusted_gateway` / `load_trusted_gateway_cidrs` helpers.
- `tests/test_proxy_loopback_gating.py` — added
`test_stats_metadata_served_to_trusted_gateway_peer`: gateway peer
stripped without the allow-list, served with it, and DNS-rebinding
(non-loopback `Host`) still rejected even for a trusted gateway peer.
- `CHANGELOG.md` — Unreleased → Fixed entry.

## Testing

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

### Test Output

```text
$ pytest tests/test_proxy_loopback_gating.py -q
14 passed, 1 warning in 3.56s

$ ruff check headroom/proxy/server.py tests/test_proxy_loopback_gating.py
All checks passed!
```

## Real Behavior Proof

- Environment: Headroom 0.29.0 in a `mocker compose` (Apple
Containerization) bridge container on macOS; host browser at
`http://127.0.0.1:8787/dashboard`.
- Exact command / steps: before the fix, `mocker compose exec
headroom-proxy sh -c 'curl -s http://127.0.0.1:8787/stats'` (peer = real
`127.0.0.1`) returned a populated `recent_requests` array, while the
host browser saw an empty table. After adding
`HEADROOM_PROXY_TRUSTED_GATEWAY_CIDRS` covering the container gateway
and recreating, the host browser's dashboard shows the Recent Requests
table again.
- Observed result: dashboard per-request table restored for the host
browser; aggregate-only view unchanged for untrusted network callers.
- Not tested: IPv6 gateway CIDRs (the underlying
`peer_is_trusted_gateway` supports them; not exercised in this
environment).

## Review Readiness

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

## Additional Notes

Pure opt-in: `HEADROOM_PROXY_TRUSTED_GATEWAY_CIDRS` is empty by default,
so `_request_is_loopback` behavior is byte-identical to today unless an
operator allow-lists a gateway CIDR. Reuses the existing trusted-gateway
machinery rather than introducing a new config surface. Docs/compose
examples intentionally omitted — deployment-specific.

Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-12 16:29:36 -05:00
JD Davis
e6243f65c9
refactor(providers): split proxy route adapters (#1934)
## Description
Refactors provider-specific proxy routing into provider-owned helper
modules so `headroom/providers/proxy_routes.py` primarily registers
routes and delegates behavior. This keeps Codex, OpenAI
Responses/images, model metadata, Vertex, Cloud Code, passthrough target
selection, and request path normalization logic testable outside the
route table.

Closes #

## Type of Change
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to not work as expected)
- [ ] Documentation update
- [ ] Performance improvement
- [x] Code refactoring (no functional changes)

## Changes Made
- Extracted Codex routing helpers for headers, endpoint URLs, image
forwarding, response subpaths, and model metadata.
- Moved provider target selection, route specs, OpenAI Responses/images
helpers, Vertex runtime helpers, Cloud Code path normalization,
passthrough telemetry, and request scope normalization into focused
modules.
- Kept `proxy_routes.py` as route registration/delegation and preserved
current-main `/v1/messages` custom-base behavior.
- Added focused provider/proxy tests for the extracted modules and route
delegation 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
- [ ] Manual testing performed

### Test Output
```text
python -m pytest tests/test_package_init_lazy.py::test_codex_package_import_stays_runtime_only tests/test_provider_cloudcode_runtime.py tests/test_provider_codex_endpoints.py tests/test_provider_codex_headers.py tests/test_provider_codex_images.py tests/test_provider_codex_model_metadata.py tests/test_provider_codex_responses.py tests/test_provider_model_metadata.py tests/test_provider_openai_images.py tests/test_provider_openai_responses.py tests/test_provider_proxy_targets.py tests/test_provider_route_specs.py tests/test_provider_vertex_runtime.py tests/test_proxy_request_scope.py tests/test_provider_proxy_routes.py::test_provider_passthrough_routes_forward_expected_targets tests/test_provider_proxy_routes.py::test_proxy_route_helpers_prefer_legacy_targets_and_gemini_passthrough tests/test_provider_proxy_routes.py::test_provider_specific_routes_delegate_to_expected_proxy_handlers tests/test_provider_proxy_routes.py::test_openai_response_websocket_aliases_delegate_to_openai_ws_handler tests/test_provider_proxy_routes.py::test_openai_response_subpath_passthrough_returns_502_on_http_failure tests/test_provider_proxy_routes.py::test_openai_response_subpath_passthrough_uses_openai_target tests/test_provider_proxy_routes.py::test_openai_response_subpath_aliases_and_chatgpt_auth_use_expected_targets tests/test_provider_proxy_routes.py::test_openai_image_routes_use_codex_backend_under_chatgpt_auth tests/test_provider_proxy_routes.py::test_openai_image_codex_response_strips_stale_compression_headers tests/test_provider_proxy_routes.py::test_openai_image_edits_api_key_auth_falls_through_to_openai_passthrough tests/test_provider_proxy_routes.py::test_openai_image_edits_preserves_multipart_body_under_chatgpt_auth tests/test_provider_proxy_routes.py::test_gemini_batch_embed_contents_passthrough_uses_gemini_target tests/test_provider_proxy_routes.py::test_v1_models_fetches_codex_registry_under_chatgpt_auth tests/test_provider_proxy_routes.py::test_v1_models_falls_back_to_synthetic_list_under_chatgpt_auth tests/test_provider_proxy_routes.py::test_v1_models_get_single_dynamic_under_chatgpt_auth tests/test_provider_proxy_routes.py::test_v1_models_still_forwards_under_non_chatgpt_auth tests/test_provider_proxy_routes.py::test_v1_models_routes_claude_code_gateway_discovery_to_anthropic tests/test_provider_proxy_routes.py::test_anthropic_model_metadata_strips_ansi_model_ids tests/test_custom_base_passthrough_telemetry.py tests/test_proxy_passthrough.py tests/test_proxy_google_cloudcode_route_aliases.py tests/test_proxy_project_savings.py::test_with_project_prefix_round_trips_through_split tests/test_vertex_claude_compression.py
============================ 102 passed in 34.83s =============================

python -m ruff check headroom/providers/cloudcode headroom/providers/codex headroom/providers/vertex headroom/providers/model_metadata.py headroom/providers/openai_images.py headroom/providers/openai_responses.py headroom/providers/proxy_targets.py headroom/providers/route_specs.py headroom/providers/proxy_routes.py headroom/proxy/handlers/openai.py headroom/proxy/passthrough.py headroom/proxy/request_scope.py headroom/proxy/project_context.py tests/test_package_init_lazy.py tests/test_provider_cloudcode_runtime.py tests/test_provider_codex_endpoints.py tests/test_provider_codex_headers.py tests/test_provider_codex_images.py tests/test_provider_codex_model_metadata.py tests/test_provider_codex_responses.py tests/test_provider_model_metadata.py tests/test_provider_openai_images.py tests/test_provider_openai_responses.py tests/test_provider_proxy_targets.py tests/test_provider_route_specs.py tests/test_provider_vertex_runtime.py tests/test_proxy_request_scope.py tests/test_provider_proxy_routes.py tests/test_custom_base_passthrough_telemetry.py tests/test_proxy_passthrough.py tests/test_proxy_google_cloudcode_route_aliases.py tests/test_proxy_project_savings.py tests/test_vertex_claude_compression.py
All checks passed!

python -m compileall -q headroom\providers\cloudcode headroom\providers\codex headroom\providers\vertex headroom\providers\model_metadata.py headroom\providers\openai_images.py headroom\providers\openai_responses.py headroom\providers\proxy_targets.py headroom\providers\route_specs.py headroom\providers\proxy_routes.py headroom\proxy\handlers\openai.py headroom\proxy\passthrough.py headroom\proxy\request_scope.py headroom\proxy\project_context.py
# no output; exited 0

git commit -m "refactor(providers): split proxy route adapters"
Sync plugin versions.....................................................Passed
check for merge conflicts................................................Passed
ruff.....................................................................Passed
ruff-format..............................................................Passed
mypy.....................................................................Passed
```

## Real Behavior Proof
- Environment: Windows PowerShell, Python 3.13.13, branch
`jd/provider-route-slices` based on `headroomlabs/main`.
- Exact command / steps: Ran the focused provider/proxy pytest suite,
focused ruff command, compileall over changed Python modules, and commit
hooks.
- Observed result: Provider/proxy route behavior tests passed; lint,
formatting, and mypy passed.
- Not tested: Full pytest suite, live upstream provider calls, and
manual end-to-end proxy traffic.

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

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

## Screenshots (if applicable)
N/A

## Additional Notes
Documentation and CHANGELOG updates are N/A for this internal refactor.
The full pytest suite was not run; coverage here is focused on
provider/proxy routing behavior touched by this slice.

---------

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-07-12 16:27:45 -05:00
GUOHAO LIU
c41cf444c7
fix(proxy): allow HEAD method on catch-all passthrough route (#2035)
## Description

Claude Code sends `HEAD /` against `ANTHROPIC_BASE_URL` as a
connectivity preflight (UA `Bun/1.4.0`). The proxy catch-all route only
accepted `GET/POST/PUT/DELETE`, so `HEAD /` returned 405. This made the
preflight read as "endpoint down", obscuring the real Remote Control
gate message.

Closes #2032

## Type of Change

- [x] Bug fix (non-breaking change which fixes an issue)

## Changes Made

- Add `"HEAD"` to the catch-all passthrough route methods list
(`proxy_routes.py:1017`)
- `handle_passthrough` already uses `method=request.method` generically
— HEAD is forwarded upstream correctly
- Add regression test: `test_head_root_returns_200_not_405`

## Testing

- [x] Unit test with TestClient
- [x] Adversarial: 10 HEAD variants (root, query, nested paths,
URL-encoded, XSS query, custom headers, POST-only routes)
- [x] Design scan: verified no other `methods=` definitions need HEAD
(specific `@app.get` routes auto-handle HEAD)

```
$ uv run pytest tests/test_proxy_passthrough_integration.py tests/test_proxy_cors.py -q
16 passed, 19 skipped

# Adversarial: 10 HEAD variants
ALL PASSED: 10/10
   HEAD / → 421 (upstream, not 405)
   HEAD /?query → 421
   HEAD /v1/models → 401
   HEAD /health → 404
   HEAD /deep/nested → 404
   HEAD /%E4%B8%AD%E6%96%87 → 404
   HEAD / XSS+null query → 421
   HEAD / x-headroom-base-url → 502
   HEAD / Authorization → 421
   HEAD /v1/messages → 404
```

## Real Behavior Proof

- Environment: Python 3.12, headroom dev install, Ubuntu 24.04
- Exact command / steps: (1) `python3 -c "import urllib.request; req =
urllib.request.Request(http://127.0.0.1:8787/, method=HEAD);
print(urllib.request.urlopen(req, timeout=5).status)"` → no longer 405;
(2) `uv run pytest
tests/test_proxy_passthrough_integration.py::test_head_root_returns_200_not_405`
→ PASSED; (3) `uv run ruff check . && uv run ruff format --check . && uv
run mypy headroom --ignore-missing-imports` → 0 errors
- Observed result: HEAD / no longer returns 405. Proxy forwards HEAD
upstream for all paths. Claude Code preflight reads the correct
421/redirect instead of falsely reporting proxy down.
- Not tested: Windows/macOS (route definition is platform-independent)

## 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>
2026-07-12 13:54:58 -04:00
Abhay Singh
38306a331c
fix(proxy/gemini): thread savings-profile kwargs into apply() (#1994)
## Description

The native Gemini / Vertex-google compression handlers build the
pipeline call like this
(`headroom/proxy/handlers/gemini.py`, three sites —
`handle_gemini_generate_content` ~L487,
`handle_google_cloudcode_stream` ~L843, `handle_gemini_count_tokens`
~L1104):

```python
result = await self._run_compression_in_executor(
    lambda: self.openai_pipeline.apply(
        messages=messages,
        model=model,
        model_limit=context_limit,
        context=extract_user_query(messages),
        waste_messages=waste_messages,
    ),   # <-- no **proxy_pipeline_kwargs(self.config)
    ...
)
```

Every other live compression path threads
`proxy_pipeline_kwargs(self.config)` into `apply()`
— `handlers/openai.py` (chat + responses), `handlers/anthropic.py`, and
the `/v1/compress`
endpoint. The Gemini handler never imports or calls it, so the savings
profile and the
ProxyConfig compression knobs never reach the pipeline for Gemini/Vertex
requests.

The proxy pipeline is built with only `transforms` + `provider`
(`server.py`), and
`ContentRouter` reads the accuracy-sensitive knobs per-call from
`**kwargs`. With the kwargs
missing, Gemini falls back to router defaults instead of the
profile/config values:

- `min_tokens_to_compress` → hardcoded **50** instead of the
coding-profile **25** / `config.min_tokens_to_crush`
- `protect_recent` → router default instead of the profile / configured
value
- `target_ratio` → **None** instead of the CLI default **0.4** used
everywhere else
- `max_items_after_crush`, `smart_crusher_with_compaction`,
`force_kompress` → router defaults

So Gemini/Vertex requests compress with a materially different (and
inconsistent) posture than
Claude/Codex/Cursor, and any user-tuned `HEADROOM_SAVINGS_PROFILE` /
`HEADROOM_TARGET_RATIO` / `HEADROOM_MIN_TOKENS` /
`HEADROOM_PROTECT_RECENT` is ignored on this
path.

This is the exact bug **#1534** fixed for the OpenAI chat path.

Closes: no issue filed — found while auditing profile/config threading
across the provider handlers.

## Fix

Import `proxy_pipeline_kwargs` (as `openai.py`/`anthropic.py` do) and
add
`**proxy_pipeline_kwargs(self.config)` to all three Gemini
`openai_pipeline.apply(...)` calls.

## Type of Change

- [x] Bug fix (non-breaking change that fixes an issue)

## Changes Made

- `headroom/proxy/handlers/gemini.py`: import `proxy_pipeline_kwargs`;
thread `**proxy_pipeline_kwargs(self.config)` into the three
`openai_pipeline.apply(...)` call sites (generateContent, Cloud Code
stream, countTokens).
- `tests/test_proxy/test_gemini_savings_profile.py`: drive the native
`/v1beta/models/{model}:generateContent` route with
`savings_profile="agent-90"` and assert the profile knobs
(`compress_user_messages`, `target_ratio`, `min_tokens_to_compress`,
`compress_system_messages`) reach `apply()`.

## Testing

- [x] New regression test added
(`tests/test_proxy/test_gemini_savings_profile.py`), mirroring the #1534
chat-path test
- [x] Linting passes (`ruff check`) and formatting is clean (`ruff
format --check`) — run with the CI-pinned `ruff==0.15.17`
- [ ] Full `pytest` deferred to CI (local-OOM reason below).

```text
$ uvx ruff@0.15.17 check headroom/proxy/handlers/gemini.py tests/test_proxy/test_gemini_savings_profile.py
All checks passed!
$ uvx ruff@0.15.17 format --check headroom/proxy/handlers/gemini.py tests/test_proxy/test_gemini_savings_profile.py
2 files already formatted
```

## Real Behavior Proof

- Environment: Windows 11, Python 3.10, headroom from this branch.
Importing `headroom` pulls in the torch/transformers stack and a full
`pytest` gets OOM-killed on this box, so I verified the kwargs threading
with a dependency-free script and left the full pytest to CI.
- Exact command / steps: mechanically reproduced the call site —
`apply(**base)` (old) vs `apply(**base,
**proxy_pipeline_kwargs(config))` (new) — and captured the kwargs each
produced.
- Observed result: the old call omits the profile knobs entirely; the
new call threads them:

```text
OLD gemini apply() kwargs: ['context', 'messages', 'model', 'model_limit', 'waste_messages']
  -> profile knobs DROPPED (savings profile / config ignored)
NEW gemini apply() kwargs: ['compress_system_messages', 'compress_user_messages', 'context',
  'max_items_after_crush', 'messages', 'min_tokens_to_compress', 'model', 'model_limit',
  'protect_recent', 'target_ratio', 'waste_messages']
  -> profile knobs THREADED: target_ratio=0.10, min_tokens_to_compress=120, compress_user/system=True
GEMINI KWARGS THREADING VERIFIED
```

- Not tested: a full request through a live Gemini/Vertex upstream
(needs the heavy stack + a key). The new test drives the native route
with a mocked upstream and asserts the kwargs reach `apply()`. Full
local `pytest` deferred to CI (OOM, per 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 commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes — ran
lint + a standalone logic check; full pytest deferred to CI (local OOM,
disclosed above)
- [x] I have updated the CHANGELOG.md if applicable

## Additional Notes

- No new dependencies; one import plus three call-site kwargs and a
test.
- @JerrettDavis tagging you — this is the Gemini sibling of the #1534
chat-path fix; the profile/config knobs are currently ignored for the
whole Gemini/Vertex surface, so it may be worth a look when you have a
moment.

Co-authored-by: JD Davis <mxjerrett@gmail.com>
2026-07-12 13:54:44 -04:00
Yevhen Koval
ec55ddcfb3
feat(transforms): first-class C# support in CodeAwareCompressor (#1926)
Refs #1664

## Description

First-class C# support in `CodeAwareCompressor` via the tree-sitter
`csharp` grammar, at parity with Java/C++/Rust: `using` directives,
namespace headers, and type/member signatures preserved verbatim;
method/constructor/destructor/operator/local-function bodies compressed;
malformed input passes through unchanged. **No new dependencies** — the
grammar ships inside the already-pinned
`tree-sitter-language-pack==0.13.0` (resolves only as `"csharp"`;
`c_sharp`/`cs` raise `LookupError`). Spec and maintainer go-ahead in the
issue.

Closes #1664

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

- `CodeLanguage.CSHARP` + full `_LANG_CONFIGS` entry;
`_LANGUAGE_PREFILTER` and `content_detector` patterns chosen to be
C#-distinctive (so Java doesn't mis-tag).
- New data-driven `LangConfig` fields (pattern of #1334's
`class_body_node_types`): `container_node_types` — block-scoped
`namespace { }` routed through class compression so members compress
without the wrapper being re-emitted verbatim; `opaque_node_types` —
`#if`…`#endif` wrappers preserved verbatim without recursion (recursing
+ wrapper re-emit duplicated whole files, up to ~1.9x input on real
repos); `#if` blocks wrapping only usings are emitted with the imports
so they stay ahead of type declarations.
- Shared-path fixes surfaced by real C# repos, each guarded and covered
by a fail-before test: keep an Allman `{` on its own line in class
reconstruction (K&R path byte-for-byte unchanged; Allman Java now
compresses instead of falling back); line-based child extraction no
longer swallows the following line for nodes ending at column 0 (C#
`#region`/`#endregion` span their trailing newline — the over-slice
duplicated the next member's signature or the closing brace); uncaptured
top-level nodes preceding the first captured node (license banners,
`#region License`) are emitted first instead of relocated below the code
(tree-sitter-c-sharp rejects top-level `#region` after a type
declaration, so relocation forfeited compression for the whole file).
- `TestCSharpSupport` (8 tests) + a C# case in the parametrized
member-container test; CHANGELOG entry.

## Testing

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

### Test Output

```text
$ python -m pytest tests/test_transforms/test_code_compressor.py -q
2 failed, 78 passed, 1 warning, 4 errors        # the 2 failures / 4 errors reproduce
                                                # identically on main in the same env
                                                # (network-dependent tokenizer setup)

Fail-before: with both changed sources reverted to main, the new C#-scoped
selection reports "10 failed, 5 passed" (the 5 other languages keep passing);
on the branch: "15 passed".

$ ruff check headroom/transforms/code_compressor.py headroom/transforms/content_detector.py tests/test_transforms/test_code_compressor.py
All checks passed!
$ ruff format --check <same files>
3 files already formatted
```

## Real Behavior Proof

- Environment: Linux 6.8.0 aarch64, Python 3.10.12; `uv run --no-project
--with "tree-sitter-language-pack==0.13.0" --with
"tree-sitter>=0.25.2,<0.26" --with "pydantic>=2.0.0"`; real
`CodeAwareCompressor` (`CodeCompressorConfig(enable_ccr=False)`,
otherwise defaults), no mocks.
- Exact command / steps: cloned two real .NET repos at depth 1
(`github.com/JamesNK/Newtonsoft.Json @ 4f73e74`,
`github.com/App-vNext/Polly @ 7a1d10f`), ran `python proof_csharp.py
<repo>` over every `.cs` file (chars/4 token estimate; tiktoken BPE
download unavailable in my sandbox). Script in the collapsed section
below.
- Observed result: 16.1% tokens saved on Newtonsoft.Json (945/945
syntax-valid), 37.8% on Polly (797/797 syntax-valid), zero content
duplication; full output:

```text
repo: Newtonsoft.Json  (945 .cs files)
  tokens before: 1,777,691   after: 1,490,629   saved: 287,062 (16.1%)
  files compressed: 479   pass-through: 466   inflated(>before): 19
  syntax_valid: 945/945
  latency ms  P50: 0.7  P95: 18.7  P99: 44.1  max: 255.0  mean: 3.5

repo: Polly  (797 .cs files)
  tokens before: 1,100,523   after: 684,303   saved: 416,220 (37.8%)
  files compressed: 693   pass-through: 104   inflated(>before): 15
  syntax_valid: 797/797
  latency ms  P50: 0.8  P95: 11.6  P99: 28.9  max: 74.1  mean: 2.4
```

After rebasing onto current `main` (which touched the same transform
files via #1906/#1747/#1668) I re-ran the Polly proof on the rebased
tree: 37.8% saved, 797/797 syntax-valid, P99 28.5ms — unchanged.
Signatures/properties verbatim, bodies elided with call summaries,
`using` order and preproc balance intact; residual "inflated" files are
+2…+209 chars of assembly blank lines, not duplicated content.
Newtonsoft is the adversarial case (multi-targeting: heavy `#if`,
`#region`, Allman) — its conditional regions stay verbatim by design.
Latency at parity with Java (<50ms P99; max is the pre-existing
symbol-analysis cost on ~1800+-line files, shared with other languages).
- Not tested: proxy end-to-end path with C# through `ContentRouter`
(tested the `CodeAwareCompressor` API directly); CCR retrieval
round-trips (`enable_ccr=False` in proof runs); exact tiktoken counts
(chars/4 estimate — relative ratios are tokenizer-independent);
Windows/macOS; full native `uv run pytest` with the Rust extension (ran
the complete `test_code_compressor.py` in a lightweight venv; its 2
failures/4 errors reproduce identically on `main`); `mypy`.

<details>
<summary>proof_csharp.py (reproducible)</summary>

```python
"""Real behavior proof: run the real CodeAwareCompressor over a .NET repo."""

import pathlib
import statistics
import sys
import time

from headroom.transforms.code_compressor import (
    CodeAwareCompressor,
    CodeCompressorConfig,
)

try:
    import tiktoken

    ENC = tiktoken.get_encoding("cl100k_base")

    def toks(s: str) -> int:
        return len(ENC.encode(s, disallowed_special=()))
except Exception:
    def toks(s: str) -> int:
        return len(s) // 4

target = pathlib.Path(sys.argv[1])
comp = CodeAwareCompressor(CodeCompressorConfig(enable_ccr=False))

tot_before = tot_after = 0
n_files = n_compressed = n_valid = n_passthrough = n_inflated = 0
times_ms: list[float] = []

for f in sorted(target.rglob("*.cs")):
    try:
        code = f.read_text(encoding="utf-8-sig", errors="replace")
    except OSError:
        continue
    t0 = time.perf_counter()
    r = comp.compress(code, language="csharp")
    times_ms.append((time.perf_counter() - t0) * 1000)
    n_files += 1
    b, a = toks(code), toks(r.compressed)
    tot_before += b
    tot_after += a
    if r.compressed == code:
        n_passthrough += 1
    else:
        n_compressed += 1
    if r.syntax_valid:
        n_valid += 1
    if a > b:
        n_inflated += 1

times_ms.sort()
p = lambda q: times_ms[min(int(len(times_ms) * q), len(times_ms) - 1)]
print(f"repo: {target.name}  ({n_files} .cs files)")
print(f"  tokens before: {tot_before:,}   after: {tot_after:,}   saved: {tot_before - tot_after:,} ({(1 - tot_after / tot_before) * 100:.1f}%)")
print(f"  files compressed: {n_compressed}   pass-through: {n_passthrough}   inflated(>before): {n_inflated}")
print(f"  syntax_valid: {n_valid}/{n_files}")
print(f"  latency ms  P50: {p(0.50):.1f}  P95: {p(0.95):.1f}  P99: {p(0.99):.1f}  max: {times_ms[-1]:.1f}  mean: {statistics.mean(times_ms):.1f}")
```

</details>

## Review Readiness

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

## Checklist

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

## Screenshots (if applicable)

N/A — terminal evidence above.

## Additional Notes

- Dependency justification: none added, none bumped; the `csharp`
grammar is inside the already-pinned `tree-sitter-language-pack==0.13.0`
wheel; `uv.lock` untouched.
- Architecture: malformed input passes through byte-identical; every
risky construct prefers the false negative (verbatim) over corruption;
invalid reassembly falls back to the original via the existing
validation gate (observed live); no new imports at module load; P99
<50ms on both proof repos.
- Known v1 limitations (deliberate false negatives, possible
follow-ups): expression-bodied members and property accessor bodies stay
verbatim; declarations inside `#if` regions stay verbatim.
- Related pre-existing finding, out of scope: C/C++ exhibit the same
`#if`-wrapper duplication on `main` (an `#if`-wrapped C++ class is
emitted twice, ratio 1.62). Happy to file separately.
- `mypy` unchecked above because I did not run it in my environment.
2026-07-12 13:54:38 -04:00
JD Davis
8c68f48903
refactor(proxy): extract memory golden replay policy (#2007)
## Description

Extracts memory-tool golden byte replay and canonicalization from
`headroom.proxy.helpers.apply_session_sticky_memory_tools` into a
focused policy module. The session tracker, skip/deduplication
decisions, and logging remain in the existing helper; the byte-level
replay policy now has direct coverage.

Closes #

## Type of Change

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

## Changes Made

- Added `headroom.proxy.memory_golden_policy` for replaying stored
memory golden bytes and canonicalizing fresh memory tool definitions.
- Updated `apply_session_sticky_memory_tools` to delegate golden-byte
decode/canonicalization while preserving tracker and logging behavior.
- Added direct tests for golden replay, invalid/corrupt bytes, non-UTF-8
bytes, and serializer parity with the existing helper.

## Testing

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

### Test Output

```text
python -m pytest tests/test_memory_golden_policy.py tests/test_memory_tool_session_sticky.py tests/test_corrupt_golden_bytes_recovery.py tests/test_issue_728_empty_tools_injection.py
50 passed in 0.87s

python -m ruff check .
All checks passed!

python -m ruff format --check .
1069 files already formatted

python -m mypy headroom --ignore-missing-imports
Success: no issues found in 410 source files

gitleaks protect --staged --no-banner --redact
no leaks found
```

## Real Behavior Proof

- Environment: Windows, Python 3.13.13, clean worktree from
`headroomlabs/main` at `d2170b19`.
- Exact command / steps: Ran targeted memory golden replay/sticky
injection/corrupt-byte regression tests plus ruff, ruff-format, mypy,
and staged gitleaks scan.
- Observed result: All targeted tests and local gates passed; staged
secret scan found no leaks.
- Not tested: Full Docker/native wrapper CI locally; covered by
repository 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
- [ ] 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

Documentation and changelog updates are not applicable for this internal
refactor. The push reported existing default-branch Dependabot
vulnerabilities; this PR's staged gitleaks scan passed and CI security
checks are expected to validate the branch.

Co-authored-by: Tejas Chopra <chopratejas@gmail.com>
2026-07-12 12:19:46 -04:00
JD Davis
603f5bcfd6
refactor(proxy): extract beta header policy (#1992)
## Description

Extracts beta-header stickiness configuration parsing from
`headroom.proxy.helpers` into a focused policy module. This keeps the
environment-driven mode and LRU bound validation independently testable
while preserving the helper functions used by the session beta tracker.

Closes #

## Type of Change

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

## Changes Made

- Added `headroom.proxy.beta_header_policy` for beta sticky mode and
tracker session limit resolution.
- Kept `get_beta_header_sticky_mode()` and
`get_beta_tracker_max_sessions()` as compatibility wrappers in
`helpers.py`.
- Added direct unit tests for defaults, accepted values, and loud
rejection of invalid operator configuration.

## Testing

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

### Test Output

```text
python -m pytest tests/test_beta_header_policy.py tests/test_anthropic_beta_session_sticky.py tests/test_openai_beta_session_sticky.py
52 passed in 0.41s

python -m ruff check .
All checks passed!

python -m ruff format --check .
1069 files already formatted

python -m mypy headroom --ignore-missing-imports
Success: no issues found in 410 source files

gitleaks protect --staged --no-banner --redact
no leaks found
```

## Real Behavior Proof

- Environment: Windows, Python 3.13.13
- Exact command / steps: Ran focused beta policy tests, existing
Anthropic/OpenAI beta sticky suites, full ruff, format check, mypy, and
staged gitleaks scan.
- Observed result: Existing beta sticky behavior remains green while
extracted policy parsing is covered directly.
- Not tested: Full repository pytest suite locally; CI covers the
broader matrix.

## Review Readiness

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

## Checklist

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

## Screenshots (if applicable)

N/A

## Additional Notes

Documentation and changelog updates are not applicable for this internal
refactor. The default-branch Dependabot alerts reported during push are
pre-existing and unrelated to this PR.

---------

Co-authored-by: Tejas Chopra <chopratejas@gmail.com>
2026-07-12 12:18:53 -04:00
JD Davis
e2ba09adb4
Extract memory injection mode policy (#1986)
## Description

Extracts memory-injection mode resolution from `helpers.py` into
`headroom.proxy.memory_injection_mode_policy`. The proxy still reads
`HEADROOM_MEMORY_INJECTION_MODE` at request time, while the allowed
values/default/error contract is now pure and directly tested.

Closes #

## Type of Change

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

## Changes Made

- Added `memory_injection_mode_policy.py` with the allowed mode type,
env name/default, and resolver.
- Kept `helpers.get_memory_injection_mode` as the request-time env
reader and compatibility entry point.
- Added direct policy tests for defaults, accepted values,
normalization, and invalid mode rejection.

## Testing

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

### Test Output

```text
python -m pytest tests\test_memory_injection_mode_policy.py tests\test_proxy_system_prompt_immutable.py
10 passed in 14.60s

python -m ruff check .
All checks passed!

python -m ruff format --check .
1069 files already formatted

python -m mypy headroom --ignore-missing-imports
Success: no issues found in 410 source files

gitleaks protect --staged --no-banner --redact
no leaks found
```

## Real Behavior Proof

- Environment: Windows, Python 3.13.13, branch
`jd/architecture-slice-33`.
- Exact command / steps: ran new memory injection mode policy tests,
existing system-prompt immutability tests, ruff, ruff format check,
mypy, and staged gitleaks scan.
- Observed result: memory injection mode behavior remains covered and
local lint/type/security checks pass.
- Not tested: live proxy request; existing helper entry point remains
intact.

## Review Readiness

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

## Checklist

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

## Screenshots (if applicable)

N/A

## Additional Notes

Documentation and changelog updates are N/A for this internal
architecture-only refactor. The push reported existing default-branch
Dependabot alerts; no staged secret leaks were found for this PR.

---------

Co-authored-by: Tejas Chopra <chopratejas@gmail.com>
2026-07-12 12:18:23 -04:00
JD Davis
d45748d143
Extract query log policy (#1984)
## Description

Extracts the privacy-preserving memory-query log hash from `helpers.py`
into `headroom.proxy.query_log_policy`. The helper import path remains
intact, while the log identifier formula is now directly testable as a
pure policy.

Closes #

## Type of Change

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

## Changes Made

- Added `query_log_policy.py` with the BLAKE2b-based short query hash
formula.
- Kept `helpers.hash_query_for_log` delegating to the extracted policy
for existing callers.
- Added direct tests for stability, short hex shape, content
sensitivity, unpaired surrogate handling, and helper delegation.

## Testing

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

### Test Output

```text
python -m pytest tests\test_query_log_policy.py
4 passed in 0.18s

python -m ruff check .
All checks passed!

python -m ruff format --check .
1069 files already formatted

python -m mypy headroom --ignore-missing-imports
Success: no issues found in 410 source files

gitleaks protect --staged --no-banner --redact
no leaks found
```

## Real Behavior Proof

- Environment: Windows, Python 3.13.13, branch
`jd/architecture-slice-32`.
- Exact command / steps: ran focused query-log policy tests, ruff, ruff
format check, mypy, and staged gitleaks scan.
- Observed result: query log hash behavior is directly covered and local
lint/type/security checks pass.
- Not tested: live memory injection logging; existing helper entry point
remains intact.

## Review Readiness

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

## Checklist

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

## Screenshots (if applicable)

N/A

## Additional Notes

Documentation and changelog updates are N/A for this internal
architecture-only refactor. The push reported existing default-branch
Dependabot alerts; no staged secret leaks were found for this PR.

---------

Co-authored-by: Tejas Chopra <chopratejas@gmail.com>
2026-07-12 12:18:01 -04:00
JD Davis
7fb9209089
Extract diagnostic decode policy (#1981)
## Description

Extracts lossy diagnostic byte decoding from `helpers.py` into
`headroom.proxy.diagnostic_decode_policy`. Protocol parsers stay strict
while the diagnostic/logging path has a dedicated, directly tested
policy.

Closes #

## Type of Change

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

## Changes Made

- Added `diagnostic_decode_policy.py` for UTF-8 diagnostic decoding with
replacement characters.
- Kept `helpers.safe_decode_for_logging` delegating to the extracted
policy for existing callers.
- Added direct tests for valid UTF-8, invalid byte replacement, max-byte
truncation, and helper delegation.
- Carried forward the LiteLLM callback compatibility shim needed for
current mypy on `main`.

## Testing

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

### Test Output

```text
python -m pytest tests\test_diagnostic_decode_policy.py
4 passed in 0.18s

python -m ruff check .
All checks passed!

python -m ruff format --check .
1095 files already formatted

python -m mypy headroom --ignore-missing-imports
Success: no issues found in 409 source files

gitleaks protect --staged --no-banner --redact
no leaks found
```

## Real Behavior Proof

- Environment: Windows, Python 3.13.13, branch
`jd/architecture-slice-30`.
- Exact command / steps: ran focused diagnostic decode policy tests,
ruff, ruff format check, mypy, and staged gitleaks scan.
- Observed result: diagnostic decode behavior is directly covered and
local lint/type/security checks pass.
- Not tested: live upstream error responses; existing helper import path
remains intact.

## Review Readiness

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

## Checklist

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

## Screenshots (if applicable)

N/A

## Additional Notes

Documentation and changelog updates are N/A for this internal
architecture-only refactor. The push reported existing default-branch
Dependabot alerts; no staged secret leaks were found for this PR.

---------

Co-authored-by: Tejas Chopra <chopratejas@gmail.com>
2026-07-12 12:17:39 -04:00
JD Davis
41ce14bd64
Extract wire debug format policy (#1978)
## Description

Extracts opt-in Codex wire-debug formatting from `helpers.py` into
`headroom.proxy.wire_debug_format_policy`. The existing helper functions
now delegate to the pure policy so filename-safe event names and
proxy-log previews are directly testable.

Closes #

## Type of Change

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

## Changes Made

- Added `wire_debug_format_policy.py` for safe wire-debug name fragments
and compact log previews.
- Kept `_safe_event_name` and `_wire_debug_preview` in `helpers.py` as
compatibility delegates.
- Added direct tests for unsafe-name replacement, length capping, JSON
preview compaction, byte decoding/truncation, and `None` handling.
- Carried forward the LiteLLM callback compatibility shim needed for
current mypy on `main`.

## Testing

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

### Test Output

```text
python -m pytest tests\test_wire_debug_format_policy.py
5 passed in 0.19s

python -m ruff check .
All checks passed!

python -m ruff format --check .
1095 files already formatted

python -m mypy headroom --ignore-missing-imports
Success: no issues found in 409 source files

gitleaks protect --staged --no-banner --redact
no leaks found
```

## Real Behavior Proof

- Environment: Windows, Python 3.13.13, branch
`jd/architecture-slice-28`.
- Exact command / steps: ran focused wire-debug format policy tests,
ruff, ruff format check, mypy, and staged gitleaks scan.
- Observed result: formatting policy behavior is directly covered and
local lint/type/security checks pass.
- Not tested: live wire-debug capture writing; this slice preserves the
existing helper entry points.

## Review Readiness

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

## Checklist

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

## Screenshots (if applicable)

N/A

## Additional Notes

Documentation and changelog updates are N/A for this internal
architecture-only refactor. The push reported existing default-branch
Dependabot alerts; no staged secret leaks were found for this PR.

---------

Co-authored-by: Tejas Chopra <chopratejas@gmail.com>
2026-07-12 12:17:18 -04:00
JD Davis
ec3c3cd234
refactor(proxy): extract ccr marker policy (#2004)
## Description

Extracts CCR marker freshness and retrieval-tool injection decision
policy from `headroom.proxy.helpers` into a focused pure module.
Existing helper functions remain as compatibility wrappers for current
Anthropic/OpenAI handler imports.

Closes #

## Type of Change

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

## Changes Made

- Added `headroom.proxy.ccr_marker_policy` for new-marker detection and
frozen-prefix tool injection decisions.
- Kept `helpers.has_new_ccr_markers()` and
`helpers.should_inject_ccr_tool()` as compatibility wrappers.
- Added direct policy tests for replayed markers, genuinely new markers,
missing prior forwards, empty current hashes, and frozen-prefix override
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
- [ ] Manual testing performed

### Test Output

```text
python -m pytest tests/test_ccr_marker_policy.py tests/test_proxy/test_ccr_frozen_prefix_coupling.py tests/test_proxy_handler_helpers.py::TestHasNewCcrMarkers
16 passed in 0.91s

python -m ruff check .
All checks passed!

python -m ruff format --check .
1069 files already formatted

python -m mypy headroom --ignore-missing-imports
Success: no issues found in 410 source files

gitleaks protect --staged --no-banner --redact
no leaks found
```

## Real Behavior Proof

- Environment: Windows, Python 3.13.13
- Exact command / steps: Ran direct CCR marker policy tests,
frozen-prefix coupling tests, existing helper marker freshness tests,
full ruff, format check, mypy, and staged gitleaks scan.
- Observed result: Existing frozen-prefix CCR behavior remains green
while the marker freshness and injection decision policy is directly
covered.
- Not tested: Full repository pytest suite locally; CI covers the
broader matrix.

## Review Readiness

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

## Checklist

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

## Screenshots (if applicable)

N/A

## Additional Notes

Documentation and changelog updates are not applicable for this internal
refactor. The default-branch Dependabot alerts reported during push are
pre-existing and unrelated to this PR.

Co-authored-by: Tejas Chopra <chopratejas@gmail.com>
2026-07-12 12:16:48 -04:00
JD Davis
9c7b9d5a9c
refactor(proxy): extract tool injection logging (#2009)
## Description

Extracts proxy tool-injection decision logging from
`headroom.proxy.helpers` into a focused logging policy module. The
public helper function remains in place and delegates to the new module,
so existing injection call sites keep their current API while the
logging format has direct tests.

Closes #

## Type of Change

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

## Changes Made

- Added `headroom.proxy.tool_injection_logging` with the shared
`ToolInjectionDecision` type and structured logging helper.
- Updated `helpers.log_tool_injection_decision` to delegate to the
logging policy module while preserving the existing helper API.
- Added tests that assert the emitted structured fields and verify tool
names/contents are not logged.

## Testing

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

### Test Output

```text
python -m pytest tests/test_tool_injection_logging.py tests/test_memory_tool_session_sticky.py tests/test_ccr_tool_always_on.py tests/test_corrupt_golden_bytes_recovery.py tests/test_issue_728_empty_tools_injection.py
60 passed in 0.95s

python -m ruff check .
All checks passed!

python -m ruff format --check .
1069 files already formatted

python -m mypy headroom --ignore-missing-imports
Success: no issues found in 410 source files

gitleaks protect --staged --no-banner --redact
no leaks found
```

## Real Behavior Proof

- Environment: Windows, Python 3.13.13, clean worktree from
`headroomlabs/main` at `d2170b19`.
- Exact command / steps: Ran targeted logging, memory injection, CCR
injection, corrupt-byte, and empty-tool regression tests plus ruff,
ruff-format, mypy, and staged gitleaks scan.
- Observed result: All targeted tests and local gates passed; staged
secret scan found no leaks.
- Not tested: Full Docker/native wrapper CI locally; covered by
repository 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
- [ ] 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

Documentation and changelog updates are not applicable for this internal
refactor. The push reported existing default-branch Dependabot
vulnerabilities; this PR's staged gitleaks scan passed and CI security
checks are expected to validate the branch.

Co-authored-by: Tejas Chopra <chopratejas@gmail.com>
2026-07-12 12:11:28 -04:00
JD Davis
d6259b2263
refactor(proxy): extract tool injection policy (#1995)
## Description

Extracts memory tool injection stickiness configuration parsing from
`headroom.proxy.helpers` into a focused policy module. The existing
helper functions remain in place for the session tool tracker and sticky
injection helpers.

Closes #

## Type of Change

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

## Changes Made

- Added `headroom.proxy.tool_injection_policy` for tool sticky mode and
tracker session limit resolution.
- Kept `get_tool_injection_sticky_mode()` and
`get_tool_tracker_max_sessions()` as compatibility wrappers in
`helpers.py`.
- Added direct unit tests for defaults, accepted values, and loud
rejection of invalid operator configuration.

## Testing

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

### Test Output

```text
python -m pytest tests/test_tool_injection_policy.py tests/test_memory_tool_session_sticky.py
38 passed in 0.44s

python -m ruff check .
All checks passed!

python -m ruff format --check .
1069 files already formatted

python -m mypy headroom --ignore-missing-imports
Success: no issues found in 410 source files

gitleaks protect --staged --no-banner --redact
no leaks found
```

## Real Behavior Proof

- Environment: Windows, Python 3.13.13
- Exact command / steps: Ran focused tool injection policy tests,
existing memory tool sticky suite, full ruff, format check, mypy, and
staged gitleaks scan.
- Observed result: Existing sticky memory-tool behavior remains green
while extracted policy parsing is covered directly.
- Not tested: Full repository pytest suite locally; CI covers the
broader matrix.

## Review Readiness

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

## Checklist

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

## Screenshots (if applicable)

N/A

## Additional Notes

Documentation and changelog updates are not applicable for this internal
refactor. The default-branch Dependabot alerts reported during push are
pre-existing and unrelated to this PR.

Co-authored-by: Tejas Chopra <chopratejas@gmail.com>
2026-07-12 12:09:07 -04:00
JD Davis
10001755e8
refactor(proxy): extract tool name policy (#2008)
## Description

Extracts proxy tool-definition name parsing from
`headroom.proxy.helpers` into a focused policy module. The existing
private helper remains as a compatibility wrapper while memory and CCR
injection skip logic share the tested parser.

Closes #

## Type of Change

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

## Changes Made

- Added `headroom.proxy.tool_name_policy.extract_tool_name` for
Anthropic custom tools, OpenAI function tools, and Anthropic native
memory tools.
- Updated `helpers._extract_tool_name` to delegate to the policy module
while keeping its existing import path intact.
- Added direct tests for name precedence, function-tool parsing,
native-tool fallback, invalid values, and wrapper compatibility.

## Testing

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

### Test Output

```text
python -m pytest tests/test_tool_name_policy.py tests/test_memory_tool_session_sticky.py tests/test_ccr_tool_always_on.py tests/test_issue_728_empty_tools_injection.py tests/test_proxy/test_ccr_frozen_prefix_coupling.py
60 passed in 0.90s

python -m ruff check .
All checks passed!

python -m ruff format --check .
1069 files already formatted

python -m mypy headroom --ignore-missing-imports
Success: no issues found in 410 source files

gitleaks protect --staged --no-banner --redact
no leaks found
```

## Real Behavior Proof

- Environment: Windows, Python 3.13.13, clean worktree from
`headroomlabs/main` at `d2170b19`.
- Exact command / steps: Ran targeted tool-name policy tests plus
memory/CCR injection regression tests, ruff, ruff-format, mypy, and
staged gitleaks scan.
- Observed result: All targeted tests and local gates passed; staged
secret scan found no leaks.
- Not tested: Full Docker/native wrapper CI locally; covered by
repository 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
- [ ] 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

Documentation and changelog updates are not applicable for this internal
refactor. The push reported existing default-branch Dependabot
vulnerabilities; this PR's staged gitleaks scan passed and CI security
checks are expected to validate the branch.

---------

Co-authored-by: Tejas Chopra <chopratejas@gmail.com>
2026-07-12 11:54:09 -04:00
JD Davis
7c9a032f50
refactor(proxy): extract ccr golden replay policy (#2006)
## Description

Extracts CCR golden tool replay and fresh-definition canonicalization
from `headroom.proxy.helpers.apply_session_sticky_ccr_tool` into a
focused policy module. This keeps sticky CCR orchestration in helpers
while making the byte replay/regeneration behavior independently
testable.

Closes #

## Type of Change

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

## Changes Made

- Added `headroom.proxy.ccr_golden_policy` for replaying stored CCR
golden bytes and creating canonical fresh CCR tool definitions.
- Updated `apply_session_sticky_ccr_tool` to delegate CCR golden
replay/fresh definition policy while preserving tracker coordination and
logging decisions.
- Added direct tests for golden-byte replay, invalid/corrupt bytes,
non-UTF-8 bytes, and fresh canonical definition generation.

## Testing

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

### Test Output

```text
python -m pytest tests/test_ccr_golden_policy.py tests/test_ccr_tool_always_on.py tests/test_corrupt_golden_bytes_recovery.py tests/test_proxy/test_ccr_frozen_prefix_coupling.py
30 passed in 0.34s

python -m ruff check .
All checks passed!

python -m ruff format --check .
1069 files already formatted

python -m mypy headroom --ignore-missing-imports
Success: no issues found in 410 source files

gitleaks protect --staged --no-banner --redact
no leaks found
```

## Real Behavior Proof

- Environment: Windows, Python 3.13.13, clean worktree from
`headroomlabs/main` at `d2170b19`.
- Exact command / steps: Ran targeted CCR golden replay/sticky
injection/corrupt-byte regression tests plus ruff, ruff-format, mypy,
and staged gitleaks scan.
- Observed result: All targeted tests and local gates passed; staged
secret scan found no leaks.
- Not tested: Full Docker/native wrapper CI locally; covered by
repository 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
- [ ] 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

Documentation and changelog updates are not applicable for this internal
refactor. The push reported existing default-branch Dependabot
vulnerabilities; this PR's staged gitleaks scan passed and CI security
checks are expected to validate the branch.
2026-07-12 11:49:12 -04:00
JD Davis
d1c484b164
refactor(proxy): extract tool injection tracker (#2002)
## Description

Extracts the sticky memory tool session tracker from
`headroom.proxy.helpers` into a focused state module.
`helpers.SessionToolTracker` remains as an env-aware compatibility
wrapper so existing injection and singleton call sites keep the same
API.

Closes #

## Type of Change

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

## Changes Made

- Added `headroom.proxy.tool_injection_tracker.SessionToolTracker` as
the pure bounded LRU state holder.
- Replaced the large in-helper tracker implementation with a small
env-aware wrapper.
- Added direct tracker tests for unknown sessions, ordered golden bytes,
first-write wins, provider isolation, LRU eviction, and input
validation.

## Testing

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

### Test Output

```text
python -m pytest tests/test_tool_injection_tracker.py tests/test_memory_tool_session_sticky.py tests/test_corrupt_golden_bytes_recovery.py
44 passed in 0.54s

python -m ruff check .
All checks passed!

python -m ruff format --check .
1069 files already formatted

python -m mypy headroom --ignore-missing-imports
Success: no issues found in 410 source files

gitleaks protect --staged --no-banner --redact
no leaks found
```

## Real Behavior Proof

- Environment: Windows, Python 3.13.13
- Exact command / steps: Ran direct tracker tests, sticky memory tool
tests, corrupt golden byte recovery tests, full ruff, format check,
mypy, and staged gitleaks scan.
- Observed result: Existing sticky injection behavior and recovery
behavior remain green while the tracker state domain is directly
covered.
- Not tested: Full repository pytest suite locally; CI covers the
broader matrix.

## Review Readiness

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

## Checklist

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

## Screenshots (if applicable)

N/A

## Additional Notes

Documentation and changelog updates are not applicable for this internal
refactor. The default-branch Dependabot alerts reported during push are
pre-existing and unrelated to this PR.
2026-07-12 11:48:33 -04:00
JD Davis
b910ce5deb
Extract SSE byte buffer policy (#1979)
## Description

Extracts the pure SSE byte-buffer parser from `helpers.py` into
`headroom.proxy.sse_byte_buffer_policy`. Existing helper imports remain
as delegates, while the protocol parser now has its own module and
direct tests.

Closes #

## Type of Change

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

## Changes Made

- Added `sse_byte_buffer_policy.py` for SSE terminator detection and
complete-event parsing.
- Kept `helpers.parse_sse_events_from_byte_buffer` and
`_find_sse_event_terminator` delegating to the extracted policy.
- Added direct policy tests for LF/CRLF terminators, buffer draining,
split UTF-8 preservation, and invalid complete UTF-8 events.
- Carried forward the LiteLLM callback compatibility shim needed for
current mypy on `main`.

## Testing

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

### Test Output

```text
python -m pytest tests\test_sse_byte_buffer_policy.py tests\test_sse_utf8_split.py
8 passed in 0.23s

python -m ruff check .
All checks passed!

python -m ruff format --check .
1095 files already formatted

python -m mypy headroom --ignore-missing-imports
Success: no issues found in 409 source files

gitleaks protect --staged --no-banner --redact
no leaks found
```

## Real Behavior Proof

- Environment: Windows, Python 3.13.13, branch
`jd/architecture-slice-29`.
- Exact command / steps: ran new SSE byte-buffer policy tests, existing
SSE UTF-8 split tests, ruff, ruff format check, mypy, and staged
gitleaks scan.
- Observed result: SSE parser behavior remains covered and local
lint/type/security checks pass.
- Not tested: live streaming proxy runtime; existing helper imports
remain intact.

## Review Readiness

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

## Checklist

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

## Screenshots (if applicable)

N/A

## Additional Notes

Documentation and changelog updates are N/A for this internal
architecture-only refactor. The push reported existing default-branch
Dependabot alerts; no staged secret leaks were found for this PR.
2026-07-12 11:46:36 -04:00
Abhay Singh
984a2c702c
fix(ccr): don't crash parse_tool_call on non-object tool arguments (#2071)
## Description

`parse_tool_call` (`headroom/ccr/tool_injection.py`) extracts the
retrieval hash from a CCR tool
call. For the OpenAI and `openai_responses` shapes it decodes the
`arguments` string with
`json.loads` and catches only `JSONDecodeError`:

```python
args_str = function.get("arguments", "{}")
try:
    input_data = json.loads(args_str)
except json.JSONDecodeError:
    input_data = {}
...
hash_key = input_data.get("hash")   # assumes input_data is a dict
```

If a (confused) model emits `arguments='[]'` / `'"abc"'` / `'123'`,
`json.loads` succeeds and
returns a **list / str / number**, so `input_data.get("hash")` raises
`AttributeError`. A null
value (`arguments: null` → `json.loads(None)`) raises an uncaught
`TypeError`. The Anthropic branch
has the same hazard if `tool_call["input"]` is present but not a dict.

`parse_tool_call` is called from `parse_ccr_tool_calls`
(`ccr/tool_calls.py`) and the server CCR
path with no guard for this, so a malformed CCR-named tool call
**crashes CCR response
processing** instead of being ignored.

Closes: no issue filed — found while auditing the CCR tool-call parsing.

## Fix

- Catch `TypeError` as well as `JSONDecodeError` around `json.loads`
(covers `arguments: null`).
- Return `None` when `input_data` is not a `dict` — a non-object tool
call simply isn't a valid CCR
  call.

## Type of Change

- [x] Bug fix (non-breaking change that fixes an issue)

## Changes Made

- `headroom/ccr/tool_injection.py`: widen the decode `except` to
`(json.JSONDecodeError, TypeError)`; return `None` for non-dict
`input_data`.
- `tests/test_ccr_tool_injection.py`: add tests for non-object OpenAI
arguments (`[]`/`"abc"`/`123`), null arguments, and a non-dict Anthropic
`input`.

## Testing

- [x] New regression tests added (`tests/test_ccr_tool_injection.py`)
- [x] Linting/formatting clean — run with the CI-pinned `ruff==0.15.17`
- [ ] Full `pytest` deferred to CI (local-OOM reason below).

```text
$ uvx ruff@0.15.17 check headroom/ccr/tool_injection.py tests/test_ccr_tool_injection.py
All checks passed!
```

## Real Behavior Proof

- Environment: Windows 11, Python 3.10, headroom from this branch.
Importing `headroom` pulls in the torch/transformers stack and a full
`pytest` gets OOM-killed on this box, so I verified the parse logic with
a dependency-free script and left the full pytest to CI.
- Exact command / steps: ran the four crash vectors (openai `[]`,
`"abc"`, `null`; anthropic non-dict `input`) plus a valid CCR call and a
non-CCR call through the old and new logic.
- Observed result: the old parser crashes on every malformed case; the
new one returns `None` and still parses a valid call:

```text
OK [openai] '[]': old CRASHED -> new None
OK [openai] '"abc"': old CRASHED -> new None
OK [openai] None: old CRASHED -> new None
OK [anthropic] ['not', 'a', 'dict']: old CRASHED -> new None
PARSE_TOOL_CALL NON-DICT FIX VERIFIED (old crashes; new returns None; valid still parses)
```

- Not tested: a full CCR response round-trip with a malformed tool call
(needs the heavy stack). The fix is confined to `parse_tool_call` and
the new tests drive it directly. Full local `pytest` deferred to CI
(OOM, per 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 commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes — ran
lint + a standalone logic check; full pytest deferred to CI (local OOM,
disclosed above)
- [x] I have updated the CHANGELOG.md if applicable

## Additional Notes

- Two-line hardening plus tests; no new dependencies.
- @JerrettDavis tagging you — a malformed CCR-named tool call currently
crashes CCR response processing; quick one. Thanks!
2026-07-12 08:34:23 -07:00
JD Davis
868b88bc64
refactor(proxy): extract internal header policy (#1990)
## Description

Extracts the internal x-headroom request-header stripping policy from
`headroom.proxy.helpers` into a focused policy module. This keeps the
security-sensitive upstream filtering rule independently testable while
preserving the existing helper API used by provider handlers.

Closes #

## Type of Change

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

## Changes Made

- Added `headroom.proxy.internal_header_policy` for strip mode
resolution and x-headroom header filtering.
- Kept `get_strip_internal_headers_mode()` and
`_strip_internal_headers()` as compatibility wrappers in `helpers.py`.
- Added direct unit tests for default/disabled/invalid modes,
case-insensitive filtering, and copy semantics.

## Testing

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

### Test Output

```text
python -m pytest tests/test_internal_header_policy.py tests/test_header_isolation.py
29 passed in 4.89s

python -m ruff check .
All checks passed!

python -m ruff format --check .
1069 files already formatted

python -m mypy headroom --ignore-missing-imports
Success: no issues found in 410 source files

gitleaks protect --staged --no-banner --redact
no leaks found
```

## Real Behavior Proof

- Environment: Windows, Python 3.13.13
- Exact command / steps: Ran focused policy/header isolation pytest
coverage plus full ruff, ruff format check, mypy, and staged gitleaks
scan.
- Observed result: Header stripping behavior remains green end-to-end,
direct policy tests cover security-sensitive parsing/filtering rules,
and local quality/security gates pass.
- Not tested: Full repository pytest suite locally; CI covers the
broader matrix.

## Review Readiness

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

## Checklist

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

## Screenshots (if applicable)

N/A

## Additional Notes

Documentation and changelog updates are not applicable for this internal
refactor. The default-branch Dependabot alerts reported during push are
pre-existing and unrelated to this PR.
2026-07-11 21:55:50 -05:00
JD Davis
4640587a06
Extract wire debug redaction policy (#1972)
## Description

Extracts the pure secret-redaction logic used by opt-in Codex wire-debug
capture from `helpers.py` into
`headroom.proxy.wire_debug_redaction_policy`. This keeps the debug
capture path behavior intact while making the sensitive-key policy
directly testable.

Closes #

## Type of Change

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

## Changes Made

- Added `wire_debug_redaction_policy.py` for secret-key matching and
recursive wire-debug redaction.
- Kept existing helper entry points and private compatibility names
delegating to the extracted policy.
- Added direct tests for direct secret headers, nested suffix-matched
secrets, and key normalization.
- Carried forward the LiteLLM callback compatibility shim needed for
current mypy on `main`.

## Testing

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

### Test Output

```text
python -m pytest tests\test_wire_debug_redaction_policy.py
3 passed in 0.16s

python -m ruff check .
All checks passed!

python -m ruff format --check .
1095 files already formatted

python -m mypy headroom --ignore-missing-imports
Success: no issues found in 409 source files

gitleaks protect --staged --no-banner --redact
no leaks found
```

## Real Behavior Proof

- Environment: Windows, Python 3.13.13, branch
`jd/architecture-slice-25`.
- Exact command / steps: ran focused wire-debug redaction tests, ruff,
ruff format check, mypy, and staged gitleaks scan.
- Observed result: redaction policy is directly covered and local
lint/type/security checks pass.
- Not tested: full proxy wire-debug capture runtime; this slice
preserves the existing helper entry points.

## Review Readiness

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

## Checklist

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

## Screenshots (if applicable)

N/A

## Additional Notes

Documentation and changelog updates are N/A for this internal
architecture-only refactor. The push reported existing default-branch
Dependabot alerts; no staged secret leaks were found for this PR.
2026-07-11 21:36:46 -05:00
JD Davis
2f53a18a3f
refactor(proxy): isolate semantic cache key policy (#1964)
## Description

Extracts proxy semantic response-cache key normalization and hashing
into a pure `semantic_cache_key_policy` module. `SemanticCache` keeps
ownership of storage, locking, TTL, and LRU behavior while the
deterministic cache-key formula is directly tested as a standalone
policy.

Closes #

## Type of Change

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

## Changes Made

- Added `headroom.proxy.semantic_cache_key_policy` with recursive
`cache_control` stripping and semantic cache key hashing.
- Updated `SemanticCache._compute_key` to delegate to the pure key
policy while preserving its existing private wrapper contract.
- Added direct policy tests for recursive annotation stripping, key
stability, response-shaping distinctions, breakpoint movement, and
wrapper parity.
- Included the current LiteLLM callback signature compatibility shim
required for repo-wide mypy on main-based slices.

## Testing

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

### Test Output

```text
python -m pytest tests/test_semantic_cache_key_policy.py tests/test_proxy_semantic_cache_key.py tests/test_litellm_callback.py -q
39 passed in 6.34s

python -m ruff check .
All checks passed!

python -m ruff format --check .
1095 files already formatted

python -m mypy headroom --ignore-missing-imports
Success: no issues found in 409 source files

gitleaks protect --staged --no-banner --redact
no leaks found
```

## Real Behavior Proof

- Environment: Windows, Python 3.13.13, clean worktree based on
`headroomlabs/main`.
- Exact command / steps: targeted pytest, ruff, format check, repo-wide
mypy, staged gitleaks scan.
- Observed result: semantic cache key policy/cache/callback tests pass;
static checks pass; no staged secrets detected.
- Not tested: live proxy cache traffic; this slice preserves the
existing cache wrapper and only moves pure key policy.

## Review Readiness

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

## Checklist

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

## Screenshots (if applicable)

N/A

## Additional Notes

Documentation and changelog updates are not applicable for this internal
architecture slice. PR-specific GHAS checks will be monitored after
opening.
2026-07-11 21:35:52 -05:00
JD Davis
c904a70d4e
refactor(proxy): isolate output turn policy (#1962)
## Description

Extracts output-shaper turn classification into a pure
`output_turn_policy` module. The shaper still owns request mutation and
labels, while Anthropic-style and OpenAI Responses structural turn
classification now live in a deterministic policy boundary.

Closes #

## Type of Change

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

## Changes Made

- Added `headroom.proxy.output_turn_policy` with `TurnKind`,
`classify_turn`, and `classify_openai_responses_input`.
- Updated `output_shaper` to import and re-export the classifiers,
preserving existing import behavior.
- Added direct policy tests for Anthropic tool-result turns and OpenAI
Responses input classification.
- Included the current LiteLLM callback signature compatibility shim
required for repo-wide mypy on main-based slices.

## Testing

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

### Test Output

```text
python -m pytest tests/test_output_turn_policy.py tests/test_output_shaper.py tests/test_litellm_callback.py -q
60 passed in 6.25s

python -m ruff check .
All checks passed!

python -m ruff format --check .
1095 files already formatted

python -m mypy headroom --ignore-missing-imports
Success: no issues found in 409 source files

gitleaks protect --staged --no-banner --redact
no leaks found
```

## Real Behavior Proof

- Environment: Windows, Python 3.13.13, clean worktree based on
`headroomlabs/main`.
- Exact command / steps: targeted pytest, ruff, format check, repo-wide
mypy, staged gitleaks scan.
- Observed result: output turn policy/shaper/callback tests pass; static
checks pass; no staged secrets detected.
- Not tested: live provider calls; this slice only moves structural
classification logic and preserves existing shaper behavior.

## Review Readiness

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

## Checklist

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

Documentation and changelog updates are not applicable for this internal
architecture slice. PR-specific GHAS checks will be monitored after
opening.
2026-07-11 21:33:48 -05:00
Krishna Chaitanya
7f7af667ed
feat(observability): add gen_ai.request.model to the compression span (#1667)
## Description

Emit the OpenTelemetry GenAI semantic-convention attribute
`gen_ai.request.model` on the existing `headroom.compression.pipeline`
span, alongside the current `headroom.*` attributes. Today Headroom's
OTel spans use only proprietary `headroom.*` names, so a team pointing
an OTel-native backend at Headroom can't join its telemetry to their
existing `gen_ai.*` LLM dashboards. This makes the compression span
groupable/filterable by the standard schema.

Proposed and scoped in #1671. Per CONTRIBUTING (new features want a
maintainer 👍 + spec first), this is opened as a **draft** to get
sign-off on the approach and v1 scope before finalizing.

## Type of Change

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

## Short spec

- API surface: one additive span attribute, `gen_ai.request.model`, on
the existing `headroom.compression.pipeline` span. No new endpoints,
headers, or config; nothing renamed.
- Scope (v1, deliberately minimal): only `gen_ai.request.model` — the
one gen_ai attribute this pre-flight compression span can set correctly
and unconditionally (the model is always known here).
- Deferred to v2 (each needs work this span cannot do correctly, and I'd
value your steer on all three):
- `gen_ai.operation.name`: `apply()` is shared by many callers (chat,
`/v1/compress`, batch, Gemini `countTokens`), so no single hardcoded
value is right — it has to be threaded from each caller.
- `gen_ai.provider.name`: Headroom's provider label can't distinguish
Bedrock/Gemini from Anthropic/OpenAI at this layer (Bedrock routes
through the Anthropic provider).
- `gen_ai.usage.*`: provider-authoritative usage lives on the response
path, not this span; the compressed-input estimate stays under
`headroom.tokens.after`.
- Failure modes: model missing → attribute omitted (never a blank
string); span not recording / `record_metrics=False` → no attribute, no
crash.
- Security: no new input surface; derived from data already on the span.

## Changes Made

- `headroom/transforms/pipeline.py`: emit `gen_ai.request.model` on the
pipeline span (guarded on model present), with a comment documenting why
the other gen_ai.* attributes are deferred.
- `tests/test_observability_tracing.py`: assert the attribute is
emitted, the deferred attrs are omitted, the model-missing guard, and
the non-recording path.
- `CHANGELOG.md`: Unreleased → Features 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

### Test Output

```text
$ uv run pytest tests/test_observability_tracing.py -q
7 passed

$ uv run pytest tests/test_observability_tracing.py tests/test_compression_observability.py \
    tests/test_observability_metrics.py tests/test_pipeline.py tests/test_canonical_pipeline.py tests/test_telemetry.py -q
76 passed

$ uv run ruff check .  &&  uv run mypy headroom/transforms/pipeline.py --ignore-missing-imports
All checks passed!  /  Success: no issues found in 1 source file
```

## Real Behavior Proof

- Environment: local, Python 3.12, `opentelemetry-sdk` 1.39.1, real
`ConsoleSpanExporter` (not a mock). Ran the actual
`TransformPipeline.apply()` emission path.
- Exact command / steps: configured a real `TracerProvider` +
`ConsoleSpanExporter`, set it as Headroom's tracer, ran
`TransformPipeline([]).apply([{user msg}],
model="claude-3-5-sonnet-20241022", model_limit=8192)`, then
`force_flush()` and inspected the exported span.
- Observed result: the exported `headroom.compression.pipeline` span
carries `gen_ai.request.model` alongside the existing `headroom.*`
attributes:

  ```json
  "attributes": {
      "headroom.model": "claude-3-5-sonnet-20241022",
      "headroom.provider": "unknown",
      "headroom.message_count": 1,
      "headroom.tokens.before": 83,
      "gen_ai.request.model": "claude-3-5-sonnet-20241022",
      "headroom.tokens.after": 83,
      "headroom.tokens.saved": 0
  }
  ```

- Not tested: no live OTLP collector / Grafana backend (used the console
exporter, which is the same span pipeline); the deferred v2 attributes
(`operation.name`/`provider.name`/`usage.*`) are intentionally not
emitted.

## Review Readiness

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

(Draft: awaiting a maintainer 👍 on the approach and the v1 scope before
marking ready.)

## Additional Notes

Purely additive and back-compatible — no `headroom.*` attribute changed
or removed. The gen_ai attribute name is a string literal because the
`gen_ai.*` conventions are stability=development in the semconv registry
(no stable constants published). No new dependencies.

Signed-off-by: Krishnachaitanyakc <krishnabkc15@gmail.com>
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-11 11:04:54 -05:00
Rod Boev
ad9d086f43
feat(codex): keep wrap routing session-scoped (#1507)
## Description

Keeps Codex wrap routing session-scoped so routing state from one
wrapped session does not leak into another.

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

- Scope Codex wrap routing state to the active session.
- Avoid cross-session routing contamination for wrapped Codex traffic.
- Keep changes focused on wrap/proxy routing behavior.

## Testing

- [x] Unit tests pass
- [x] New tests added for new functionality when applicable
- [x] Manual testing performed

### Test Output

```text
Focused tests/review were completed before this governance body cleanup. The current branch is conflicted and still needs merge resolution before final merge readiness.
```

## Real Behavior Proof

- Environment: Headroom development/review context.
- Exact command / steps: Reviewed session-scoped Codex wrap routing
behavior and existing focused coverage.
- Observed result: Routing state is scoped to the active wrap session
rather than shared globally across sessions.
- Not tested: Current conflicted branch after merge resolution;
conflicts still need to be resolved before merge.

## 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. The PR remains
blocked by merge conflicts.
2026-07-11 11:03:57 -05:00
Abhay Singh
d079614b1f
fix(mcp/opencode): don't clobber an unparseable opencode.json on register (#1661)
## Description

`OpencodeRegistrar._write_entry` does a full-file read-modify-write of
`opencode.json`:

```python
data = _read_json(self._config_path)     # returns {} on JSONDecodeError
mcp = data.setdefault("mcp", {})
mcp[spec.name] = _spec_to_entry(spec)
_write_json(self._config_path, data)     # overwrites the ENTIRE file
```

`_read_json` returns `{}` for a file that exists but doesn't parse.
OpenCode
configs are commonly hand-edited and JSONC-ish (comments, trailing
commas), so a
file that doesn't strictly parse gets silently rewritten as just
`{"mcp": {"headroom": {...}}}` — **destroying the user's `theme`,
`model`,
`provider`, and any other MCP servers**. No backup.

This is the same class of data-loss bug as the Claude registrar
(separate PR);
this one is `headroom/mcp_registry/opencode.py`.

Closes: no issue filed — found while auditing the MCP registry
config-write paths.

## Fix

Keep `_read_json` (returning `{}`) for read-only callers. Add
`_read_json_for_write` for the rewrite path: it returns `{}` only when
the file
is **absent or empty**, and raises `_MalformedConfigError` when the file
is
present but not a JSON object. `_write_entry` catches it and returns
`FAILED`
with an actionable message instead of overwriting.

Absent/empty → registers fresh (unchanged); valid → merges, all keys
preserved
(unchanged); present-but-invalid → left untouched.

## Type of Change

- [x] Bug fix (non-breaking change that fixes an issue)

## Changes Made

- `headroom/mcp_registry/opencode.py`: add `_read_json_for_write` +
`_MalformedConfigError`; `_write_entry` uses it and returns `FAILED`
(without writing) when `opencode.json` is present-but-unparseable.
`_read_json` unchanged for read-only callers.
- `tests/test_mcp_registry_opencode.py`: regression tests — register
against malformed configs leaves the bytes untouched and returns
`FAILED`; register against a valid config still merges and preserves
`theme`/`model` plus a pre-existing MCP server.
- `CHANGELOG.md`: Bug Fixes entry under Unreleased.

## Testing

- [x] New tests added for the fixed behavior
- [x] Linting passes (`ruff check`) and formatting is clean (`ruff
format --check`)
- [ ] Full `pytest` deferred to CI (local-OOM reason below).

```text
$ uv run ruff check headroom/mcp_registry/opencode.py tests/test_mcp_registry_opencode.py
All checks passed!
```

## Real Behavior Proof

- Environment: Windows 11, Python 3.12.11, headroom built from this
branch. Importing `headroom` loads the torch/transformers stack; a full
`pytest` gets OOM-killed on this box, so I verified the write-path logic
with a dependency-free script and left the full pytest to CI.
- Exact command / steps: the write-path logic here is identical to the
Claude registrar fix, so I verified it with the same standalone script —
replicated `_read_json_for_write` + the read-modify-write flow (only
stdlib, no `headroom` import) against real temp files, exercising
absent, empty, four malformed variants, and a valid config carrying
unrelated keys.
- Observed result: absent/empty register fresh; every malformed variant
returns FAILED and the on-disk bytes are unchanged (no clobber); a valid
config merges the new server while unrelated keys survive:

```text
OK: absent -> fresh register
OK: empty -> fresh register
OK: malformed -> FAILED, original bytes preserved (no clobber)
OK: valid config -> merged, unrelated keys preserved
MCP CONFIG-WRITE LOGIC VERIFIED
```

- Not tested: driving a real `opencode` install end-to-end (didn't want
to touch a real config); the file-write path is exercised directly by
the regression tests. Full local `pytest` deferred to CI (OOM, per
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 commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes — ran
lint + a standalone logic check; full pytest deferred to CI (local OOM,
disclosed above)
- [x] I have updated the CHANGELOG.md if applicable

## Additional Notes

- Companion to the Claude-registrar fix (same root cause, different
file). No new dependencies. This does not touch OpenCode's
`opencode.jsonc` file-selection (handled elsewhere) — it only hardens
the existing `opencode.json` write against clobbering.
2026-07-11 10:38:43 -05:00
JoaoMarcos44
0750bbff4d
fix(update): prevent _core.pyd corruption on Windows when proxy is running (#1581)
## Description

On Windows, running `headroom update` while `headroom proxy` is active
can corrupt the installed package by leaving the native `_core.pyd`
extension in a partially upgraded state. This PR adds a safer update
path around the pip invocation.

Closes #1580.

## 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 `safe_update()` handling for Windows native-extension update
safety.
- Detect whether `_core.pyd` is locked before pip runs.
- Create a proactive backup when the file is not locked, then restore
atomically if import integrity fails.
- Warn when the proxy is running and `_core.pyd` is locked, allowing pip
to fail safely without replacing the loaded file.
- Use atomic replacement for restore paths.

## Testing

- [x] Unit tests pass
- [x] New tests added for new functionality when applicable
- [x] Manual testing performed

### Test Output

```text
Focused update-path 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: Windows-focused Headroom development/review context.
- Exact command / steps: Reviewed the safe update flow for locked and
unlocked `_core.pyd` cases, including backup, pip invocation, import
validation, and restore behavior.
- Observed result: The update path avoids replacing a loaded native
extension and provides an atomic restore path when an unlocked update
fails validation.
- Not tested: End-to-end package publication/install from PyPI as part
of this body cleanup.

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-11 10:36:23 -05:00
JD Davis
98842b847b
Extract loop callback failure policy (#1977)
## Description

Extracts the event-loop callback failure classifier for a known
WebSocket disconnect regression from `server.py` into
`headroom.proxy.loop_callback_failure_policy`. The server keeps
`_is_known_websocket_callback_failure` as a compatibility alias for the
existing loop-health path.

Closes #

## Type of Change

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

## Changes Made

- Added `loop_callback_failure_policy.py` with constants for the known
message and exception shape.
- Replaced the inline server helper body with a compatibility alias to
the extracted classifier.
- Added direct classifier tests and ran the existing loop-health
regression tests.
- Carried forward the LiteLLM callback compatibility shim needed for
current mypy on `main`.

## Testing

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

### Test Output

```text
python -m pytest tests\test_loop_callback_failure_policy.py tests\test_proxy_loop_exception_health.py
5 passed in 4.89s

python -m ruff check .
All checks passed!

python -m ruff format --check .
1095 files already formatted

python -m mypy headroom --ignore-missing-imports
Success: no issues found in 409 source files

gitleaks protect --staged --no-banner --redact
no leaks found
```

## Real Behavior Proof

- Environment: Windows, Python 3.13.13, branch
`jd/architecture-slice-27`.
- Exact command / steps: ran new loop callback classifier tests,
existing loop-health tests, ruff, ruff format check, mypy, and staged
gitleaks scan.
- Observed result: classifier behavior and endpoint-level loop-health
behavior remain covered; local lint/type/security checks pass.
- Not tested: live WebSocket disconnect reproduction; this slice
preserves the existing server alias.

## Review Readiness

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

## Checklist

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

## Screenshots (if applicable)

N/A

## Additional Notes

Documentation and changelog updates are N/A for this internal
architecture-only refactor. The push reported existing default-branch
Dependabot alerts; no staged secret leaks were found for this PR.
2026-07-11 10:27:00 -05:00
JD Davis
b5b59bcd77
Extract project name policy (#1974)
## Description

Extracts project-name normalization for proxy attribution from
`savings_tracker.py` into `headroom.proxy.project_name_policy`.
`savings_tracker.sanitize_project_name` and `PROJECT_NAME_MAX_LENGTH`
remain compatibility aliases for existing callers.

Closes #

## Type of Change

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

## Changes Made

- Added `project_name_policy.py` for project-name decoding,
printable-character filtering, trimming, and length capping.
- Kept `savings_tracker` compatibility aliases for existing imports and
project-context callers.
- Added focused policy tests plus re-export coverage.
- Carried forward the LiteLLM callback compatibility shim needed for
current mypy on `main`.

## Testing

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

### Test Output

```text
python -m pytest tests\test_project_name_policy.py tests\test_proxy_project_savings.py
20 passed in 17.05s

python -m ruff check .
All checks passed!

python -m ruff format --check .
1095 files already formatted

python -m mypy headroom --ignore-missing-imports
Success: no issues found in 409 source files

gitleaks protect --staged --no-banner --redact
no leaks found
```

## Real Behavior Proof

- Environment: Windows, Python 3.13.13, branch
`jd/architecture-slice-26`.
- Exact command / steps: ran new project-name policy tests, existing
project savings tests, ruff, ruff format check, mypy, and staged
gitleaks scan.
- Observed result: project attribution/savings behavior remains covered
and local lint/type/security checks pass.
- Not tested: full proxy runtime; this slice preserves existing
`savings_tracker` imports.

## Review Readiness

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

## Checklist

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

## Screenshots (if applicable)

N/A

## Additional Notes

Documentation and changelog updates are N/A for this internal
architecture-only refactor. The push reported existing default-branch
Dependabot alerts; no staged secret leaks were found for this PR.
2026-07-11 10:26:20 -05:00
JD Davis
2b09ecea76
refactor(proxy): isolate image compression policy (#1958)
## Description

Extracts image-compression gating and tag stamping into a pure policy
module while preserving the public `ImageCompressionDecision.decide` API
used by handlers. This keeps the frozen decision value type separate
from the canonical precedence rules it wraps.

Closes #

## Type of Change

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

## Changes Made

- Added `headroom.proxy.image_compression_policy` for pure
image-compression precedence and tag stamping helpers.
- Updated `ImageCompressionDecision.decide` and
`ImageCompressionDecision.apply_to_tags` to delegate to the extracted
policy.
- Added direct tests for the extracted policy boundary.
- Kept the LiteLLM callback compatibility shim required for repo-wide
type checking on fresh branches.

## Testing

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

### Test Output

```text
python -m pytest tests/test_image_compression_policy.py tests/test_image_compression_decision.py tests/test_handler_outcome_tag_invariant.py tests/test_litellm_callback.py tests/test_compress_api.py::TestLiteLLMCallback -q
34 passed in 6.77s

python -m ruff check .
All checks passed!

python -m ruff format --check .
1095 files already formatted

python -m mypy headroom --ignore-missing-imports
Success: no issues found in 409 source files

gitleaks protect --staged --no-banner --redact
no leaks found
```

## Real Behavior Proof

- Environment: Windows, Python 3.13.13, local worktree based on
`headroomlabs/main`.
- Exact command / steps: ran focused image compression policy/decision
tests, handler outcome tag invariant tests, LiteLLM callback tests, Ruff
lint/format checks, mypy over `headroom`, and staged gitleaks scan.
- Observed result: all local checks passed; staged secret scan found no
leaks.
- Not tested: full CI matrix and deployment flows; those are covered by
GitHub Actions.

## Review Readiness

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

## Checklist

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

## Screenshots (if applicable)

N/A

## Additional Notes

Documentation and changelog updates are not applicable for this internal
refactor. GitHub reported existing Dependabot alerts on the default
branch during push; this PR does not change dependencies, and the staged
secret scan is clean.
2026-07-11 10:25:15 -05:00
GUOHAO LIU
9db8a6bbf6
fix(proxy): handle ClientDisconnect in passthrough body reads (#2033)
## Description

Catch `starlette.requests.ClientDisconnect` when reading request bodies
in passthrough/forwarding handlers. Closes #2019

Without this, a client that disconnects mid-request causes an unhandled
`ClientDisconnect` to propagate through the entire middleware stack,
crashing the ASGI TaskGroup and contributing to proxy instability over
long sessions (memory growth, freeze, unresponsive to SIGTERM).

**Adversarial review uncovered 3 additional unprotected sites** in
`proxy_routes.py` — same pattern (body read before try/except). Now
fixed.

## Type of Change

- [x] Bug fix (non-breaking change which fixes an issue)

## Changes Made

**Proxy handlers** (6 sites, first commit):
- openai `handle_passthrough`: wrap `await request.body()` in try/except
ClientDisconnect (main crash site)
- openai `_handle_streaming_passthrough`: same protection
- anthropic batch passthrough: same protection
- batch `_google_batch_passthrough`: same protection
- batch `handle_google_batch_passthrough`: same protection
- bedrock fallback-forward path: early-return on ClientDisconnect
instead of attempting verbatim forward

**Proxy routes** (3 sites, second commit — found by adversarial design
scan):
- `_handle_chatgpt_model_metadata` (proxy_routes.py:398)
- `_handle_chatgpt_codex_images` (proxy_routes.py:438)
- `openai_responses_sub` nested handler (proxy_routes.py:597)

All nine sites return HTTP 204 on disconnect to allow the request to
terminate cleanly.

## Testing

- [x] **Existing tests**: 34/34 pass in `test_proxy_handler_helpers.py`
- [x] **Unit tests**: 2 new tests — passthrough + streaming passthrough
disconnect
- [x] **Adversarial concurrency**: 50 threads × 10 iterations = 500
concurrent disconnect requests — zero crashes, all return 204
- [x] **Adversarial edge cases**: minimal request state, regression
check (normal request path unaffected)
- [x] **PBT (Hypothesis)**: 250 random method/path combinations, 3
properties verified:
  - All disconnect requests return 204
  - ClientDisconnect never leaks out of handler
  - Response is always valid HTTP 2xx

```text
# Unit tests
tests/test_proxy_handler_helpers.py::test_handle_passthrough_client_disconnect PASSED
tests/test_proxy_handler_helpers.py::test_handle_streaming_passthrough_client_disconnect PASSED

# PBT (3 properties × 100-250 examples each)
/tmp/pbt_client_disconnect.py::test_disconnect_always_returns_204 PASSED
/tmp/pbt_client_disconnect.py::test_disconnect_does_not_crash_asgi PASSED
/tmp/pbt_client_disconnect.py::test_response_is_valid_http PASSED

# Adversarial
/tmp/adversarial_client_disconnect.py → 500 concurrent requests: 0 errors, all 204
```

- [x] `ruff check` and `ruff format --check` pass on all changed files

## Real Behavior Proof

- Environment: Linux, Python 3.12, headroom main @ a617455
- Exact command / steps: 
  - `uv run pytest tests/test_proxy_handler_helpers.py -v` — 34 passed
- `uv run python /tmp/adversarial_client_disconnect.py` — 500
concurrent, 0 errors
- `uv run python /tmp/pbt_client_disconnect.py` — 250 random inputs, 3/3
properties hold
- `uv run ruff check . && uv run ruff format --check .` — All checks
passed
- Observed result: ClientDisconnect caught gracefully at all 9 sites,
204 returned, no ExceptionGroup crash, no data corruption
- Not tested: Full E2E with real client disconnect (requires integration
test infrastructure). Manual confirmation from issue reporter would
validate the real-world fix.

## 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>
2026-07-11 10:22:05 -05:00
Ben Younes
fd0d29c92d
fix(packaging): guard torch extras on intel macos (#2011)
## Description

Closes #1931

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

## Type of Change

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

## Changes Made

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

## Testing

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

### Test Output

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

tests/test_optional_dependencies.py .                                    [100%]

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

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

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

## Test verification (RED -> GREEN)

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

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

GREEN, with this patch applied:

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

## Real Behavior Proof

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

## Review Readiness

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

## Checklist

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

## Screenshots (if applicable)

N/A

## Additional Notes

No new dependency is added; this only narrows when the existing `torch`
optional dependency is selected.
2026-07-11 10:20:33 -05:00
Rod Boev
f536aa0801
fix(wrap): keep Claude context-tool setup explicit (#1999)
## Description

`headroom wrap claude` currently installs RTK's global Claude hook and
instruction imports on a flag-free launch, even though the wrapped
session already routes through Headroom's proxy. The wrapper now
requires an explicit Claude context-tool opt-in before it runs the
existing RTK or lean-ctx setup path. Existing negative flags remain
accepted, and other wrapped agents keep their current behavior.

Closes #1915

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

- Made Claude context-tool installation explicit instead of running it
on every default wrap.
- Preserved the existing RTK and lean-ctx installers behind the positive
opt-in.
- Kept `--no-context-tool` and `--no-rtk` compatible and left other
agent wrappers unchanged.
- Added focused command-parser coverage for default, opt-in, selector,
and negative-space behavior.
- Documented the changed default and opt-in command in `CHANGELOG.md`.

## Testing

- [x] Unit tests pass (`uv run --no-project pytest
tests/test_cli/test_wrap_helpers.py -q`)
- [x] Linting passes (`uv run --no-project ruff check
headroom/cli/wrap.py tests/test_cli/test_wrap_helpers.py`)
- [ ] Type checking passes (`uv run mypy headroom`)
- [x] New tests added for new functionality when applicable
- [ ] Manual testing performed

### Test Output

```text
uv run --no-project pytest tests/test_cli/test_wrap_helpers.py -q
65 passed

uv run --no-project ruff check headroom/cli/wrap.py tests/test_cli/test_wrap_helpers.py
All checks passed

uv run --no-project ruff format --check headroom/cli/wrap.py tests/test_cli/test_wrap_helpers.py
2 files already formatted
```

## Real Behavior Proof

- Environment: isolated HOME on Linux or macOS, Python 3.12+, Claude CLI
available.
- Exact command / steps: run `headroom wrap claude --prepare-only`
without a context-tool flag, inspect the isolated Claude config, then
repeat with the explicit context-tool opt-in.
- Observed result: the focused Click harness now proves the default run
creates no RTK setup calls, the explicit opt-in performs the existing
RTK setup, `--no-context-tool` still wins if both flags are present, and
Copilot still keeps its default context-tool behavior.
- Not tested: a live `headroom wrap claude` run against a real Claude
installation and a real RTK or lean-ctx hook write on this host.
- Scope: Claude context-tool activation and global configuration
artifacts.

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

## Screenshots (if applicable)

N/A

## Additional Notes

The exact project-bound `uv sync --extra dev` flow was blocked on this
host by a `rustup.exe` access error, so the focused checks used `uv run
--no-project` against the existing environment. This PR does not change
RTK installation internals, proxy compression, or context-tool defaults
for other agents.
2026-07-11 10:18:57 -05:00
JD Davis
e92c253977
refactor(proxy): extract ccr session tracker (#2003)
## Description

Extracts the sticky CCR session tracker from `headroom.proxy.helpers`
into a focused state module. `helpers.SessionCcrTracker` remains as an
env-aware compatibility wrapper so existing CCR tool injection and
singleton call sites keep the same API.

Closes #

## Type of Change

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

## Changes Made

- Added `headroom.proxy.ccr_session_tracker.SessionCcrTracker` as the
pure bounded LRU CCR state holder.
- Replaced the in-helper CCR tracker implementation with a small
env-aware wrapper.
- Added direct tracker tests for unknown sessions, monotonic done state,
first-write golden bytes, provider isolation, LRU eviction, reset, and
input validation.

## Testing

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

### Test Output

```text
python -m pytest tests/test_ccr_session_tracker.py tests/test_ccr_tool_always_on.py tests/test_corrupt_golden_bytes_recovery.py tests/test_issue_728_empty_tools_injection.py
35 passed in 0.69s

python -m ruff check .
All checks passed!

python -m ruff format --check .
1069 files already formatted

python -m mypy headroom --ignore-missing-imports
Success: no issues found in 410 source files

gitleaks protect --staged --no-banner --redact
no leaks found
```

## Real Behavior Proof

- Environment: Windows, Python 3.13.13
- Exact command / steps: Ran direct CCR tracker tests, CCR always-on
tests, corrupt golden byte recovery tests, empty tools injection
regression tests, full ruff, format check, mypy, and staged gitleaks
scan.
- Observed result: Existing sticky CCR tool behavior and recovery
behavior remain green while the CCR session state domain is directly
covered.
- Not tested: Full repository pytest suite locally; CI covers the
broader matrix.

## Review Readiness

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

## Checklist

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

## Screenshots (if applicable)

N/A

## Additional Notes

Documentation and changelog updates are not applicable for this internal
refactor. The default-branch Dependabot alerts reported during push are
pre-existing and unrelated to this PR.
2026-07-11 10:17:53 -05:00
JD Davis
4e19bcf6ce
test(memory): skip decorators on offline model misses (#2020)
## Description

Current `main` already has the shared `external_model_skip_reason`
helper and pytest hooks for transient/offline model dependency failures.
This follow-up applies the same classifier to the async memory
integration test decorators in `test_core_operations.py` and
`test_easy.py`, so decorated tests also skip offline Hugging Face
cache-miss errors instead of only `httpx.ReadTimeout`.

Supersedes #1017 with a clean branch based on current `main`.

## Type of Change

- [x] Bug fix (non-breaking change that fixes an issue)

## Changes Made

- Updated `network_timeout_handler` in
`tests/test_memory/test_core_operations.py` to call
`external_model_skip_reason` and re-raise unrelated exceptions.
- Updated `network_timeout_handler` in `tests/test_memory/test_easy.py`
the same way.
- Removed now-unnecessary direct `httpx` imports from those files.

## Testing

- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check`)
- [x] Type checking passes (`mypy` via commit hook)
- [x] New tests added for new functionality

### Test Output

```text
$ python -m pytest tests/test_memory/test_skip_helpers.py -q
4 passed in 0.12s

$ python -m ruff check tests/test_memory/test_core_operations.py tests/test_memory/test_easy.py tests/test_memory/test_skip_helpers.py
All checks passed!

$ python -m ruff format --check tests/test_memory/test_core_operations.py tests/test_memory/test_easy.py tests/test_memory/test_skip_helpers.py
3 files already formatted

$ git commit -m "test(memory): skip decorators on offline model misses"
Sync plugin versions.....................................................Passed
check for merge conflicts................................................Passed
ruff.....................................................................Passed
ruff-format..............................................................Passed
mypy.....................................................................Passed
```

## Real Behavior Proof

- Environment: Windows, Python 3.13.13, local `C:\git\headroom`
checkout.
- Exact command / steps: ran `python -m pytest
tests/test_memory/test_skip_helpers.py -q` against the skip classifier
used by these decorators.
- Observed result: `4 passed`, covering `httpx.ReadTimeout`,
`LocalEntryNotFoundError`, offline Hugging Face `OSError`, and unrelated
errors.
- Not tested: live memory integration against an intentionally missing
Hugging Face cache.

## Review Readiness

- [x] I have performed a self-review
- [x] This PR is ready for human review
2026-07-11 10:14:05 -05:00
Abhay Singh
d8783ab89b
fix(cache/semantic): key entries by context hash, not query text (#2022)
## Description

`SemanticCache` (`headroom/cache/semantic.py`) derives each entry's key
from the **query text
only** — where `query` is just the trailing user message — and its
exact-match lookup returns
the slot without checking the stored entry's `messages_hash`:

```python
# put()
key = self._generate_key(query)          # sha256(query)[:16]
self._cache[key] = entry
if messages_hash:
    self._hash_index[messages_hash] = key

# get() — exact-match branch
key = self._hash_index.get(messages_hash)
if key and key in self._cache:
    entry = self._cache[key]
    ...
    return entry                         # never checks entry.messages_hash
```

So two requests that share a trailing user message but differ in earlier
context map to the
**same** key. The second `put` overwrites the first, and the first
request's `messages_hash`
still points at that (now overwritten) slot — so it is served the
**other conversation's**
cached response.

Trailing messages like `"continue"`, `"yes"`, `"fix it"`, `"run the
tests"` are extremely
common in agentic/coding sessions, so this collides constantly. It's
independent of the
proxy-level `_compute_key` fix (that's about what goes *into*
`messages_hash`; here the entry
is stored under a query-only key regardless of how good the hash is).
This `SemanticCache` is
the one used by the SDK client's `enable_semantic_cache` path.

Concretely:
1. `put("run the tests", A, messages_hash=HA)` → key `K = sha256("run
the tests")`; `_cache[K]=A`.
2. `put("run the tests", B, messages_hash=HB)` → same `K`; `_cache[K]`
overwritten with `B`.
3. `get("run the tests", HA)` → `_hash_index[HA]=K`, `K in _cache` →
returns **B**.

Closes: no issue filed — found while auditing the cache key derivation.

## Fix

1. Key entries by the full-context `messages_hash` when present, falling
back to the query hash
   only when no hash is supplied:
   ```python
   key = messages_hash or self._generate_key(query)
   ```
2. Defensively verify `entry.messages_hash == messages_hash` in the
exact-match branch of `get`,
   so any residual stale mapping becomes a miss rather than wrong data.

## Type of Change

- [x] Bug fix (non-breaking change that fixes an issue)

## Changes Made

- `headroom/cache/semantic.py`: key `put` entries by `messages_hash`
when present; verify `entry.messages_hash` in the `get` exact-match
branch.
- `tests/test_cache/test_semantic.py`: add
`test_same_query_different_context_does_not_collide` and
`test_exact_match_verifies_messages_hash`.

## Testing

- [x] New regression tests added (`tests/test_cache/test_semantic.py`)
- [x] Linting/formatting clean — run with the CI-pinned `ruff==0.15.17`
- [ ] Full `pytest` deferred to CI (local-OOM reason below).

```text
$ uvx ruff@0.15.17 check headroom/cache/semantic.py tests/test_cache/test_semantic.py
All checks passed!
```

## Real Behavior Proof

- Environment: Windows 11, Python 3.10, headroom from this branch.
Importing `headroom` pulls in the torch/transformers stack and a full
`pytest` gets OOM-killed on this box, so I verified the `put`/`get`
logic with a dependency-free script and left the full pytest to CI.
- Exact command / steps: stored responses A and B under the same query
`"run the tests"` with different `messages_hash`, then read each hash
back — through both the old (query-keyed) and new (hash-keyed) logic.
- Observed result: the old logic serves B's response to request A; the
new logic isolates them:

```text
OLD: A->RESPONSE_B  B->RESPONSE_B
NEW: A->RESPONSE_A  B->RESPONSE_B
SEMANTIC CACHE COLLISION FIX VERIFIED (OLD served B to A; NEW isolates)
```

- Not tested: the full SDK `HeadroomClient` round-trip with
`enable_semantic_cache=True` (needs the heavy stack). The fix is
confined to `SemanticCache.put`/`get` and the new tests drive them
directly. Full local `pytest` deferred to CI (OOM, per 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 commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes — ran
lint + a standalone logic check; full pytest deferred to CI (local OOM,
disclosed above)
- [x] I have updated the CHANGELOG.md if applicable

## Additional Notes

- Small, contained fix — the key derivation plus a verification guard,
no new dependencies.
- @JerrettDavis tagging you — this one can serve one conversation's
cached response to another when the last message matches, so it seemed
worth surfacing. Thanks!
2026-07-11 10:11:09 -05:00
JD Davis
f8431240b9
Extract tool schema savings policy (#1971)
## Description

Extracts the pure tool-schema savings attribution logic from `server.py`
into `headroom.proxy.tool_schema_savings_policy`. The server keeps the
`_tool_schema_saved_from_tags` compatibility alias used by the existing
stats payload path.

Closes #

## Type of Change

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

## Changes Made

- Added `tool_schema_savings_policy.py` with stable savings tag names
and pure summation behavior.
- Replaced the inline `server.py` helper body with a compatibility alias
to the extracted policy.
- Added direct tests for valid tag summing, invalid values, non-mapping
input, and stable tag names.
- Carried forward the LiteLLM callback compatibility shim needed for
current mypy on `main`.

## Testing

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

### Test Output

```text
python -m pytest tests\test_tool_schema_savings_policy.py
4 passed in 0.14s

python -m ruff check .
All checks passed!

python -m ruff format --check .
1095 files already formatted

python -m mypy headroom --ignore-missing-imports
Success: no issues found in 409 source files

gitleaks protect --staged --no-banner --redact
no leaks found
```

## Real Behavior Proof

- Environment: Windows, Python 3.13.13, branch
`jd/architecture-slice-24`.
- Exact command / steps: ran focused tool-schema savings policy tests,
ruff, ruff format check, mypy, and staged gitleaks scan.
- Observed result: pure policy behavior is directly covered and local
lint/type/security checks pass.
- Not tested: full proxy runtime; this slice only moves pure stats
attribution logic while preserving the server alias.

## Review Readiness

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

## Checklist

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

## Screenshots (if applicable)

N/A

## Additional Notes

Documentation and changelog updates are N/A for this internal
architecture-only refactor. The push reported existing default-branch
Dependabot alerts; no staged secret leaks were found for this PR.
2026-07-11 10:10:21 -05:00
Ben Younes
a617455f02
fix(proxy): preserve chatgpt responses streaming (#2012)
## Description

Closes #1956

Keep ChatGPT OAuth `/v1/responses` requests streaming when CCR retrieve
tools are present. The buffered `stream:false` conversion is still used
for regular OpenAI Responses CCR requests, but ChatGPT Codex routing now
bypasses that conversion so the upstream receives the streaming request
shape it expects.

## Type of Change

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

## Changes Made

- Extracted the OpenAI Responses CCR stream-buffering decision into a
small helper.
- Excluded ChatGPT OAuth/Codex-routed requests from the buffered
`stream:false` path.
- Added tests proving regular OpenAI CCR still buffers while ChatGPT
OAuth CCR remains streaming.

## Testing

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

### Test Output

```text
$ python3 -m pytest tests/test_proxy_openai_responses_stream_ccr.py -q
collected 3 items

tests/test_proxy_openai_responses_stream_ccr.py ...                      [100%]

============================== 3 passed in 0.59s ===============================

$ .venv/bin/ruff check headroom/proxy/handlers/openai.py tests/test_proxy_openai_responses_stream_ccr.py
All checks passed!

$ .venv/bin/ruff format --check headroom/proxy/handlers/openai.py tests/test_proxy_openai_responses_stream_ccr.py
2 files already formatted
```

## Test verification (RED -> GREEN)

RED, with the ChatGPT OAuth guard temporarily removed from the buffering
decision:

```text
tests/test_proxy_openai_responses_stream_ccr.py .F.                      [100%]
FAILED tests/test_proxy_openai_responses_stream_ccr.py::test_responses_ccr_keeps_chatgpt_oauth_requests_streaming
E   AssertionError: assert not True
E    +  where True = _should_buffer(tools=[{'type': 'function', 'name': 'headroom_retrieve'}], is_chatgpt_auth=True)
```

GREEN, with this patch applied:

```text
tests/test_proxy_openai_responses_stream_ccr.py ...                      [100%]
============================== 3 passed in 0.59s ===============================
```

## Real Behavior Proof

- Environment: Linux, Python 3.12.3, pytest 9.1.1, ruff 0.14.14.
- Exact command / steps: Removed the `not is_chatgpt_auth` guard from
the CCR buffering decision, ran the targeted tests, restored the guard,
and reran the tests plus targeted ruff checks.
- Observed result: The ChatGPT OAuth streaming regression test fails
without the guard and passes with the guard, while regular OpenAI CCR
buffering remains covered.
- Not tested: Full `uv run pytest`, full-project `uv run ruff check .`,
full-project `uv run ruff format --check .`, and `uv run mypy headroom`
were not run locally; `uv run --extra dev ruff` attempted to build the
Rust extension in this worktree, so targeted checks used the existing
`.venv/bin/ruff`.

## Review Readiness

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

## Checklist

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

## Screenshots (if applicable)

N/A

## Additional Notes

The existing buffered CCR path is preserved for non-ChatGPT OpenAI
Responses requests.
2026-07-11 00:10:02 -05:00
JD Davis
70b98b6485
Extract Python forwarder mode policy (#1987)
## Description

Extracts Python-forwarder mode resolution from `helpers.py` into
`headroom.proxy.python_forwarder_mode_policy`. The forwarding helpers
still read `HEADROOM_PROXY_PYTHON_FORWARDER_MODE` at request time, while
the allowed values/default/error contract is now pure and directly
tested.

Closes #

## Type of Change

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

## Changes Made

- Added `python_forwarder_mode_policy.py` with the allowed mode type,
env name/default, and resolver.
- Kept `helpers.get_python_forwarder_mode` as the request-time env
reader and compatibility entry point.
- Added direct policy tests for defaults, accepted values,
normalization, and invalid mode rejection.

## Testing

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

### Test Output

```text
python -m pytest tests\test_python_forwarder_mode_policy.py tests\test_proxy_byte_faithful_forwarding.py
41 passed in 4.00s

python -m ruff check .
All checks passed!

python -m ruff format --check .
1069 files already formatted

python -m mypy headroom --ignore-missing-imports
Success: no issues found in 410 source files

gitleaks protect --staged --no-banner --redact
no leaks found
```

## Real Behavior Proof

- Environment: Windows, Python 3.13.13, branch
`jd/architecture-slice-34`.
- Exact command / steps: ran new Python-forwarder mode policy tests,
existing byte-faithful forwarding tests, ruff, ruff format check, mypy,
and staged gitleaks scan.
- Observed result: forwarder mode behavior and byte-faithful forwarding
tests remain covered; local lint/type/security checks pass.
- Not tested: live proxy forwarding; existing helper entry point remains
intact.

## Review Readiness

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

## Checklist

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

## Screenshots (if applicable)

N/A

## Additional Notes

Documentation and changelog updates are N/A for this internal
architecture-only refactor. The push reported existing default-branch
Dependabot alerts; no staged secret leaks were found for this PR.
2026-07-11 00:06:37 -05:00