Commit graph

2630 commits

Author SHA1 Message Date
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
JD Davis
2c9eb7c5f1
feat(simulators): add provider simulator service (#2014)
## Description  
Adds a Rust-only `headroom-simulators` workspace crate: a deterministic
local upstream simulator service for Headroom proxy and pipeline
validation. It supplies configurable stubs plus bottled provider-shaped
responses for supported provider/path surfaces without calling real
LLMs.

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

## Changes Made  
- Added `crates/headroom-simulators` Rust crate with library and
`headroom-simulators` binary.
- Added clean domain classification for supported surfaces: Anthropic
`/v1/messages`, OpenAI chat/responses/conversations, Bedrock
invoke/stream routes, Vertex raw/stream predict, health, and generic
fallback.
- Added JSON-configured stub matching by method, path, body substring,
and JSON pointer.
- Added bottled provider-shaped JSON, SSE, and Bedrock EventStream
responses for unconfigured requests.
- Added a container `Dockerfile` and README for local/GitHub Actions
usage.
- Added unit and HTTP integration tests for defaults, configured stubs,
SSE, Vertex, and Bedrock EventStream behavior.
- Added proxy-level simulator-backed E2E tests that run Headroom against
the simulator across Anthropic, OpenAI Chat, OpenAI Responses, OpenAI
Conversations, Bedrock invoke/converse/streaming, Vertex raw/stream
predict, and upstream health.
- Added simulator-backed provider error-path E2E coverage for OpenAI
429, Anthropic 529, Bedrock 502, and Vertex 503 responses flowing
through Headroom unchanged.
- Added Headroom-owned preflight error E2E coverage proving Bedrock
missing credentials and invalid Vertex envelopes stop inside the proxy
instead of silently falling through to the simulator/provider.
- Fixed direct Rust `headroom-core` binaries/tests on Windows so Magika
initializes ONNX Runtime via `ort::init_from` from an explicit pip
`onnxruntime` library path, with fail-fast fallback only when no safe
runtime is discoverable.
- Added a Rust CI `simulator-e2e` matrix for `ubuntu-latest`,
`macos-latest`, and `windows-latest` that runs `cargo test -p
headroom-proxy --test e2e_simulators`.
- Gated dynamic Magika `Path`/`PathBuf` imports to Windows and x86_64
macOS so Linux clippy does not see unused dynamic-ORT-only imports.

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

### Test Output  
cargo fmt --all -- --check  
# passed  

cargo clippy --workspace -- -D warnings  
# passed  

$env:ORT_DYLIB_PATH=$null  
cargo test -p headroom-core transforms::magika_detector::tests:: --lib  
# 17 passed, 0 failed; Magika initialized from discovered pip
onnxruntime DLL

$env:ORT_DYLIB_PATH=$null  
cargo test --workspace  
# passed  

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

gitleaks git --log-opts="headroomlabs/main..HEAD" --no-banner --redact  
# 5 commits scanned; no leaks found

## Real Behavior Proof  
- **Environment:** Windows PowerShell, Rust toolchain `1.95.0`, clean
worktree from `headroomlabs/main` at `9bacf481`.
- **Exact simulator command / steps:**  
  - `cargo run -p headroom-simulators -- --listen 127.0.0.1:8789`  
- Point Headroom proxy upstream at `http://127.0.0.1:8789` for local
deterministic provider responses.
- Use optional `--config path/to/simulator.json` to bind exact request
fixtures.
- **Observed simulator result:**  
  - OpenAI chat default returns `chat.completion` shape.  
  - OpenAI Responses stream returns named SSE events.  
  - Vertex raw predict returns Anthropic message shape.  
- Bedrock stream can return binary `application/vnd.amazon.eventstream`
bytes.
  - Configured stubs override bottled defaults.  
- **Observed Magika result:**  
- Direct Rust `headroom-core` tests pass with `ORT_DYLIB_PATH` unset.
- Magika discovers the installed pip `onnxruntime.dll`, loads it via
`ort::init_from`, and only falls back if no safe runtime is available.
- **Not tested:**  
- No live provider calls; simulator behavior is intentionally offline
and deterministic.

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

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

## Screenshots (if applicable)  
N/A

## Additional Notes  
No CHANGELOG entry was added because this introduces a developer/CI
simulator crate plus a Windows direct-Rust Magika runtime fix, without
changing shipped Python package behavior. The simulator intentionally
does not include a lightweight fallback LLM in this slice; unbound
inputs receive deterministic bottled responses so tests stay
reproducible and offline.
2026-07-11 09:41:49 -07: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
Tejas Chopra
b0440f958d
fix(cache): partial cached-prefix replay + idle-aware net-cost; don't… (#1933)
… revert an overlaid prefix

overlay_cached_prefix (prefix_tracker):
- Replay the previously-forwarded (cached, compressed) prefix up to the
FIRST divergence instead of all-or-nothing. Previously a single changed
leading message — most commonly the just-added assistant turn, whose
client-resent form can differ trivially from the copy we reconstruct +
record — made the guard bail and forward the freshly-recompressed
prefix, busting the ENTIRE cache from message 0. Stopping at the
divergence keeps the (large) cache-hit region and only re-forwards from
the changed message on. This is the token-mode cache-safety fix:
measured REAL_BUST 50-65% -> ~6% on Opus SWE-bench, with token mode
landing resolve-neutral vs cache mode.
- Safe by construction: only replays prev_fwd[k] where
current_original[k] canonicalize-equals prev_orig[k] (positional 1:1
guaranteed by the count check), so no wrong bytes are ever forwarded.

idle plumbing (prefix_tracker + anthropic):
- Snapshot idle-since-last-response in get_or_create BEFORE it bumps the
access clock (otherwise seconds_since_activity reads ~0 every turn), and
forward it to the pipeline as idle_seconds so the dormant net-cost/TTL
P_alive gate (HEADROOM_NET_COST_POLICY=1) can actually see idle time.
Harmless when the policy is off; ~0 for back-to-back agent turns.

anthropic inflation guard:
- Skip the "optimization inflated tokens -> revert to originals" guard
when overlay just replayed a byte-identical cached prefix. Reverting
there would re-forward the raw uncompressed prefix and bust the live
prompt cache (trading a 90% read discount for a full re-write) — far
costlier than the small tail inflation the guard exists to avoid.

## Description

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

Closes #

## Type of Change

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

## Changes Made

- 

## Testing

<!-- Check what you actually ran, then paste the real command output
below. -->

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

### Test Output

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

## Real Behavior Proof

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

## Review Readiness

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

## Checklist

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

## Screenshots (if applicable)

Add screenshots to help explain your changes.

## Additional Notes

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

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


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

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

---

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

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-11 10:38:04 -05:00
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
dependabot[bot]
5229c98228
deps: bump prometheus from 0.13.4 to 0.14.0 (#1518)
Bumps [prometheus](https://github.com/tikv/rust-prometheus) from 0.13.4
to 0.14.0.
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/tikv/rust-prometheus/blob/master/CHANGELOG.md">prometheus's
changelog</a>.</em></p>
<blockquote>
<h2>0.14.0</h2>
<ul>
<li>
<p>API change: Use <code>AsRef&lt;str&gt;</code> for owned label values
(<a
href="https://redirect.github.com/tikv/rust-prometheus/issues/537">#537</a>)</p>
</li>
<li>
<p>Improvement: Hashing improvements (<a
href="https://redirect.github.com/tikv/rust-prometheus/issues/532">#532</a>)</p>
</li>
<li>
<p>Dependency upgrade: Update <code>hyper</code> to 1.6 (<a
href="https://redirect.github.com/tikv/rust-prometheus/issues/524">#524</a>)</p>
</li>
<li>
<p>Dependency upgrade: Update <code>procfs</code> to 0.17 (<a
href="https://redirect.github.com/tikv/rust-prometheus/issues/543">#543</a>)</p>
</li>
<li>
<p>Dependency upgrade: Update <code>protobuf</code> to 3.7.2 for
RUSTSEC-2024-0437 (<a
href="https://redirect.github.com/tikv/rust-prometheus/issues/541">#541</a>)</p>
</li>
<li>
<p>Dependency upgrade: Update <code>thiserror</code> to 2.0 (<a
href="https://redirect.github.com/tikv/rust-prometheus/issues/534">#534</a>)</p>
</li>
<li>
<p>Internal change: Fix LSP and Clippy warnings (<a
href="https://redirect.github.com/tikv/rust-prometheus/issues/540">#540</a>)</p>
</li>
<li>
<p>Internal change: Bump MSRV to 1.81 (<a
href="https://redirect.github.com/tikv/rust-prometheus/issues/539">#539</a>)</p>
</li>
<li>
<p>Documentation: Fix <code>register_histogram_vec_with_registry</code>
docstring (<a
href="https://redirect.github.com/tikv/rust-prometheus/issues/528">#528</a>)</p>
</li>
<li>
<p>Documentation: Fix typos in static-metric docstrings (<a
href="https://redirect.github.com/tikv/rust-prometheus/issues/479">#479</a>)</p>
</li>
<li>
<p>Documentation: Add missing <code>protobuf</code> feature to README
list (<a
href="https://redirect.github.com/tikv/rust-prometheus/issues/531">#531</a>)</p>
</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="e07efb4f37"><code>e07efb4</code></a>
prometheus: release 0.14.0 (<a
href="https://redirect.github.com/tikv/rust-prometheus/issues/545">#545</a>)</li>
<li><a
href="26e46ec03a"><code>26e46ec</code></a>
Hashing improvements (<a
href="https://redirect.github.com/tikv/rust-prometheus/issues/532">#532</a>)</li>
<li><a
href="e17c5ced2b"><code>e17c5ce</code></a>
build(deps): update procfs requirement from ^0.16 to ^0.17 (<a
href="https://redirect.github.com/tikv/rust-prometheus/issues/543">#543</a>)</li>
<li><a
href="e5809b7ab9"><code>e5809b7</code></a>
build(deps): update hyper requirement from ^0.14 to ^1.4 (<a
href="https://redirect.github.com/tikv/rust-prometheus/issues/524">#524</a>)</li>
<li><a
href="4a0e282888"><code>4a0e282</code></a>
Use AsRef&lt;str&gt; for owned label values (<a
href="https://redirect.github.com/tikv/rust-prometheus/issues/537">#537</a>)</li>
<li><a
href="c3865f3c40"><code>c3865f3</code></a>
cargo: upgrade to protobuf 3.7 (<a
href="https://redirect.github.com/tikv/rust-prometheus/issues/541">#541</a>)</li>
<li><a
href="7e4e6f2d33"><code>7e4e6f2</code></a>
docs: fix <code>register_histogram_vec_with_registry</code> docstring
(<a
href="https://redirect.github.com/tikv/rust-prometheus/issues/528">#528</a>)</li>
<li><a
href="5b62f4b78b"><code>5b62f4b</code></a>
Fix LSP and Clippy warnings and errors (<a
href="https://redirect.github.com/tikv/rust-prometheus/issues/540">#540</a>)</li>
<li><a
href="52d76fc2d8"><code>52d76fc</code></a>
cargo: bump MSRV to 1.81 (<a
href="https://redirect.github.com/tikv/rust-prometheus/issues/539">#539</a>)</li>
<li><a
href="3bd0e82f1f"><code>3bd0e82</code></a>
Upgrade <code>thiserror</code> crate from 1.0 to 2.0 version (<a
href="https://redirect.github.com/tikv/rust-prometheus/issues/534">#534</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/tikv/rust-prometheus/compare/v0.13.4...v0.14.0">compare
view</a></li>
</ul>
</details>
<br />


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

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

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

---

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

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-11 10:33:54 -05:00
dependabot[bot]
e448d7ba4d
deps: bump thiserror from 1.0.69 to 2.0.18 (#1519)
Bumps [thiserror](https://github.com/dtolnay/thiserror) from 1.0.69 to
2.0.18.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/dtolnay/thiserror/releases">thiserror's
releases</a>.</em></p>
<blockquote>
<h2>2.0.18</h2>
<ul>
<li>Make compatible with project-level <code>needless_lifetimes =
&quot;forbid&quot;</code> (<a
href="https://redirect.github.com/dtolnay/thiserror/issues/443">#443</a>,
thanks <a
href="https://github.com/LucaCappelletti94"><code>@​LucaCappelletti94</code></a>)</li>
</ul>
<h2>2.0.17</h2>
<ul>
<li>Use differently named __private module per patch release (<a
href="https://redirect.github.com/dtolnay/thiserror/issues/434">#434</a>)</li>
</ul>
<h2>2.0.16</h2>
<ul>
<li>Add to &quot;no-std&quot; crates.io category (<a
href="https://redirect.github.com/dtolnay/thiserror/issues/429">#429</a>)</li>
</ul>
<h2>2.0.15</h2>
<ul>
<li>Prevent <code>Error::provide</code> API becoming unavailable from a
future new compiler lint (<a
href="https://redirect.github.com/dtolnay/thiserror/issues/427">#427</a>)</li>
</ul>
<h2>2.0.14</h2>
<ul>
<li>Allow build-script cleanup failure with NFSv3 output directory to be
non-fatal (<a
href="https://redirect.github.com/dtolnay/thiserror/issues/426">#426</a>)</li>
</ul>
<h2>2.0.13</h2>
<ul>
<li>Documentation improvements</li>
</ul>
<h2>2.0.12</h2>
<ul>
<li>Prevent elidable_lifetime_names pedantic clippy lint in generated
impl (<a
href="https://redirect.github.com/dtolnay/thiserror/issues/413">#413</a>)</li>
</ul>
<h2>2.0.11</h2>
<ul>
<li>Add feature gate to tests that use std (<a
href="https://redirect.github.com/dtolnay/thiserror/issues/409">#409</a>,
<a
href="https://redirect.github.com/dtolnay/thiserror/issues/410">#410</a>,
thanks <a
href="https://github.com/Maytha8"><code>@​Maytha8</code></a>)</li>
</ul>
<h2>2.0.10</h2>
<ul>
<li>Support errors containing a generic type parameter's associated type
in a field (<a
href="https://redirect.github.com/dtolnay/thiserror/issues/408">#408</a>)</li>
</ul>
<h2>2.0.9</h2>
<ul>
<li>Work around <code>missing_inline_in_public_items</code> clippy
restriction being triggered in macro-generated code (<a
href="https://redirect.github.com/dtolnay/thiserror/issues/404">#404</a>)</li>
</ul>
<h2>2.0.8</h2>
<ul>
<li>Improve support for macro-generated <code>derive(Error)</code> call
sites (<a
href="https://redirect.github.com/dtolnay/thiserror/issues/399">#399</a>)</li>
</ul>
<h2>2.0.7</h2>
<ul>
<li>Work around conflict with #[deny(clippy::allow_attributes)] (<a
href="https://redirect.github.com/dtolnay/thiserror/issues/397">#397</a>,
thanks <a
href="https://github.com/zertosh"><code>@​zertosh</code></a>)</li>
</ul>
<h2>2.0.6</h2>
<ul>
<li>Suppress deprecation warning on generated From impls (<a
href="https://redirect.github.com/dtolnay/thiserror/issues/396">#396</a>)</li>
</ul>
<h2>2.0.5</h2>
<ul>
<li>Prevent deprecation warning on generated impl for deprecated type
(<a
href="https://redirect.github.com/dtolnay/thiserror/issues/394">#394</a>)</li>
</ul>
<h2>2.0.4</h2>
<ul>
<li>Eliminate needless_lifetimes clippy lint in generated
<code>From</code> impls (<a
href="https://redirect.github.com/dtolnay/thiserror/issues/391">#391</a>,
thanks <a
href="https://github.com/matt-phylum"><code>@​matt-phylum</code></a>)</li>
</ul>
<h2>2.0.3</h2>
<ul>
<li>Support the same Path field being repeated in both Debug and Display
representation in error message (<a
href="https://redirect.github.com/dtolnay/thiserror/issues/383">#383</a>)</li>
<li>Improve error message when a format trait used in error message is
not implemented by some field (<a
href="https://redirect.github.com/dtolnay/thiserror/issues/384">#384</a>)</li>
</ul>
<h2>2.0.2</h2>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="dc0f6a23a3"><code>dc0f6a2</code></a>
Release 2.0.18</li>
<li><a
href="0275292735"><code>0275292</code></a>
Touch up PR 443</li>
<li><a
href="3c33bc60ad"><code>3c33bc6</code></a>
Merge pull request <a
href="https://redirect.github.com/dtolnay/thiserror/issues/443">#443</a>
from LucaCappelletti94/master</li>
<li><a
href="995939cc2e"><code>995939c</code></a>
Reproduce issue 442</li>
<li><a
href="21653d1d33"><code>21653d1</code></a>
Made clippy lifetime allows conditional</li>
<li><a
href="45e5388009"><code>45e5388</code></a>
Update actions/upload-artifact@v5 -&gt; v6</li>
<li><a
href="386aac126a"><code>386aac1</code></a>
Update actions/upload-artifact@v4 -&gt; v5</li>
<li><a
href="ec50561375"><code>ec50561</code></a>
Update actions/checkout@v5 -&gt; v6</li>
<li><a
href="247eab5d79"><code>247eab5</code></a>
Update name of empty_enum clippy lint</li>
<li><a
href="91b181f089"><code>91b181f</code></a>
Raise required compiler to Rust 1.68</li>
<li>Additional commits viewable in <a
href="https://github.com/dtolnay/thiserror/compare/1.0.69...2.0.18">compare
view</a></li>
</ul>
</details>
<br />


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

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

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

---

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

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-11 10:33:38 -05:00
dependabot[bot]
98f7f1c2a3
deps: bump tower-http from 0.6.11 to 0.7.0 (#1520)
Bumps [tower-http](https://github.com/tower-rs/tower-http) from 0.6.11
to 0.7.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/tower-rs/tower-http/releases">tower-http's
releases</a>.</em></p>
<blockquote>
<h2>tower-http-0.7.0</h2>
<p><a
href="https://github.com/tower-rs/tower-http/compare/tower-http-0.6.11...tower-http-0.7.0">Changes
since 0.6.11</a></p>
<h2>Added</h2>
<ul>
<li>
<p><code>csrf</code>: add cross-site request forgery (CSRF) protection
middleware, porting the cross-origin protection scheme introduced in Go
1.25 (<a
href="https://redirect.github.com/tower-rs/tower-http/issues/699">#699</a>)</p>
<pre lang="rust"><code>use tower::ServiceBuilder;
use tower_http::csrf::CsrfLayer;
<p>// Rejects cross-origin state-changing requests using
<code>Sec-Fetch-Site</code>,<br />
// an <code>Origin</code> allow-list, and an
<code>Origin</code>/<code>Host</code> fallback. No per-request<br />
// token state required.<br />
let layer = CsrfLayer::new().add_trusted_origin(&quot;<a
href="https://example.com">https://example.com</a>&quot;)?;</p>
<p>let service =
ServiceBuilder::new().layer(layer).service_fn(handler);<br />
</code></pre></p>
</li>
<li>
<p><code>timeout</code>: add <code>DeadlineBody</code> for non-resetting
body timeouts, applied via the new <code>RequestBodyDeadlineLayer</code>
and <code>ResponseBodyDeadlineLayer</code> (<a
href="https://redirect.github.com/tower-rs/tower-http/issues/688">#688</a>)</p>
<p>Unlike <code>TimeoutBody</code>, which resets its deadline on every
frame, <code>DeadlineBody</code> caps the total time of a body transfer.
A slow client trickling one byte at a time never trips an idle timeout
but will trip a deadline.</p>
<pre lang="rust"><code>use std::time::Duration;
use tower::ServiceBuilder;
use tower_http::timeout::RequestBodyDeadlineLayer;
<p>// Abort the request body transfer after 30s total, regardless of
how<br />
// frequently data arrives.<br />
let service = ServiceBuilder::new()<br />
.layer(RequestBodyDeadlineLayer::new(Duration::from_secs(30)))<br />
.service_fn(handler);<br />
</code></pre></p>
</li>
<li>
<p><code>fs</code>: add strong <code>ETag</code> support to
<code>ServeDir</code>, including <code>If-Match</code> and
<code>If-None-Match</code> precondition handling per RFC 9110. <code>304
Not Modified</code> responses now carry the <code>ETag</code> and
<code>Last-Modified</code> validators (<a
href="https://redirect.github.com/tower-rs/tower-http/issues/691">#691</a>)</p>
</li>
<li>
<p><code>fs</code>: add a <code>Backend</code> trait to make
<code>ServeDir</code> work with non-filesystem sources (e.g. embedded
assets or object storage). The default <code>TokioBackend</code>
preserves existing behavior. Use <code>ServeDir::with_backend()</code>
to plug in custom implementations (<a
href="https://redirect.github.com/tower-rs/tower-http/issues/684">#684</a>)</p>
<pre lang="rust"><code>use tower_http::services::fs::ServeDir;
<p>// <code>MyBackend</code> implements
<code>tower_http::services::fs::Backend</code>.<br />
// The default <code>ServeDir::new()</code> continues to use
<code>TokioBackend</code> (local FS).<br />
let service = ServeDir::with_backend(&quot;assets&quot;,
MyBackend::new());<br />
</code></pre></p>
</li>
<li>
<p><code>fs</code>: add <code>html_as_default_extension</code> option to
<code>ServeDir</code>, appending <code>.html</code> when the request
path has no extension (<a
href="https://redirect.github.com/tower-rs/tower-http/issues/519">#519</a>)</p>
</li>
<li>
<p><code>fs</code>: add <code>redirect_path_prefix</code> option to
<code>ServeDir</code>, prepending a prefix on trailing-slash redirects
so the service can be mounted under a sub-path (<a
href="https://redirect.github.com/tower-rs/tower-http/issues/486">#486</a>)</p>
</li>
<li>
<p><code>validate-request</code>: add
<code>ValidateRequestHeaderLayer::has_header_value()</code> to reject
requests when a header does not have an expected value (<a
href="https://redirect.github.com/tower-rs/tower-http/issues/360">#360</a>)</p>
</li>
<li>
<p><code>body</code>: <code>UnsyncBoxBody::new()</code> constructor and
<code>From&lt;ServeFileSystemResponseBody&gt;</code> conversion to avoid
double-boxing when combining <code>ServeDir</code> responses with other
body types (<a
href="https://redirect.github.com/tower-rs/tower-http/issues/537">#537</a>)</p>
</li>
<li>
<p><code>limit</code>: implement <code>Default</code> for
<code>limit::ResponseBody</code> when the wrapped body also implements
<code>Default</code> (<a
href="https://redirect.github.com/tower-rs/tower-http/issues/679">#679</a>)</p>
</li>
</ul>
<h2>Changed</h2>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="b194fcfef3"><code>b194fcf</code></a>
v0.7.0</li>
<li><a
href="af828a6ec9"><code>af828a6</code></a>
feat(follow_redirect)!: preserve request extensions across redirects (<a
href="https://redirect.github.com/tower-rs/tower-http/issues/706">#706</a>)</li>
<li><a
href="8cb8d99a84"><code>8cb8d99</code></a>
feat(ValidateRequestHeaderLayer): add
has_header(&quot;...&quot;).with_value(&quot;...&quot;) fun...</li>
<li><a
href="3b56d2d2e8"><code>3b56d2d</code></a>
feat!: Add configurable Backend trait for ServeDir, bump MSRV 1.65 (<a
href="https://redirect.github.com/tower-rs/tower-http/issues/684">#684</a>)</li>
<li><a
href="8508716431"><code>8508716</code></a>
Add <code>redirect_path_prefix</code> option (<a
href="https://redirect.github.com/tower-rs/tower-http/issues/486">#486</a>)</li>
<li><a
href="56327b27f4"><code>56327b2</code></a>
Add Windows drive-prefix path regression test (<a
href="https://redirect.github.com/tower-rs/tower-http/issues/705">#705</a>)</li>
<li><a
href="54c6db8590"><code>54c6db8</code></a>
feat(compression)!: upgrade SizeAbove threshold from u16 to u64 (<a
href="https://redirect.github.com/tower-rs/tower-http/issues/704">#704</a>)</li>
<li><a
href="68cd6d8f3c"><code>68cd6d8</code></a>
Add DeadlineBody for non-resetting body timeouts (<a
href="https://redirect.github.com/tower-rs/tower-http/issues/688">#688</a>)</li>
<li><a
href="fa8a98cb3e"><code>fa8a98c</code></a>
feat(fs): add strong ETag support to ServeDir (<a
href="https://redirect.github.com/tower-rs/tower-http/issues/691">#691</a>)</li>
<li><a
href="36d2205eb6"><code>36d2205</code></a>
fix: Make SetMultiple*Header Clone for !Clone http bodies (<a
href="https://redirect.github.com/tower-rs/tower-http/issues/703">#703</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/tower-rs/tower-http/compare/tower-http-0.6.11...tower-http-0.7.0">compare
view</a></li>
</ul>
</details>
<br />


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

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

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

---

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

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-11 10:33:28 -05:00
dependabot[bot]
6c705b4066
deps: bump toml from 0.8.23 to 1.1.2+spec-1.1.0 (#1517)
Bumps [toml](https://github.com/toml-rs/toml) from 0.8.23 to
1.1.2+spec-1.1.0.
<details>
<summary>Commits</summary>
<ul>
<li><a
href="a3d0047c95"><code>a3d0047</code></a>
chore: Release</li>
<li><a
href="cc37615fc8"><code>cc37615</code></a>
docs: Update changelog</li>
<li><a
href="7f5e9e130a"><code>7f5e9e1</code></a>
fix(parser): Consolidate invalid unquoted key into one error (<a
href="https://redirect.github.com/toml-rs/toml/issues/1138">#1138</a>)</li>
<li><a
href="52feb9070c"><code>52feb90</code></a>
fix(parser): Consolidate invalid unquoted key into one error</li>
<li><a
href="aad85d4921"><code>aad85d4</code></a>
chore(deps): Update j178/prek-action action to v2 (<a
href="https://redirect.github.com/toml-rs/toml/issues/1136">#1136</a>)</li>
<li><a
href="8b1ac44bca"><code>8b1ac44</code></a>
chore(deps): Update compatible (dev) (<a
href="https://redirect.github.com/toml-rs/toml/issues/1135">#1135</a>)</li>
<li><a
href="9effd79ff2"><code>9effd79</code></a>
chore(deps): Update j178/prek-action action to v2</li>
<li><a
href="9db8aad6ea"><code>9db8aad</code></a>
chore: Release</li>
<li><a
href="e55a6633d9"><code>e55a663</code></a>
docs: Update changelog</li>
<li><a
href="c11d7d7ad3"><code>c11d7d7</code></a>
Optimisations (<a
href="https://redirect.github.com/toml-rs/toml/issues/1133">#1133</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/toml-rs/toml/compare/toml-v0.8.23...toml-v1.1.2">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=toml&package-manager=cargo&previous-version=0.8.23&new-version=1.1.2+spec-1.1.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

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

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

---

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

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-11 10:32:53 -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
GUOHAO LIU
12aa2cbf6c
fix(kompress): surface model-not-ready state via logs and health endpoint (#2034)
## Description

Kompress model download failures (HuggingFace unreachable, corporate
firewall, SSL errors) previously caused **silent 0% compression** — the
model isn't loaded, `is_ready()` returns `False`, and the proxy passes
through with no warning, no health indicator, nothing. Operators cannot
detect degraded operation without manually comparing
`x-headroom-tokens-before` vs `x-headroom-tokens-after` headers.

Closes #2029

## Type of Change

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

## Changes Made

Three surfaces where silent failure is now visible:

- **Request-time (hot path)**: `ContentRouter.compress_text()` now logs
a WARNING once per router instance when `is_ready()` is False and the
model is not cached. Rate-limited to one log per session to avoid spam.

- **Startup (eager preload)**: `ContentRouter.eager_load_compressors()`
now logs WARNING (was INFO) when `KompressModelNotCached` is raised,
with actionable guidance ("Check HuggingFace connectivity or
pre-download with headroom-ai[ml]").

- **Health endpoint**: `/health` response now includes a `kompress`
component with the standard `enabled`/`ready`/`status`/`backend` fields.
Kompress is treated as **optional** in the aggregate readiness check — a
cold model cache does NOT degrade the overall proxy health status
(matching the semantics of `cache` and `rate_limiter` which report
`ready=True` when disabled).

## Testing

- [x] **Existing tests**: 13/13 pass in
`test_kompress_preload_deferral.py` + `test_proxy_disable_kompress.py` +
`test_proxy_ccr.py` + `test_proxy_debug_endpoints.py`
- [x] **Adversarial**: 5 endpoint-level tests verify `/health`,
`/healthz`, `/livez`, `/readyz` all return 200 even when kompress model
is not cached
- [x] **PBT (Hypothesis)**: 3 properties × 136 random combinations
confirm aggregate readiness ignores kompress, `_component_health()`
invariants hold, and kompress is never the sole cause of `unhealthy`
- [x] **Lint**: `ruff check` and `ruff format --check` pass on all
changed files

```text
$ uv run pytest tests/test_kompress_preload_deferral.py tests/test_proxy_disable_kompress.py \
    tests/test_proxy_ccr.py::TestCCRIntegration::test_health_endpoint \
    tests/test_proxy_debug_endpoints.py::test_existing_health_routes_unchanged -q
.............                              [100%]
13 passed in 2.90s

$ uv run pytest /tmp/adversarial_kompress_health.py -q
.....                                      [100%]
5 passed in 9.60s
# Includes: /healthz, /livez, /readyz all 200; disable_kompress=True → status=disabled

$ uv run pytest /tmp/pbt_kompress_health.py -q
...                                        [100%]
3 passed in 0.56s
# Hypothesis: 128 aggregate combos + 4 invariant combos + 4 isolation combos
```

## Real Behavior Proof

- Environment: Linux, Python 3.12, headroom main @ a617455f
- Exact command / steps: `uv run pytest
tests/test_kompress_preload_deferral.py
tests/test_proxy_disable_kompress.py -q` → 13 passed; adversarial 5/5;
PBT 3/3 (136 combos); `uv run ruff format --check
headroom/transforms/content_router.py headroom/proxy/server.py` → 2
files already formatted
- Observed result: Startup warning fires when model not cached.
Request-time warning fires once when `is_ready()` returns False.
`/health` returns `kompress` with `enabled`, `ready`, and `backend`
fields. Overall proxy health remains `healthy` regardless of kompress
cold-cache state.
- Not tested: Full E2E with HuggingFace blocked (network simulation).
Manual testing recommended for operators behind corporate firewalls.

## 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:12:08 -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
JD Davis
0f846e5a8f
refactor(proxy): extract tool injection config (#2010)
## Description

Extracts memory tool-injection operator config parsing from
`headroom.proxy.helpers` into a focused config policy module. Existing
helper functions and imports remain available while the environment
parsing is now 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 `headroom.proxy.tool_injection_config` for
`HEADROOM_TOOL_INJECTION_STICKY` and
`HEADROOM_TOOL_TRACKER_MAX_SESSIONS` parsing.
- Updated `helpers.get_tool_injection_sticky_mode` and
`helpers.get_tool_tracker_max_sessions` to delegate to the config module
while preserving existing import paths.
- Added direct tests for defaults, valid values, invalid values, and
helper 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_injection_config.py tests/test_memory_tool_session_sticky.py tests/test_issue_728_empty_tools_injection.py
46 passed in 0.53s

python -m ruff check .
All checks passed!

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

python -m mypy headroom --ignore-missing-imports
Success: no issues found in 415 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 `cb38f793`.
- Exact command / steps: Ran targeted tool-injection config, memory
session sticky, 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.
2026-07-11 00:03:15 -05:00
Ztkent
b3a559ba56
fix(savings): cap ledger retention at 30 days (#1985)
## Description
The durable savings ledger (`headroom savings`) retained up to 365 days
of history with an unbounded-sounding "All time" window. Long-lived
installs accumulate an ever-growing `~/.headroom/savings_events.jsonl`,
and `--days` had no upper bound so a caller could request an arbitrarily
large lookback. This caps retention at 30 days everywhere it's read,
shrinks the compaction threshold to match, and renames the "All time"
window to reflect what it actually is now: `Last 30 days`.

## Type of Change
- [x] 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 change)
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)

## Changes Made
- `headroom/savings_ledger.py`: `DEFAULT_RETENTION_DAYS` 365 → 30; add
`MAX_RETENTION_DAYS = 30` and hard-clamp the lookback inside
`aggregate_savings` so no caller (CLI or programmatic) can read back
further than 30 days, regardless of the `retention_days` argument passed
in.
- `headroom/savings_ledger.py`: report window `all_time` →
`last_30_days` (the bucket is exactly 30-day-bounded now, so it doubles
as the lifetime view too). `_COMPACT_SIZE_BYTES` 8 MiB → 1 MiB, since a
30-day-bounded ledger should never need to grow large.
- `headroom/cli/savings.py`: `--days` is now `click.IntRange(min=1,
max=30)` (was unbounded); help text states the max. Window label `"All
time"` → `"Last 30 days"`, and the label column width bumped 11 → 12 so
the longer label stays aligned with the other rows' progress bars.
- `tests/test_savings_ledger.py`: updated window-label assertions; added
a hard-cap regression test (`retention_days=365` passed explicitly still
excludes a 60-day-old event) and a `--days` range-rejection test
(31/60/365 all rejected).

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

### Test Output

```text
$ ruff check headroom/savings_ledger.py headroom/cli/savings.py tests/test_savings_ledger.py
All checks passed!

$ ruff format --check headroom/savings_ledger.py headroom/cli/savings.py tests/test_savings_ledger.py
3 files already formatted

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

$ pytest tests/test_savings_ledger.py -q
............ss....                                                     [100%]
16 passed, 2 skipped in 6.11s
```

(ruff `0.15.17`, mypy `1.20.2` — pinned to match
`.github/workflows/ci.yml`'s `lint` job. Full multi-shard suite left to
CI; ran the full touched-module suite locally.)

## Real Behavior Proof
- Environment: macOS (Darwin 25.5.0), Python 3.13.14, local `uv` venv;
branch built and installed via `uv tool install --force`.
- Exact command / steps: ran `headroom savings` against a ledger holding
multiple models' events (claude-opus-4-8, claude-sonnet-5,
claude-haiku-4-5) recorded across the retention window, then ran
`headroom savings --days 60` to exercise the new upper bound.
- Observed result: all three windows (Today / Last 7 days / Last 30
days) populate and are each bounded to at most 30 days; cost-avoided
breaks down per model; `--days 60` is rejected by the new `1..30` range
instead of silently accepted.
- Not tested: Windows/macOS native-wrapper e2e jobs — left to CI.

```text
$ headroom savings

Today        █████░░░░░░░░░░░  33.8%  saved 8,702,348 / 25,781,326 tokens  $25.5830
Last 7 days  ██████░░░░░░░░░░  36.3%  saved 11,289,737 / 31,072,254 tokens  $34.8287
Last 30 days ██████░░░░░░░░░░  38.2%  saved 14,449,516 / 37,821,634 tokens  $48.5385

Cost avoided per model:
  claude-opus-4-8          $33.0494
  claude-sonnet-5          $15.2989
  claude-haiku-4-5-20251001 $0.1902

$ headroom savings --days 60
Usage: headroom savings [OPTIONS]
Try 'headroom savings --help' for help.

Error: Invalid value for '--days': 60 is not in the range 1<=x<=30.
```

- Not tested: Windows/macOS native-wrapper e2e jobs — left to CI.

## 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 commented my code, particularly in hard-to-understand areas
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] I have updated the CHANGELOG.md if applicable

## Screenshots (if applicable)
N/A — CLI text output only, see Real Behavior Proof above.
2026-07-11 00:02:23 -05:00
JD Davis
82af5cdfe2
refactor(proxy): isolate proxy mode policy (#1965)
## Description

Extracts proxy mode normalization into a pure `proxy_mode_policy`
module. `modes.py` keeps the existing public API and logging, while
alias/default/unknown-mode decisions are now represented by a
deterministic value object with 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.proxy_mode_policy` with canonical mode
constants, alias mapping, `ProxyModeDecision`, and pure normalization
helpers.
- Updated `headroom.proxy.modes` to delegate normalization decisions
while preserving existing constants, predicates, fallback behavior, and
logging.
- Added direct policy tests for canonical modes, legacy aliases, blank
values, unknown values, and value-only normalization.
- 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_proxy_mode_policy.py tests/test_proxy_modes.py tests/test_litellm_callback.py -q
17 passed in 7.84s

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: proxy mode policy/modes/callback tests pass; static
checks pass; no staged secrets detected.
- Not tested: live proxy run; this slice preserves existing public mode
helpers and only moves pure normalization 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 00:00:03 -05:00
Abhay Singh
27ddde1f5e
fix(transforms/code): coerce language aliases instead of raising (#1975)
## Description

`CodeAwareCompressor.compress()` picks the language for AST-based
compression like this
(`headroom/transforms/code_compressor.py`):

```python
if language:
    detected_lang = CodeLanguage(language.lower())   # <-- raises on anything not an exact enum value
    confidence = 1.0
elif self.config.language_hint:
    detected_lang = CodeLanguage(self.config.language_hint.lower())
    confidence = 1.0
else:
    detected_lang, confidence = detect_language(code)
```

`CodeLanguage` only accepts
`python`/`javascript`/`typescript`/`go`/`rust`/`java`/`c`/`cpp`/`perl`.
The very common markdown fence tags and hints — `js`, `ts`, `py`, `jsx`,
`tsx`, `node`, `rs`,
`c++` — are **not** enum values, so `CodeLanguage("js")` raises
`ValueError`. That construction
is *above* the method's own `try/except`, so:

- **Direct callers** — `CodeAwareCompressor().compress(code,
language="js")` and the module-level
`compress_code(code, language="js")` — crash with an uncaught
`ValueError`.
- **In the router (mixed content):** `split_into_sections` extracts the
raw fence tag
(`_CODE_FENCE_PATTERN` captures `\w*`, e.g. `js`) into
`ContentSection.language`, and that string
is passed straight into `compress(...)`. The `ValueError` is swallowed
by the outer `try/except`
in the strategy dispatch, so a ` ```js ` / ` ```ts ` / ` ```py ` block
silently **skips
code-aware compression** even when `enable_code_aware=True`, falling
back to the generic path.

So the three most common web/scripting languages, written with their
usual fence tags, never get
the structure-aware compressor.

Closes: no issue filed — found while auditing the code-compression
language path.

## Fix

Add a `coerce_language()` helper that maps common aliases/fence tags to
the canonical
`CodeLanguage` and returns `CodeLanguage.UNKNOWN` (never raises) for
anything unrecognized.
`compress()` now coerces the hint and, when the result is `UNKNOWN`,
falls back to
content-based `detect_language(code)` instead of constructing the enum
directly:

```python
if language:
    detected_lang = coerce_language(language)
    if detected_lang == CodeLanguage.UNKNOWN:
        detected_lang, confidence = detect_language(code)
    else:
        confidence = 1.0
```

## Type of Change

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

## Changes Made

- `headroom/transforms/code_compressor.py`: add `_LANGUAGE_ALIASES` and
`coerce_language()`; use them in `compress()` for both the `language`
argument and `config.language_hint`, with a content-detection fallback
on `UNKNOWN`.
- `tests/test_code_compressor_language_alias.py`: cover alias mapping,
canonical passthrough, case/whitespace handling, unknown-returns-UNKNOWN
(no `ValueError`), and that `compress(language="js")` no longer raises.

## Testing

- [x] New regression tests added
(`tests/test_code_compressor_language_alias.py`)
- [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/transforms/code_compressor.py tests/test_code_compressor_language_alias.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 coercion logic
with a dependency-free script (replicating the enum + helper) and left
the full pytest to CI.
- Exact command / steps: ran the common aliases and the canonical values
through both the old `CodeLanguage(value.lower())` construction and the
new `coerce_language()`.
- Observed result: the old construction raises `ValueError` on every
alias (the crash / silent-skip); the new helper maps them and never
raises:

```text
OK alias 'js': old raised ValueError -> new maps to javascript
OK alias 'ts': old raised ValueError -> new maps to typescript
OK alias 'py': old raised ValueError -> new maps to python
OK alias 'jsx': old raised ValueError -> new maps to javascript
OK alias 'node': old raised ValueError -> new maps to javascript
OK canonical values pass through
OK case-insensitive + trimmed
OK unknown -> UNKNOWN (no ValueError)
LANGUAGE COERCION VERIFIED
```

- Not tested: running a full mixed-content document with ` ```js `
fences through a booted compression pipeline (needs the heavy stack).
The unit tests exercise the coercion directly and the
`compress(language="js")` entry point. 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 small lookup table plus a helper and a
call-site change.
- @JerrettDavis tagging you — this one silently disables code-aware
compression for the most common fence tags (`js`/`ts`/`py`), so it may
be worth a look when you have a moment.
2026-07-10 23:57:33 -05:00
JD Davis
69fd2189a3
Extract request limit policy (#1982)
## Description

Extracts request/stream limit validation from `helpers.py` into
`headroom.proxy.request_limit_policy`. The helpers still read
environment variables at request time, but validation of SSE event size
and body-too-large status values 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 `request_limit_policy.py` for resolving SSE event max bytes and
body-too-large HTTP status values.
- Kept `helpers.get_sse_event_max_bytes` and
`helpers.get_body_too_large_status` reading env vars and delegating to
the pure policy.
- Added direct tests for defaults, valid override values, and invalid
values.
- 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_request_limit_policy.py
10 passed in 0.17s

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-31`.
- Exact command / steps: ran focused request-limit policy tests, ruff,
ruff format check, mypy, and staged gitleaks scan.
- Observed result: limit validation behavior is directly covered and
local lint/type/security checks pass.
- Not tested: live proxy request rejection; existing helper entry points
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-10 23:55:11 -05:00
JD Davis
094a53c047
refactor(proxy): isolate output effort policy (#1961)
## Description

Extracts provider-neutral output effort decisions into a pure
`output_effort_policy` module. `output_shaper` still owns request
mutation and labels, while the rank comparisons, legacy thinking clamp,
and OpenAI text verbosity eligibility now live behind small
deterministic 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_effort_policy` for effort lowering,
legacy thinking budget clamping, and OpenAI text verbosity decisions.
- Updated `output_shaper` to delegate those pure decisions while
preserving existing labels and request mutation behavior.
- Added focused policy tests for effort rank transitions, thinking clamp
boundaries, and verbosity creation/lowering.
- 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_effort_policy.py tests/test_output_shaper.py tests/test_litellm_callback.py -q
56 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: output effort 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 policy decisions.

## 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-10 23:53:00 -05:00
JD Davis
b1e871d51c
refactor(proxy): isolate memory rank policy (#1960)
## Description

Extracts the proxy memory ranking formulas into a pure
`memory_rank_policy` module and keeps `MemoryCandidate` /
`RecencyBoostRanker` as the public adapter-facing API. Also preserves
backend memory IDs when ranked candidates are rebuilt, so downstream
memory update/delete handles survive the ranking 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.memory_rank_policy` for timestamp parsing,
recency factor calculation, and score boosting.
- Updated `RecencyBoostRanker` to delegate policy math while preserving
the existing public API.
- Preserved `MemoryCandidate.id` when rank output candidates are
rebuilt.
- Added focused policy tests plus an ID-preservation regression test.
- 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_memory_rank_policy.py tests/test_memory_ranker.py tests/test_litellm_callback.py -q
32 passed in 6.22s

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: targeted pytest, ruff, ruff format check,
repo-wide mypy, staged gitleaks scan.
- Observed result: memory rank policy/ranker/callback tests pass; static
checks pass; no staged secrets detected.
- Not tested: full provider/API integration; this slice only changes
pure policy delegation and candidate shape preservation.

## 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-10 23:51:51 -05:00
JD Davis
1c1e360112
refactor(proxy): isolate project attribution policy (#1957)
## Description

Extracts pure project attribution policy from the runtime project
context holder. Header classification, project path splitting, and
project-prefixed base URL construction now live in a policy module while
`project_context` keeps the ContextVar and ASGI scope adapter
responsibilities.

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.project_policy` for pure project attribution
header/path/base-URL helpers.
- Updated `headroom.proxy.project_context` to re-export the pure helpers
and retain only request context binding and ASGI scope mutation.
- Added direct tests for the extracted project attribution 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_project_policy.py tests/test_proxy_project_savings.py tests/test_litellm_callback.py tests/test_compress_api.py::TestLiteLLMCallback -q
29 passed in 13.70s

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 project policy tests, project
savings 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-10 23:50:27 -05:00
JD Davis
740fb9bc16
refactor(cache): isolate semantic key policy (#1953)
## Description

Extracts proxy semantic-cache key normalization and hashing into a pure
policy module while preserving `SemanticCache._compute_key` for existing
callers and tests. This separates deterministic cache-key construction
from the async cache adapter and LRU storage concerns.

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` for pure cache-control
stripping and semantic cache key construction.
- Updated `SemanticCache._compute_key` to delegate to the extracted
policy while preserving the local `_strip_cache_control` compatibility
alias.
- Added direct tests for the extracted semantic-cache key policy.
- 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_proxy_semantic_cache_key_policy.py tests/test_proxy_semantic_cache_key.py tests/test_proxy_semantic_cache_key_integration.py tests/test_proxy_openai_cache_key_integration.py tests/test_litellm_callback.py tests/test_compress_api.py::TestLiteLLMCallback -q
46 passed in 11.83s

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 semantic cache key tests, handler
cache-key integration 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-10 23:49:27 -05:00
JD Davis
ea1951508b
refactor(proxy): isolate rate limit policy (#1954)
## Description

Extracts token-bucket refill, consume, wait-time, and stale-bucket
selection formulas into a pure rate-limit policy module while preserving
the async `TokenBucketRateLimiter` adapter for locks and mutable bucket
storage.

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.rate_limit_policy` for pure token-bucket
calculations.
- Updated `TokenBucketRateLimiter` to delegate refill, consume, and
stale-key selection to the extracted policy.
- Added direct tests for the rate-limit 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_rate_limit_policy.py tests/test_proxy_healthchecks.py tests/test_litellm_callback.py tests/test_compress_api.py::TestLiteLLMCallback -q
27 passed in 17.82s

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 rate-limit policy tests, proxy
health checks, 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-10 23:47:33 -05:00
JD Davis
c20f3b1c04
refactor(memory): isolate injection decision policy (#1952)
## Description

Extracts the memory injection decision precedence and skip-reason tag
stamping into a pure policy module while preserving the public
`MemoryDecision.decide` API used by handlers. This keeps the frozen
decision value type separate from the gate policy 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.memory_decision_policy` for pure
memory-injection precedence and tag stamping helpers.
- Updated `MemoryDecision.decide` and `MemoryDecision.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_memory_decision_policy.py tests/test_memory_decision.py tests/test_memory_invariants.py tests/test_litellm_callback.py tests/test_compress_api.py::TestLiteLLMCallback -q
37 passed in 6.74s

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 memory decision tests, memory
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-10 23:45:44 -05:00
JD Davis
235c986c9c
refactor(memory): isolate query construction policy (#1950)
## Description

Extracts memory retrieval query construction policy into a pure helper
module while preserving `MemoryQuery` as the public frozen value type.
The dataclass now delegates source extraction and embedding-input
rendering to policy helpers, keeping query construction separate from
the value wrapper.

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_query_policy` for pure retrieval query
source extraction and rendering.
- Updated `MemoryQuery.to_embedding_input` and
`MemoryQuery.from_messages` to delegate to the extracted policy.
- Added direct tests for the 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_memory_query_policy.py tests/test_memory_query.py tests/test_memory_invariants.py tests/test_litellm_callback.py tests/test_compress_api.py::TestLiteLLMCallback -q
30 passed in 6.69s

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 memory query tests, memory
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-10 23:44:27 -05:00
JD Davis
c29b4ba84f
refactor(output): isolate savings policy (#1947)
## Description

Extracts the output-savings stratification, holdout assignment,
conversation key, and transform-label helpers into a pure policy module
while preserving the existing `headroom.proxy.output_savings` public
imports. This keeps the estimator/ledger adapter focused on statistics
and persistence.

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_savings_policy` for pure savings policy
helpers.
- Re-exported the moved helpers from `headroom.proxy.output_savings` to
keep callers stable.
- 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_output_savings_policy.py tests/test_output_savings.py tests/test_litellm_callback.py tests/test_compress_api.py::TestLiteLLMCallback -q
54 passed in 6.42s

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 the focused pytest set, 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-10 23:42:11 -05:00
Abhay Singh
b699bedf95
fix(models): version-boundary longest-prefix match in ModelRegistry.get (#1658)
## Description

`ModelRegistry.get()` has a prefix fallback for versioned model ids. It
accepted
**any** registered name as a bare `str.startswith` prefix and returned
the
**first** match in dict-insertion order:

```python
for name, info in _MODELS.items():
    if model_lower.startswith(name):
        return info
```

Two concrete failures fall out of that:

- `gpt-4` is registered before `gpt-4-32k`, so `get("gpt-4-32k-0613")`
matches
`gpt-4` first and returns an **8192**-token window instead of
`gpt-4-32k`'s
  **32768**.
- `gpt-4.1` / `gpt-4.5-preview` aren't registered, so they also match
`gpt-4`
and inherit its **8192**-token window — even though they're much larger,
  distinct models.

`get_context_limit()` reads straight from `get()` (no LiteLLM fallback),
so both
cases make the proxy believe a nearly-empty context is almost full and
compress
far too aggressively — or reject — on requests that are actually small.
This is
silent: no error, just a wrong number driving every downstream
compression
decision for those models.

## Fix

The fallback now:

1. Only matches when the registered name ends at a **version boundary**
in the
query — the next character must be a separator (`-`, `/`, `:`, `@`, `_`)
— so
`gpt-4.1`'s `.` no longer matches `gpt-4` (it falls through to the
caller's
   default instead of a wrong 8192).
2. Picks the **longest** qualifying name, so `gpt-4-32k-0613` →
`gpt-4-32k`.

Exact and alias lookups are unchanged, and boundary-separated variants
like
`gpt-4o-new-version` still resolve to `gpt-4o`.

## Type of Change

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

## Changes Made

- `headroom/models/registry.py`: replace the first-match `startswith`
prefix loop in `ModelRegistry.get` with a
longest-prefix-at-a-version-boundary match.
- `tests/test_models.py`: add regression tests — `gpt-4-32k-0613` →
`gpt-4-32k` (32768), and `gpt-4.1`/`gpt-4.5-preview` no longer resolve
to gpt-4's 8192 window.
- `CHANGELOG.md`: Bug Fixes entry under Unreleased.

## Testing

- [x] New tests added for the fixed behavior (`tests/test_models.py`)
- [x] Linting passes (`ruff check`) and formatting is clean (`ruff
format --check`)
- [ ] Full `pytest` run deferred to CI — see Real Behavior Proof for why
I verify the logic with a dependency-free script locally.

```text
$ uv run ruff check headroom/models/registry.py tests/test_models.py
All checks passed!
$ uv run ruff format --check headroom/models/registry.py tests/test_models.py
2 files already formatted
```

## Real Behavior Proof

- Environment: Windows 11, Python 3.12.11, headroom built from this
branch (`uv sync --extra dev`). Importing `headroom` pulls in the
torch/transformers stack; a full `pytest` run exhausts memory and gets
OOM-killed on this box, so I verify the matching logic with a
dependency-free script (only stdlib) and leave the full pytest to CI.
- Exact command / steps: replicated the relevant `_MODELS` registration
order (`gpt-4o`, `gpt-4-turbo`, `gpt-4`, `gpt-4-32k`) and the new
longest-prefix-with-boundary loop in a standalone script (no `headroom`
import), then asserted the resolved context windows.
- Observed result: `gpt-4-32k-0613` resolves to 32768 (was 8192 under
first-match), `gpt-4.1`/`gpt-4.5-preview` fall through to the caller
default (no longer 8192), and `gpt-4o-new-version` / `gpt-4` /
`gpt-4-0613` resolve exactly as before:

```text
OK: gpt-4-32k-0613 -> 32768 (was 8192 under old first-prefix-wins)
OK: gpt-4.1 / gpt-4.5-preview -> default (not 8192)
OK: gpt-4o-new-version, gpt-4, gpt-4-0613 still resolve as before
REGISTRY LOGIC VERIFIED
```

- Not tested: I did not add explicit registry entries for
`gpt-4.1`/`gpt-4.5` (their real windows) — that's a data addition,
separate from this matching-logic fix; today they fall back to the
caller's default, which is honest for an unregistered model and strictly
better than the previous wrong 8192. 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; pure logic change in one function plus tests.
- Found via a read-through of the registry while looking at how context
limits drive compression decisions.
2026-07-10 23:07:31 -05:00
sgfdhgfnjgmhgmnfd657-sys
48f06caca7
ci: add Windows wheel build job (win_amd64) (#1086)
### Summary

Adds `build-wheel-windows` job to the CI pipeline that compiles the Rust
extension on `windows-latest` and uploads the resulting `.whl` as a
separate artifact (`headroom-wheel-windows`).

This addresses the long-standing missing Windows wheel.

### Changes

- New job `build-wheel-windows`: mirrors the existing `build-wheel`
(Linux) job
- Uses `dtolnay/rust-toolchain@stable` for Rust setup on Windows
- Uses `Swatinem/rust-cache` for dependency caching
- Builds with CI cargo profile for speed
- 45-minute timeout (Windows Rust builds are slower)
- Uploads wheel as `headroom-wheel-windows` artifact

### Testing

 **Local compilation verified**: built v0.26.0 from source on Windows
10 (Python 3.12.10, Rust 1.96.0, MSVC Build Tools 2022). The wheel
installed and ran successfully.

### Notes

Only the CI-profile build is added here. The release-wheel publish
(`release.yml`) can be updated in a follow-up PR once this basic Windows
build is proven in CI.

Co-authored-by: Win He <win-he@users.noreply.github.com>
2026-07-10 23:05:03 -05:00
Rod Boev
d1db00ab86
fix(proxy): keep PRE_SEND from reintroducing empty tool arrays (#2015)
## Description

The direct body-write fix for empty `tools: []` already landed, but the
later OpenAI PRE_SEND write-back path still reintroduces the empty
array. This aligns that guard with the existing direct-assignment
contract so tools-free requests stay tools-free while explicit client
`tools: []` stays preserved. Anthropic's current-main PRE_SEND path
already had the equivalent empty-tools protection and needed no code
change.

Closes #1983

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

- Mirror the direct `tools or _original_tools is not None` guard in the
OpenAI PRE_SEND write-back path.
- Leave Anthropic unchanged because current `main` already protects the
empty-tools case there.
- Extend the focused #728 regression file with PRE_SEND-specific
coverage.
- Add a changelog note for providers that reject empty `tools` arrays.

## Testing

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

### Test Output

```text
uv run pytest tests/test_issue_728_empty_tools_injection.py -q
11 passed

uv run ruff check headroom/proxy/handlers/openai.py tests/test_issue_728_empty_tools_injection.py
All checks passed

uv run ruff format --check headroom/proxy/handlers/openai.py tests/test_issue_728_empty_tools_injection.py
2 files already formatted
```

## Real Behavior Proof

- Environment: OpenAI-compatible provider that rejects empty `tools`
arrays
- Exact command / steps: send a request without `tools`, then repeat
with explicit `tools: []`
- Observed result: the OpenAI PRE_SEND path now skips `tools: []` when
the client omitted tools, while the focused regression still preserves
explicit client `tools: []` and deliberate clearing of a previously
present tool list
- Not tested: live provider run on this host
- Scope: PRE_SEND request-body write-back

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

## Additional Notes

The change is intentionally narrow. It only brings PRE_SEND write-back
into parity with the direct-assignment guard that already exists.
2026-07-10 22:38:27 -05:00
JD Davis
9bacf4810f
refactor(transforms): isolate mixed content parsing (#1939)
## Description

Extracts mixed-content parsing out of the large `ContentRouter` module
into a pure transform-domain module. The router still exports the
existing compatibility names, but section typing, mixed-content
indicators, section splitting, and JSON block extraction now live in a
focused domain object/function layer.

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.transforms.mixed_content` with `ContentSection`,
`mixed_content_indicators`, `is_mixed_content`, `split_into_sections`,
and JSON block extraction.
- Updated `ContentRouter` to delegate mixed-content debug indicators and
parsing to the new module while preserving legacy imports from
`content_router.py`.
- Added direct unit coverage for mixed-content detection, section
boundaries, and JSON delimiters inside string literals.
- Included the LiteLLM callback signature compatibility shim needed for
repo-wide mypy while the earlier architecture PRs are still open.

## 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_mixed_content_sections.py tests/test_transforms_content_router.py tests/test_litellm_callback.py tests/test_compress_api.py::TestLiteLLMCallback -q
50 passed in 6.82s

python -m ruff check .
All checks passed!

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

python -m mypy headroom --ignore-missing-imports
headroom\proxy\server.py:1457: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
headroom\proxy\server.py:1468: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
Success: no issues found in 409 source files
```

## Real Behavior Proof

- Environment: Windows, Python 3.13.13, clean worktree
`C:\git\headroom-pr-slice6`
- Exact command / steps: ran the pytest, Ruff, format, and mypy commands
listed above.
- Observed result: mixed-content parsing behavior remains covered
through existing router tests and new direct tests; repo-wide lint/type
checks pass.
- Not tested: full pytest suite and Docker/native CI jobs are left to
GitHub Actions.

## Review Readiness

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

## Checklist

- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] 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, changelog, and screenshots are N/A for this internal
refactor.
- Manual UI testing is N/A; this is pure transform parsing logic.
- Comment checklist is unchecked because the extracted functions are
small and covered by direct tests.
2026-07-10 19:28:21 -05:00
JD Davis
5a7265daa8
refactor(proxy): isolate auth classification policy (#1945)
## Description

Extract auth-mode and client-harness classification rules into
`headroom.proxy.auth_policy`, leaving `auth_mode` as the
header-reading/logging adapter. This gives the proxy a pure
`AuthSignals` value object and deterministic policy functions for auth
mode, client classification, and Codex Responses stamping.

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 `AuthSignals` as the normalized input model for pure auth/client
policy.
- Moved `AuthMode`, subscription UA prefixes, client UA map, Codex
Responses path, auth-mode classification, client classification, and
Codex stamping rules into `headroom.proxy.auth_policy`.
- Kept `headroom.proxy.auth_mode` public API stable by adapting headers
into `AuthSignals` and delegating to policy functions.
- Added direct pure-policy tests for subscription precedence, OAuth/PAYG
token shapes, explicit client override, and Codex Responses stamping.
- Included the LiteLLM callback hook compatibility shim needed for
repo-wide mypy on branches based 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_auth_policy.py tests/test_auth_mode.py tests/test_codex_client_stamp.py tests/test_litellm_callback.py tests/test_compress_api.py::TestLiteLLMCallback -q
48 passed in 6.63s

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

## Real Behavior Proof

- Environment: Windows, Python 3.13.13, local worktree
`C:\git\headroom-pr-slice9`.
- Exact command / steps: Ran the focused pytest suite plus repo-wide
Ruff, format check, and mypy commands listed above.
- Observed result: Existing adapter behavior remains covered by
`tests/test_auth_mode.py` and `tests/test_codex_client_stamp.py`, while
the extracted pure policy is covered by `tests/test_auth_policy.py`.
- Not tested: Full test suite locally; CI will run the full 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 LiteLLM shim is repeated here because this branch is
intentionally independent from the other open architecture slices and
must stay green against current `main`.
2026-07-10 19:27:08 -05:00
JD Davis
b5aa8a358e
refactor(cache): isolate compression strategy outcomes (#1938)
## Description

Extracts local compression strategy accounting out of
`CompressionFeedback` into a pure cache-domain object. This keeps
strategy counters, retrieval-rate math, pruning, and best-strategy
selection independently testable while preserving the existing
`LocalToolPattern` public 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 `CompressionStrategyOutcomes` as the strategy-outcome domain for
compression/retrieval counters, pruning, retrieval rates, and
recommendation selection.
- Updated `LocalToolPattern` and `CompressionFeedback` to delegate
strategy accounting to that domain while keeping existing fields and
methods intact.
- Added direct unit coverage for strategy outcome math and bounded
pruning behavior.
- Updated the LiteLLM callback hook signature to remain compatible with
current LiteLLM typing and the existing three-argument call shape.

## 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 ruff check .
All checks passed!

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

python -m mypy headroom --ignore-missing-imports
headroom\proxy\server.py:1457: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
headroom\proxy\server.py:1468: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
Success: no issues found in 409 source files

python -m pytest tests/test_compression_strategy_outcomes.py tests/test_ccr_feedback.py tests/test_toin_fixes.py tests/test_litellm_callback.py tests/test_compress_api.py::TestLiteLLMCallback -q
collected 54 items
46 passed, 8 skipped in 6.58s
```

## Real Behavior Proof

- Environment: Windows, Python 3.13.13, clean worktree
`C:\git\headroom-pr-slice5`
- Exact command / steps: ran the lint, format, type-check, and focused
pytest commands listed above.
- Observed result: strategy outcome tests and existing
feedback/TOIN/LiteLLM compatibility tests pass; repo-wide lint/type
validation passes.
- Not tested: full pytest suite and Docker/native CI jobs are left to
GitHub Actions.

## Review Readiness

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

## Checklist

- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] 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 are N/A for this internal refactor.
- Manual UI testing is N/A; this is cache feedback and integration
callback logic.
- Comment checklist is unchecked because the extracted object is
intentionally straightforward and covered by tests.
2026-07-10 19:21:47 -05:00
Rod Boev
41af39d769
fix(proxy): preserve terminal tool on Codex Responses (#2000)
## Description

Cache-mode optimization can make a client-defined Responses function
named `terminal` invalid by treating it as a deferrable tool. On
supported models with a large tool set, Headroom adds `defer_loading`
and tool search; the Codex endpoint then rejects the request as
`terminal.terminal` in a reserved namespace.

This keeps the exact `terminal` function resident in the OpenAI
Responses deferral helper. Other non-core functions and MCP tools remain
eligible for deferral, and unsupported models or small tool sets keep
their existing no-op behavior.

Closes #1946

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

- Keep the exact OpenAI Responses function name `terminal` resident
during server-side tool-search deferral.
- Preserve deferral for adjacent and unrelated function names, MCP
tools, and the existing model and tool-count gates.
- Add issue-shaped regression and negative-space coverage.
- Document the user-visible fix in `CHANGELOG.md`.

## Testing

- [x] Unit tests pass (`uv run pytest
tests/test_openai_tool_search_deferral.py -q`)
- [x] Linting passes (`uv run ruff check headroom/proxy/helpers.py
tests/test_openai_tool_search_deferral.py`)
- [ ] Type checking passes (`uv run mypy headroom`)
- [x] New tests added for new functionality when applicable
- [ ] Manual testing performed

### Test Output

```text
uv sync --extra dev
OK

uv run pytest tests/test_openai_tool_search_deferral.py -q
25 passed

uv run ruff check headroom/proxy/helpers.py tests/test_openai_tool_search_deferral.py
All checks passed

uv run ruff format --check headroom/proxy/helpers.py tests/test_openai_tool_search_deferral.py
2 files already formatted
```

## Real Behavior Proof

- Environment: credentialed Codex Responses endpoint, `gpt-5.6-terra`,
Headroom cache mode with lossless compression
- Exact command / steps: start `headroom proxy --mode cache --lossless`,
then send a Responses request with at least 12 tools including the bare
client-defined `terminal` function
- Observed result: local proof now locks the emitted request shape,
`terminal` stays resident, adjacent names such as `terminal_helper`
still defer, and the input remains unchanged; live upstream acceptance
on `gpt-5.6-terra` still needs a credentialed run
- Not tested: live upstream acceptance on a credentialed `gpt-5.6-terra`
Responses request with the exact issue-shaped tool set.
- Scope: OpenAI Responses tool-search deferral in the optimized request
path

## Review Readiness

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

## Checklist

- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [ ] 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 change is scoped to the exact `terminal` function name in OpenAI
Responses tool-search deferral. It does not change ContentRouter policy,
Anthropic tool deferral, tool schema compaction, or unrelated function
names. Live endpoint acceptance is still an external proof item and is
called out in Real Behavior Proof.
2026-07-10 16:17:25 -07:00
Dima Solodukha
75d786117a
fix(proxy): cache_savings_usd silently zeroes when litellm is unavailable (#2005)
## Description

On any install where `litellm` cannot be imported, `SavingsTracker`'s
`lifetime.cache_savings_usd` and `display_session.cache_savings_usd`
stay pinned at exactly `0.0` forever — while `cache_read_tokens`
accumulates correctly and `total_input_cost_usd` stays nonzero, so the
tracker looks alive and the zero is easy to miss.

This hits every Python 3.14 install out of the box: the project's own
dependency spec is `litellm>=1.86.2,<2.0 ; python_full_version <
'3.14'`, so on 3.14 `LITELLM_AVAILABLE` is `False` and cache savings
silently read as $0. Observed in the wild with 45.8M lifetime
`cache_read_tokens` and `cache_savings_usd: 0.0` in
`proxy_savings.json`.

Root cause: `_estimate_cache_savings_usd` is the only one of the three
USD estimators with no fallback when litellm is missing —
`_estimate_input_cost_usd` and `_estimate_compression_savings_usd` both
fall back to `DEFAULT_FALLBACK_INPUT_COST_PER_TOKEN`, while
`_estimate_cache_savings_usd` returns `0.0`.

## Type of Change

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

## Changes Made

- `headroom/proxy/savings_tracker.py`: when litellm is unavailable,
`_estimate_cache_savings_usd` now estimates at
`DEFAULT_FALLBACK_INPUT_COST_PER_TOKEN` per cache-read token — mirroring
the fallback its two sibling estimators already use (approximate over
zero). Unknown-model behaviour with litellm present is unchanged (still
fails open to `0.0`).
- `tests/test_proxy_savings_history.py`: new regression test
`test_cache_savings_usd_falls_back_when_litellm_unavailable` (unit +
through `SavingsTracker.record_request`);
`test_cache_savings_edge_cases_zero_and_unpriced` now pins a fake
litellm price table so it keeps testing the unpriced-model path on every
environment (without litellm installed it would otherwise exercise the
fallback path instead).

## 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_proxy_savings_history.py -k "cache_savings" -q
========================= 3 passed, 1 warning in 0.34s =========================

$ ruff check headroom/proxy/savings_tracker.py tests/test_proxy_savings_history.py
All checks passed!
$ ruff format --check headroom/proxy/savings_tracker.py tests/test_proxy_savings_history.py
2 files already formatted

$ mypy headroom/proxy/savings_tracker.py
Success: no issues found in 1 source file
```

## Real Behavior Proof

- Environment: macOS, Python 3.14.6 venv, headroom installed editable —
litellm absent (excluded by the project's own `python_full_version <
'3.14'` marker), i.e. the exact environment the bug ships in.
- Exact command / steps: ran the one-liner below twice in that venv —
once with `headroom/proxy/savings_tracker.py` checked out from main
(`d2170b19`), once from this branch — output pasted verbatim:
  ```text
  $ python -c 'import headroom.proxy.savings_tracker as st;
    print("litellm importable:", st._get_litellm_module() is not None);
    print("cache_savings_usd for 1M cache-read tokens:",
st._estimate_cache_savings_usd("claude-sonnet-4-6", 1_000_000))'

  # on main (d2170b19):
  litellm importable: False
  cache_savings_usd for 1M cache-read tokens: 0.0

  # on this branch:
  litellm importable: False
  cache_savings_usd for 1M cache-read tokens: 3.0
  ```
- Observed result: with litellm missing, main reports $0 saved for 1M
cache-read tokens; this branch reports the blended-rate estimate ($3.00
at the default fallback rate), consistent with what
`_estimate_input_cost_usd` already does for input cost.
- Not tested: proxy end-to-end on Python < 3.14 with litellm installed
(that path is unchanged — the litellm branch of the function is
untouched, covered by the existing
`test_cache_savings_usd_uses_litellm_discount_delta`).

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

- Documentation / CHANGELOG: no user-facing docs describe the
per-function fallback behaviour, and I didn't find a maintained
CHANGELOG.md at the repo root — happy to add an entry if there's a
preferred place.
- Full `tests/test_proxy_savings_history.py` run in my venv shows 7
failures that are identical on current main (missing optional dashboard
deps in my environment, unrelated to this change); every test touching
this change passes.
- `mypy headroom/proxy/savings_tracker.py` also prints a pre-existing
`pyproject.toml: note: unused section(s)` notice unrelated to this diff.
2026-07-10 16:12:32 -07:00
JD Davis
cb38f79377
refactor(proxy): isolate forwarded header policy (#1942)
## Description

Extract the trusted forwarded-header trust policy into
`headroom.proxy.forwarded_policy`, leaving `forwarded_headers` as the
FastAPI/request-state adapter. This makes CIDR parsing, peer trust,
leftmost forwarded-for handling, and rejection decisions deterministic
and directly testable without request/logging side effects.

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 `ForwardedHeaderInputs` and `ForwardedHeaderResolution` as pure
policy value objects.
- Moved CIDR parsing, IP normalization, trust membership, header
splitting, and forwarded-header resolution into
`headroom.proxy.forwarded_policy`.
- Kept `headroom.proxy.forwarded_headers` as the request adapter with
the same public API and compatibility helper names.
- Added direct tests for trusted, rejected, direct-client, IPv4-mapped
IPv6, and leftmost `X-Forwarded-For` policy behavior.
- Included the LiteLLM callback hook compatibility shim needed for
repo-wide mypy on branches based 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_forwarded_policy.py tests/test_forwarded_headers.py tests/test_litellm_callback.py tests/test_compress_api.py::TestLiteLLMCallback -q
51 passed in 6.36s

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

## Real Behavior Proof

- Environment: Windows, Python 3.13.13, local worktree
`C:\git\headroom-pr-slice8`.
- Exact command / steps: Ran the focused pytest suite plus repo-wide
Ruff, format check, and mypy commands listed above.
- Observed result: The existing request-facing forwarded-header behavior
remains covered by `tests/test_forwarded_headers.py`, while the
extracted pure policy is covered by `tests/test_forwarded_policy.py`.
- Not tested: Full test suite locally; CI will run the full 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 LiteLLM shim is repeated here because this branch is
intentionally independent from the other open architecture slices and
must stay green against current `main`.
2026-07-10 17:41:38 -05:00
JD Davis
0ce09fb63f
refactor(output): isolate verbosity steering (#1940)
## Description

Extract byte-stable output verbosity steering into
`headroom.proxy.output_steering` so `output_shaper` can focus on turn
classification and effort routing while preserving the existing public
import surface.

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_steering` for Anthropic system steering
and OpenAI Responses instruction steering.
- Kept existing `headroom.proxy.output_shaper` imports compatible by
re-exporting the moved helpers.
- Added direct tests for replacement, cache-prefix preservation, and
idempotent OpenAI Responses steering.
- Included the LiteLLM callback hook compatibility shim needed for
repo-wide mypy on branches based 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_output_steering.py tests/test_output_shaper.py tests/test_litellm_callback.py tests/test_compress_api.py::TestLiteLLMCallback -q
57 passed in 6.17s

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

## Real Behavior Proof

- Environment: Windows, Python 3.13.13, local worktree
`C:\git\headroom-pr-slice7`.
- Exact command / steps: Ran the focused pytest suite plus repo-wide
Ruff, format check, and mypy commands listed above.
- Observed result: Steering behavior remains covered through the
existing `output_shaper` tests and the new direct `output_steering`
tests.
- Not tested: Full test suite locally; CI will run the full 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 LiteLLM shim is repeated here because this branch is
intentionally independent from the other open architecture slices and
must stay green against current `main`.
2026-07-10 17:39:39 -05:00
JD Davis
fd5b9e75ad
refactor(ccr): isolate tool call classification (#1937)
## Description
Extracts provider-shaped CCR tool-call extraction and classification
into `headroom.ccr.tool_calls`. `CCRResponseHandler` now delegates
detection/parsing to a pure domain module and stays focused on retrieval
execution and continuation orchestration.

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
- Added `headroom.ccr.tool_calls` with provider-native extraction, CCR
detection, provider-specific tool result IDs, and CCR/other-tool
splitting.
- Re-exported the pure CCR tool-call helpers from `headroom.ccr`.
- Kept `CCRResponseHandler` private compatibility methods while
delegating to the new module.
- Added focused tests for Anthropic, OpenAI, Google, and OpenAI
Responses tool-call shapes.

## 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_tool_calls.py tests/test_ccr_response_handler.py tests/test_ccr_response_handler_extra.py tests/test_ccr_response_handler_openai_responses.py tests/test_ccr_batch_processor.py::TestCCRToolCallDetectionInBatch -q
============================= 64 passed in 0.57s =============================

python -m ruff check headroom/ccr/tool_calls.py headroom/ccr/response_handler.py headroom/ccr/__init__.py tests/test_ccr_tool_calls.py tests/test_ccr_response_handler.py tests/test_ccr_response_handler_extra.py tests/test_ccr_response_handler_openai_responses.py tests/test_ccr_batch_processor.py
All checks passed!

python -m mypy headroom/ccr/tool_calls.py headroom/ccr/response_handler.py
Success: no issues found in 2 source files

python -m compileall -q headroom\ccr\tool_calls.py headroom\ccr\response_handler.py headroom\ccr\__init__.py
# no output; exited 0

git commit -m "refactor(ccr): isolate tool call classification"
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/architecture-slice-4` based on `headroomlabs/main`.
- Exact command / steps: Ran CCR tool-call tests, existing CCR response
handler tests, OpenAI Responses CCR tests, CCR batch detection tests,
focused ruff, targeted mypy, compileall, and commit hooks.
- Observed result: Existing handler behavior remains covered while
provider-shaped CCR classification is now directly testable as a pure
module.
- Not tested: Full pytest suite, live upstream provider traffic, and
manual streaming clients.

## 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.
Full pytest was not run; validation is focused on CCR tool-call
detection/parsing and response-handler compatibility.
2026-07-10 17:36:24 -05:00