mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-27 14:17:10 -04:00
2630 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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. |
||
|
|
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. |
||
|
|
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 `
|
||
|
|
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> |
||
|
|
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. |
||
|
|
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> |
||
|
|
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.
|
||
|
|
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 & 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=" |
||
|
|
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> |
||
|
|
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<str></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=" |
||
|
|
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 = "forbid"</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 "no-std" 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=" |
||
|
|
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("<a href="https://example.com">https://example.com</a>")?;</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("assets", 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<ServeFileSystemResponseBody></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=" |
||
|
|
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=" |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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 @
|
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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 |
||
|
|
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 @
|
||
|
|
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!
|
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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 `
|
||
|
|
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. |
||
|
|
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. |
||
|
|
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.
|
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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.
|
||
|
|
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>
|
||
|
|
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. |
||
|
|
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. |
||
|
|
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`. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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 (
|
||
|
|
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`. |
||
|
|
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`. |
||
|
|
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. |