mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-27 14:17:10 -04:00
2001 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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. |
||
|
|
4210d6e609
|
refactor(pricing): isolate litellm model resolution (#1936)
## Description Extracts LiteLLM model-name resolution rules into a pure pricing-domain module. `litellm_pricing.py` now acts as the adapter that asks LiteLLM whether candidate keys exist, while `litellm_model_resolution.py` owns prefix rules, alias rules, lookup candidate ordering, and deterministic resolution. 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.pricing.litellm_model_resolution` with explicit prefix rules, alias rules, pricing lookup candidates, and a pure resolver function. - Simplified `headroom.pricing.litellm_pricing` to delegate model-name selection to the pure resolver while keeping its public API and cache behavior intact. - Added focused tests for candidate ordering, case-insensitive MiniMax matching, aliases, and unknown-model fallback. ## 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_pricing_litellm_model_resolution.py tests/test_pricing_litellm.py tests/test_proxy_streaming_resilience.py::TestModelResolutionCaching -q ============================= 22 passed in 2.18s ============================= python -m ruff check headroom/pricing/litellm_model_resolution.py headroom/pricing/litellm_pricing.py tests/test_pricing_litellm_model_resolution.py tests/test_pricing_litellm.py tests/test_proxy_streaming_resilience.py All checks passed! python -m mypy headroom/pricing/litellm_model_resolution.py headroom/pricing/litellm_pricing.py Success: no issues found in 2 source files python -m compileall -q headroom\pricing\litellm_model_resolution.py headroom\pricing\litellm_pricing.py # no output; exited 0 git commit -m "refactor(pricing): isolate litellm model resolution" 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/pricing-model-resolution` based on `headroomlabs/main`. - Exact command / steps: Ran pure resolver tests, LiteLLM pricing adapter tests, model-resolution caching tests, focused ruff, targeted mypy, compileall, and commit hooks. - Observed result: Existing pricing behavior and cache behavior passed while model resolution is now isolated and directly testable. - Not tested: Full pytest suite and live LiteLLM network or package update behavior beyond the local installed dependency/fakes. ## 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 pricing/model-resolution behavior touched by this slice. |
||
|
|
1f3696a3d0
|
refactor(proxy): isolate body forwarding policy (#1935)
## Description Extracts the byte-faithful Python forwarder policy out of the broad proxy helpers module into a dedicated `headroom.proxy.body_forwarding` domain. The new module owns the outbound body algebra: passthrough original bytes, canonical JSON bytes for mutated bodies, and explicit legacy JSON rollback mode. 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.proxy.body_forwarding` with `OutboundBody`, `OutboundBodySource`, `BodyMutationTracker`, mode resolution, canonical serialization, and body selection helpers. - Kept `headroom.proxy.helpers` compatibility exports for existing callers. - Updated Python forwarder call sites to import body-forwarding policy from the dedicated module. - Added tests for the new value object and compatibility exports. ## 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_byte_faithful_forwarding.py -q ============================= 40 passed in 3.61s ============================= python -m ruff check headroom/proxy/body_forwarding.py headroom/proxy/helpers.py headroom/proxy/server.py headroom/proxy/handlers/streaming.py headroom/proxy/handlers/openai.py headroom/proxy/handlers/anthropic.py headroom/proxy/handlers/batch.py tests/test_proxy_byte_faithful_forwarding.py All checks passed! python -m mypy headroom/proxy/body_forwarding.py Success: no issues found in 1 source file python -m compileall -q headroom\proxy\body_forwarding.py headroom\proxy\helpers.py headroom\proxy\server.py headroom\proxy\handlers\streaming.py headroom\proxy\handlers\openai.py headroom\proxy\handlers\anthropic.py headroom\proxy\handlers\batch.py # no output; exited 0 git commit -m "refactor(proxy): isolate body forwarding policy" 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-2` based on `headroomlabs/main`. - Exact command / steps: Ran the focused byte-faithful forwarding suite, focused ruff command, targeted mypy, compileall over touched modules, and commit hooks. - Observed result: Forwarding behavior stayed byte-faithful; compatibility exports remain intact; lint, formatting, and mypy passed. - Not tested: Full pytest suite, live upstream proxy traffic, and manual end-to-end 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. The full pytest suite was not run; validation is focused on the body-forwarding domain and existing byte-faithful forwarding coverage. |
||
|
|
d2170b1922
|
fix(learn): parse fenced JSON even with a prose preamble (#1988)
## Description `_strip_fenced_json` only stripped a markdown fence when the string *started with* ```` ``` ````. When the model prefixed prose before the fence (e.g. `Here is the JSON:\n\n```json ...`) despite being told to return JSON only, the guard was skipped and `json.loads` ran on the prose, raising `JSONDecodeError`. The claude-cli streaming path surfaced this as `returned unparseable output`, and `headroom learn` silently discarded the LLM analysis, degrading to "No actionable patterns found". This is the parsing-side cousin of the silent-degradation issue fixed in #373. Closes #1989. Related: #373. ## 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/learn/analyzer.py`: rewrote `_strip_fenced_json` to locate the fenced block wherever it appears, then fall back to the whole text, then to a first-`{` / last-`}` slice, only re-raising `JSONDecodeError` if nothing parses as a JSON object. Preserves the prior "first opening / last closing fence" behaviour and triple-backtick content inside the payload. Fixes all three call sites (non-streaming CLI, claude-cli streaming, litellm). - `tests/test_learn/test_analyzer.py`: added regression cases to `TestStripFencedJson` for preamble-before-fence, prose around a bare object, and triple-backticks inside the payload. ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [x] Type checking passes (`mypy headroom`) — scoped to the changed module (see Additional Notes) - [x] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text $ pytest tests/test_learn/test_analyzer.py -q ........................................................................ [ 86%] ........... [100%] 83 passed, 1 warning in 2.18s $ ruff check headroom/learn/analyzer.py tests/test_learn/test_analyzer.py All checks passed! $ ruff format --check headroom/learn/analyzer.py tests/test_learn/test_analyzer.py 2 files already formatted $ mypy --ignore-missing-imports --follow-imports=silent headroom/learn/analyzer.py Success: no issues found in 1 source file ``` ## Real Behavior Proof - Environment: macOS, Python 3.13.12, headroom-ai at this branch (runtime deps from an installed 0.30.0 env). - Exact command / steps: ran the old vs new `_strip_fenced_json` on the exact failing model output (a prose preamble followed by a ```json fence), then applied the fix over an installed 0.30.0 and re-ran the previously failing `headroom learn --apply`. Input sample: `'The JSON is my deliverable for this analysis task. Here it is:\n\n```json\n{"context_file_rules": [], "memory_file_rules": []}\n```'` - Observed result: OLD raised `JSONDecodeError: Expecting value: line 1 column 1 (char 0)`; NEW returned `{'context_file_rules': [], 'memory_file_rules': []}`. The real `headroom learn --apply` run that had been failing with `returned unparseable output` then completed and consumed the LLM analysis instead of dropping it. Full transcript: ```text OLD: JSONDecodeError -> Expecting value: line 1 column 1 (char 0) NEW: {'context_file_rules': [], 'memory_file_rules': []} ``` - Not tested: full end-to-end `headroom learn --apply` was not re-run inside CI here (it shells out to a live `claude` CLI); the parser is exercised deterministically by the added unit tests and the before/after repro 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 (N/A — updated the function docstring only; no external docs affected) - [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 (N/A — no CHANGELOG entry convention observed for this fix; happy to add if maintainers prefer) ## Additional Notes - `mypy` was run against the changed module in isolation (`--ignore-missing-imports --follow-imports=silent`) rather than the full project, because I validated in an ad-hoc environment; the change keeps the existing `-> dict` signature and annotations, so it is type-neutral. - Not addressed here (possible follow-up): the failure is swallowed as a warning in `analyze()`, so users only see "No actionable patterns found" with no signal the LLM pass produced nothing — the same silent-degradation class as #373, on the parsing side. |
||
|
|
5e14b8c0f2
|
fix(memory/sync): don't clobber memories sharing a first line (#1976)
## Description
`ClaudeCodeAdapter.write_memories`
(`headroom/memory/sync_adapters/claude_code.py`) picks
each memory's file name from the **first line of its content only**:
```python
first_line = content.split("\n")[0][:60].strip()
slug = _sanitize_for_filename(first_line)
filename = f"headroom_{slug}.md"
```
So two *distinct* DB memories whose first lines slugify to the same
value map to the same
file. The existing "already on disk?" guard only skips when the on-disk
content hash equals
this memory's hash — for a genuine collision (same slug, different body)
it falls through and
`target.write_text(...)` overwrites the other memory. Data loss.
It also never converges. The overwritten memory never lands on disk, so
on the next
`sync_export` the adapter reads back the agent's files, doesn't find
that memory's hash in
`agent_hashes`, and re-exports it — overwriting the other one this time.
The pair ping-pongs
on every sync, and each round appends a fresh line to `MEMORY.md`.
This is realistic for headed/structured memories (e.g. several entries
that begin
`# Project conventions` or `The user prefers …`).
Closes: no issue filed — found while auditing the memory sync adapters.
## Fix
When the slug is already taken by a **different** memory (a distinct
`headroom_id` in the
existing file's frontmatter), disambiguate the file name with a short
content-hash suffix so
both survive. A matching `headroom_id` means it's an update of the same
memory, so the plain
slug file is rewritten as before — existing file names don't change, so
there's no migration
churn for the common (no-collision) case:
```python
existing_id = existing_fm.get("headroom_id", "")
if headroom_id and existing_id and existing_id != headroom_id:
suffix = (content_hash or hashlib.sha256(content.encode()).hexdigest()[:16])[:8]
filename = f"headroom_{slug}_{suffix}.md"
...
```
## Type of Change
- [x] Bug fix (non-breaking change that fixes an issue)
## Changes Made
- `headroom/memory/sync_adapters/claude_code.py`: in `write_memories`,
disambiguate the file name with a content-hash suffix when the slug
already belongs to a different `headroom_id`; same-id updates still
rewrite the slug file in place.
- `tests/test_memory_sync.py`: add
`test_write_distinct_memories_sharing_first_line_do_not_clobber` (two
files survive) and `test_write_same_memory_updates_in_place` (no
duplicate on update).
## Testing
- [x] New regression tests added (`tests/test_memory_sync.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/memory/sync_adapters/claude_code.py tests/test_memory_sync.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 write logic with
a dependency-free script (replicating `_sanitize_for_filename` /
`_parse_frontmatter` / the write loop against a real temp dir) and left
the full pytest to CI.
- Exact command / steps: wrote two memories that share the first line `#
Project conventions` but differ in body (distinct `headroom_id`),
through both the old and new logic, then wrote a same-id update.
- Observed result: the old logic reports `written=2` but leaves **one**
file (the first memory's body is gone); the new logic keeps both, and a
same-id update rewrites in place instead of duplicating:
```text
OLD: written=2 files=1 tabs=False fridays=True
NEW: written=2 files=2 tabs=True fridays=True
UPDATE: files=1 second=True
MEMORY COLLISION FIX VERIFIED
```
- Not tested: a full `sync_export`/`sync_import` round-trip through the
DB backend (needs the heavy stack). The fix is confined to the
file-naming decision in `write_memories`, and the new tests drive that
method directly. Full local `pytest` deferred to CI (OOM, per above).
## Review Readiness
- [x] I have performed a self-review
- [x] This PR is ready for human review
## Checklist
- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes — ran
lint + a standalone logic check; full pytest deferred to CI (local OOM,
disclosed above)
- [x] I have updated the CHANGELOG.md if applicable
## Additional Notes
- No new dependencies; a small, migration-safe naming guard plus tests.
- @JerrettDavis tagging you — this one is a quiet data-loss path in the
Claude memory sync (a collision drops one memory and then thrashes on
every sync), so it may be worth a look when you get a chance.
|
||
|
|
4cb33cd9e3
|
fix(proxy): strip inbound Content-Encoding on messages/chat forward (#1970)
## Description
The Anthropic `/v1/messages` and OpenAI `/v1/chat/completions` handlers
decode the
inbound request body before forwarding it upstream.
`read_request_json_with_bytes`
(helpers.py) inflates `zstd`/`gzip`/`deflate`/`br` bodies, and the
handler forwards
the resulting plain JSON. But both handlers build the upstream-bound
header dict and
pop only `host`, `content-length`, and `accept-encoding` — they leave
the original
`Content-Encoding` header in place:
```python
headers = dict(request.headers.items())
headers.pop("host", None)
headers.pop("content-length", None)
headers.pop("accept-encoding", None) # content-encoding NOT popped
```
So when a client — or an edge proxy like a Cloudflare Worker — sends a
request with
`Content-Encoding: gzip` (or `zstd`/`br`/`deflate`) and a compressed
body, Headroom
decompresses it, then forwards plain JSON that still advertises
`content-encoding: gzip`.
The upstream provider tries to gunzip already-decoded JSON and rejects
the request with
HTTP 400. Every such request fails.
This is a known class of bug: the `/v1/responses` handler already fixes
exactly this at
`openai.py` with the comment *"Leaving a stale content-encoding header
makes the upstream
try to decompress already-decoded JSON and reject it with HTTP 400
(#1542)."* That fix
landed only on the `/responses` path — the messages and chat paths were
missed.
Closes: no issue filed — found while auditing request-header forwarding
across the handlers.
## Fix
Pop `content-encoding` and `transfer-encoding` in both handlers, right
after the existing
`content-length` pop, mirroring the `/v1/responses` handler:
```python
headers.pop("content-encoding", None)
headers.pop("transfer-encoding", None)
```
## Type of Change
- [x] Bug fix (non-breaking change that fixes an issue)
## Changes Made
- `headroom/proxy/handlers/anthropic.py`: strip
`content-encoding`/`transfer-encoding` from the upstream-bound request
headers in `handle_anthropic_messages`.
- `headroom/proxy/handlers/openai.py`: same strip in the
`/v1/chat/completions` handler.
- `tests/test_proxy_compression_headers.py`: add
`TestRequestContentEncodingStripping` covering gzip/zstd/deflate/br,
`transfer-encoding`, and the absent-header (plain curl) case.
## Testing
- [x] New regression tests added
(`tests/test_proxy_compression_headers.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/proxy/handlers/anthropic.py headroom/proxy/handlers/openai.py tests/test_proxy_compression_headers.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 header logic
with a dependency-free script (the same pure-dict pattern the existing
tests in this file use) and left the full pytest to CI.
- Exact command / steps: replicated the handler's request-header
stripping (old vs new) in a standalone script and ran a
`gzip`/`zstd`/`deflate`/`br` request through both.
- Observed result: the old logic keeps `content-encoding` (which is what
makes the upstream 400); the new logic strips it while preserving
`authorization` and `content-type`:
```text
OK gzip: old leaks 'gzip' -> upstream 400 ; new strips it
OK zstd: old leaks 'zstd' -> upstream 400 ; new strips it
OK deflate: old leaks 'deflate' -> upstream 400 ; new strips it
OK br: old leaks 'br' -> upstream 400 ; new strips it
OK transfer-encoding stripped
OK safe when absent
CONTENT-ENCODING STRIP VERIFIED
```
- Not tested: an end-to-end POST of a real gzip body through a booted
proxy to a live upstream (needs the heavy stack + a provider key). The
header now matches the byte-faithful forwarding the `/responses` path
already does, and the new tests exercise the exact strip logic. 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 parity fix — two `pop()` calls plus tests, no new
dependencies.
- @JerrettDavis tagging you since you've been triaging the
proxy-forwarding fixes (this is the sibling of the #1542 `/responses`
fix) — should be a quick one if you have a moment.
|
||
|
|
10e4829201
|
fix(dedup): shorter fold pointer + MIN_LINES=3, dedup Anthropic list-… (#1932)
…content tool_results, line-number reanchoring
cross_turn_dedup:
- Compact the fold pointer (~35c vs ~100c) and lower MIN_LINES=3 /
MIN_CHARS=40 so the abundant short (2-4 line) re-read repeats pay off
(~4.3% -> ~6% lossless observation-byte reduction on Opus SWE-bench,
measured on 73 real trajectories).
- Add uniform-offset line-number reanchoring: a re-read whose line
numbers all shifted by a constant after an edit now still folds.
Matching keys on the number-stripped content; the offset is carried in
the pointer so recovery of the original numbered bytes stays exact
(unpadded grep/sed/rg -n form only). Cache-safe (prefix-monotonic) and
information-preserving (earliest copy kept verbatim). Small real-data
gain, but closes the renumbering blind spot.
content_router:
- Dedup Anthropic list-content tool_result blocks (the single
{"type":"text"} sub-block), which the string-only extraction path
previously skipped entirely, so cross-turn dedup never fired on
Anthropic-native tool results.
## 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>
|
||
|
|
7dbb9c3810
|
chore: extract agent-evals into standalone headroom-bench repo (#1967)
## Description `agent-evals` was a self-contained nested project (a coding-agent accuracy A/B framework: run trusted coding benchmarks WITH vs WITHOUT Headroom). It has no runtime coupling to the `headroom` wheel and was never wired into `make ci-precheck`. It has been extracted into its own repo (`headroom-bench`) so its heavy benchmark deps (swebench, mini-swe-agent, modal) never touch headroom and it can iterate on its own cadence. This PR removes the 33 nested files. Full history is preserved in the extracted repo. Closes # ## Type of Change - [x] Code refactoring (no functional changes) ## Changes Made - Remove `agent-evals/` (33 files) — extracted to the standalone `headroom-bench` repo. ## Testing `agent-evals` was never imported by the headroom package and never part of `make ci-precheck`, so headroom's build/lint/type/test surface is unaffected by this pure deletion. ### Test Output ```text # No headroom code touched. Verification that the removal is self-contained: $ git grep -Ei 'agent[-_]evals' -- ':!agent-evals/' ':!*.lock' CHANGELOG.md:270:* **agent-evals:** Phase 0 ... # historical changelog entry only (kept) # -> zero code / CI / import references $ git diff --name-only upstream/main..HEAD | wc -l 33 $ git diff --name-only upstream/main..HEAD | grep -vc '^agent-evals/' 0 # every changed file is under agent-evals/ ``` ## Real Behavior Proof - Environment: `headroom` @ branch `chore/extract-agent-evals` (1 commit over `upstream/main`; fork in sync, 0 drift). - Exact command / steps: `git subtree split --prefix=agent-evals` -> seeded the new repo `headroom-bench` (history preserved); `git rm -r agent-evals` here. - Observed result: 33-file deletion, all under `agent-evals/`; no dangling references in code, `Makefile`, or `.github/workflows/`. The extracted repo is intact and its suite passes (78 passed, 3 skipped). - Not tested: nothing runtime in headroom changes (agent-evals was never imported by the wheel). ## 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] My changes generate no new warnings - [x] New and existing unit tests pass locally with my changes ## Screenshots (if applicable) n/a |
||
|
|
88e41b65a1
|
Extract request log redaction policy (#1968)
## Description Extracts the pure image-base64 request-log redaction decision/transform logic from `request_logger.py` into a dedicated policy module. `RequestLogger` remains the owner of the Prometheus-facing redaction counter and existing request_logger constants remain available for compatibility. 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.request_log_redaction_policy` with a pure `RedactionResult` outcome. - Kept global redaction metrics/counter side effects in `request_logger.py`. - Added direct policy tests for count reporting, nested image paths, and data URL threshold behavior. - 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_log_redaction_policy.py tests\test_image_log_redaction.py 20 passed in 0.31s 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-23`. - Exact command / steps: ran targeted request-log redaction tests, ruff, ruff format check, mypy, and staged gitleaks scan. - Observed result: redaction behavior remains covered through existing logger tests and new pure policy tests; local lint/type/security checks pass. - Not tested: full proxy runtime; this slice only moves pure redaction policy and keeps the logger entry point 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. |
||
|
|
1d2b76e72e
|
fix: harden persistent install startup (#1851)
## Description Hardens persistent install startup and proxy compression behavior for issue #1843. Repeated `headroom install start` / scheduled ensure calls no longer spawn duplicate runtimes by default, and `/v1/compress` now fails open on compression timeout instead of returning a 503. The PR also adds a machine-readable platform feature matrix and app-level stabilization tests for health, compression functionality, timeout behavior, and matrix evidence. Refs #1843 ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) - [ ] New feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [x] Documentation update - [x] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - Wrapped direct persistent deployment starts with the existing profile-local runtime start lock. - Made `headroom install start` idempotent when the deployment is already healthy. - Added wedged-runtime handling: if a PID is running but `/readyz` does not recover inside the grace window, stop it before starting again. - Kept `install agent ensure` inside the already-held lock while delegating to the shared start helper. - Changed `/v1/compress` timeout behavior from `503 compression_timeout` to fail-open `200` with original messages, `compression_skipped: true`, and `skip_reason: compression_timeout`. - Added `tests/test_platform_stabilization_functional.py` covering real FastAPI health/compression routes, successful compression metrics, timeout fail-open speed, and a real JSON tool payload that reduces tokens. - Added `docs/platform-feature-matrix.json` and `docs/platform-stabilization.md` for Linux/macOS/Windows hardening coverage and known gaps. - Strengthened matrix tests so cited local test/workflow paths must exist. ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [x] Type checking passes (`mypy headroom`) - [x] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text # Clean source tree without copied Rust extension: functional module is skipped locally, as CI copies _core from the built wheel. > python -m pytest tests/test_install tests/test_cli/test_install_cli.py tests/test_platform_feature_matrix.py tests/test_platform_stabilization_functional.py -q collected 120 items / 1 skipped 119 passed, 2 skipped in 17.08s > python -m ruff check headroom/proxy/handlers/openai.py tests/test_platform_stabilization_functional.py tests/test_platform_feature_matrix.py All checks passed! # Local Windows compiled-core proof: > python -m maturin build --profile ci --out dist-local Built wheel for abi3 Python >= 3.10 to dist-local\headroom_ai-0.29.0-cp310-abi3-win_amd64.whl # Copied _core.pyd from the wheel into headroom/ for local route execution, then: > python -m pytest tests/test_platform_stabilization_functional.py -q collected 4 items 4 passed in 6.71s > python -m pytest tests/test_install tests/test_cli/test_install_cli.py tests/test_platform_feature_matrix.py -q collected 120 items 119 passed, 1 skipped in 17.16s Commit hooks: Sync plugin versions.....................................................Passed check for merge conflicts................................................Passed ruff.....................................................................Passed ruff-format..............................................................Passed mypy.....................................................................Passed ``` ## Real Behavior Proof - Environment: Windows 11, PowerShell, Python 3.13.13, worktree `C:\git\headroom-stabilization` on branch `jd/cross-platform-stabilization`. - Exact command / steps: built the Windows wheel with `maturin`, extracted `_core.pyd`, ran the new FastAPI route tests and install/matrix tests listed above, then removed generated artifacts before committing. - Observed result: direct start paths now no-op when healthy, skip spawning when the start lock is contended, and stop a wedged runtime before restart. `/v1/compress` now returns original messages quickly on timeout instead of a 503. The real JSON tool-payload smoke test returns `tokens_before > tokens_after`, `tokens_saved > 0`, `compression_ratio < 1.0`, and non-empty transforms through the public route. - Not tested: full native Windows persistent process e2e remains blocked by the upstream CRT/wheel issue already documented in workflows and in the matrix. No real OS service was installed locally; service manager behavior is covered by argument-level unit tests. ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Checklist - [x] My code follows the project's style guidelines - [x] I have performed a self-review of my code - [x] I have commented my code, particularly in hard-to-understand areas - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] I have updated the CHANGELOG.md if applicable ## Screenshots (if applicable) N/A ## Additional Notes CHANGELOG is not updated because this is an unreleased hardening/test/documentation pass. The platform matrix intentionally records partial/blocked Windows/macOS e2e gaps instead of claiming full coverage where the repo cannot currently run it. |
||
|
|
28ca61fc9d
|
fix: patch nltk vulnerability (CVE-2026-54293) (#1929)
## Description
Updates the locked `nltk` package from 3.9.4 to 3.10.0 to address
CVE-2026-54293, reported by OrbisAI Security as an information
disclosure/path traversal issue in `nltk.data.load()`.
Closes #
## 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
- Updated the `nltk` lockfile entry from 3.9.4 to 3.10.0.
- Added the new locked `defusedxml` dependency required by `nltk`
3.10.0.
- Added an explicit `nltk>=3.10.0` uv constraint so future lock
refreshes cannot regress below the fixed version.
- Updated the benchmark-extra comment now that the nltk CVE has an
upstream fixed release.
## Testing
<!-- Check what you actually ran, then paste the real command output
below. -->
- [ ] Unit tests pass (`pytest`)
- [ ] Linting passes (`ruff check .`)
- [ ] Type checking passes (`mypy headroom`)
- [ ] New tests added for new functionality
- [x] Manual testing performed
### Test Output
```text
uv lock --locked
Resolved 257 packages in 1ms
uv run --extra benchmark python -c "import importlib.metadata as md; print('lm-eval', md.version('lm-eval')); print('rouge-score', md.version('rouge-score')); print('nltk', md.version('nltk'))"
lm-eval 0.4.10
rouge-score 0.1.2
nltk 3.10.0
```
## Real Behavior Proof
- Environment: GitHub pull request diff for
headroomlabs-ai/headroom#1929.
- Exact command / steps: Reviewed the PR diff and ran the focused uv
lock/import checks listed above.
- Observed result: The lockfile now points at nltk 3.10.0 artifacts,
includes the new defusedxml dependency, and records the nltk>=3.10.0
resolver constraint.
- Not tested: Full local test suite was not run for this lockfile-only
security update.
## Review Readiness
- [x] I have performed a self-review
- [x] This PR is ready for human review
## Checklist
- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] I have updated the CHANGELOG.md if applicable
## Screenshots (if applicable)
N/A.
## Additional Notes
Original automated security context from OrbisAI Security:
- CVE: CVE-2026-54293
- Severity: HIGH
- Scanner: trivy
- Rule: `CVE-2026-54293`
- File: `uv.lock`
- Assessment: Likely exploitable
- Description: nltk information disclosure via path traversal in
`nltk.data.load()`.
---------
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
|
||
|
|
abc557a5dc
|
[codex] Document local LLM prefill benchmarking (#1396)
## Summary - add a Local LLM Prefill Benchmark docs page for baseline-vs-optimized proxy testing - document the `--no-optimize` baseline, optimized rerun, dashboard comparison, and optional `--learn` condition - link the workflow from the proxy and benchmarks docs ## Context This captures the local-inference workflow shown in Joe Maddalone's June 2026 Headroom demo: Headroom can improve local model prompt-processing time by sending fewer prompt tokens, even when token cost is not the main concern. ## Validation - `npm --prefix docs run types:check` - `npm --prefix docs run build` ## Notes - This PR is independent from #1395, which covers Codex audit/maturation evidence. Co-authored-by: Robert Briscoe <robert@briscoe.dev> |
||
|
|
d05802b620
|
fix: emit unknown Anthropic content blocks verbatim in buffered-to-SSE conversion (#1825)
## Description
Unknown Anthropic content block types are now emitted verbatim inside
`content_block_start` during buffered-to-SSE conversion instead of
raising `ValueError`. The block-start loop in
`StreamingMixin._response_to_sse`
(`headroom/proxy/handlers/streaming.py`) previously handled only `text`,
`tool_use`, `thinking`, and `redacted_thinking`; any other type fell
through to a hard raise, which turned a fully-generated upstream
response into an HTTP 502.
Closes #1806
## 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
- Emit unknown content block types, including `server_tool_use`,
`server_tool_result`, `mcp_tool_use`, and future Anthropic block types,
verbatim in `content_block_start` with no delta.
- Preserve main's explicit `server_tool_use` support and newer buffered
CCR/thinking regression coverage after merging current main.
- Keep `content_block_delta` generation gated on known delta-capable
block types, so unknown blocks do not produce spurious deltas.
## Testing
- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [ ] Type checking passes (`mypy headroom`)
- [x] New tests added for new functionality
- [ ] Manual testing performed
### Test Output
```text
uv run --with pytest python -m pytest tests/test_sse_thinking_blocks.py -q
12 passed, 1 warning
uv run --with ruff==0.15.17 ruff check headroom/proxy/handlers/streaming.py tests/test_sse_thinking_blocks.py
All checks passed!
```
## Real Behavior Proof
- Environment: Windows worktree `C:\git\headroom-governance-main`, PR
head `
|
||
|
|
d2a86b5909
|
fix(proxy): strip duplicated upstream server headers (#1828)
## Description Fixes duplicated upstream server headers emitted by the proxy when forwarding responses. ## 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 - Adjust proxy response forwarding so upstream server headers are not duplicated. - Preserve the intended response-header behavior while avoiding repeated header values. - Keep the change scoped to proxy/header handling. ## 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 the proxy response-header behavior and existing focused coverage for duplicate upstream server headers. - Observed result: The PR implementation prevents duplicated upstream server headers while preserving proxy forwarding behavior. - 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. --------- Co-authored-by: JerrettDavis <mxjerrett@gmail.com> |
||
|
|
98ff203f98
|
ci: allow PyPI deps during CPU torch install (#1930)
## Description Fixes the CI failure exposed on the ` |
||
|
|
8527b910dc | test(litellm): remove unused pytest import | ||
|
|
2d418335a1 | ci: preserve merge labels while state is unknown | ||
|
|
595b709a5b | ci: keep ready label off changes-requested PRs | ||
|
|
1deb947ac1
|
fix(proxy): hoist ccr_workspace_key default so /v1/messages survives CCR-inject off (#1096)
## Summary
`handle_anthropic_messages` only assigns `ccr_workspace_key` /
`ccr_workspace_label` **inside** the
`if (ccr_inject_tool or ccr_inject_system_instructions) and not
_bypass:` block (around `headroom/proxy/handlers/anthropic.py:1302`),
but references `ccr_workspace_key` **unconditionally** in the
proactive-expansion gate at
`headroom/proxy/handlers/anthropic.py:1394-1397`:
```python
if (
self.ccr_context_tracker
and self.config.ccr_proactive_expansion
and ccr_workspace_key # <-- unbound when the inject block was skipped
):
```
Running the proxy with `--no-ccr-inject-tool` and the default
`ccr_inject_system_instructions=False` (a real, supported configuration)
skips the assignment. With `ccr_context_tracking=True` and
`ccr_proactive_expansion=True` (both defaulting to `True`), the gate is
reached and raises `UnboundLocalError`, which FastAPI surfaces as HTTP
500 on **every** `/v1/messages` request. The Claude Code SDK retries ~10
times (`type=system/api_retry`) and then emits the upstream error as the
assistant reply (`API Error: 500 Internal Server Error`), which looked
exactly like an Anthropic outage from the agent side.
Fix: hoist `ccr_workspace_key, ccr_workspace_label = None, None` to
before the gated block. The downstream uses already treat a falsy key as
"workspace unresolved" — `track_compression` short-circuits to the
existing `elif self.ccr_context_tracker and not ccr_workspace_key:` log
line, and the proactive-expansion gate stays closed via short-circuit
`and`. Behavior with CCR inject enabled is byte-identical.
The bug appears to have been introduced by #500 (workspace scoping). I
traced it after my NanoClaw containers started returning `API Error: 500
Internal Server Error` for every scheduled run — `journalctl --user -u
headroom` showed the traceback.
## Reproduction
Failing test in `tests/test_anthropic_ccr_workspace_unbound.py` mirrors
the deployment config:
```python
config = ProxyConfig(
ccr_inject_tool=False, # user passed --no-ccr-inject-tool
ccr_inject_system_instructions=False, # default
ccr_context_tracking=True, # default — installs the tracker
ccr_proactive_expansion=True, # default — reaches the gate
...
)
```
Before the fix:
```
headroom/proxy/handlers/anthropic.py:1397: in handle_anthropic_messages
and ccr_workspace_key
^^^^^^^^^^^^^^^^^
E UnboundLocalError: cannot access local variable 'ccr_workspace_key' where it is not associated with a value
FAILED tests/test_anthropic_ccr_workspace_unbound.py::test_proactive_expansion_does_not_raise_when_ccr_inject_disabled
```
After the fix:
```
tests/test_anthropic_ccr_workspace_unbound.py . [100%]
1 passed
```
## Real behavior proof
**Setup tested on:** Ubuntu 24.04 on WSL2 (NUC15CRH), Python 3.12.3,
`headroom-ai==0.25.0` venv at `/home/adam/headroom-env/`, service
started by user-level systemd unit:
```
headroom proxy --host 0.0.0.0 --port 8787 --mode token \
--no-ccr-inject-tool --no-ccr-marker --no-telemetry --code-aware
```
Provider: Anthropic via direct `CLAUDE_CODE_OAUTH_TOKEN` injection from
the calling container (NanoClaw / Claude Agent SDK on
`claude-opus-4-8`).
**Before the patch** — every request through the proxy 500ed:
```
$ curl -sS -m 5 -o /tmp/r -w "HTTP %{http_code}\n" -X POST http://localhost:8787/v1/messages \
-H "x-api-key: placeholder" -H "anthropic-version: 2023-06-01" \
-H "content-type: application/json" \
-d '{"model":"claude-opus-4-8","max_tokens":10,"messages":[{"role":"user","content":"hi"}]}'
HTTP 500
$ head -c 40 /tmp/r
Internal Server Error
$ journalctl --user -u headroom -n 50 --no-pager | grep -A1 ccr_workspace_key | head
and ccr_workspace_key
^^^^^^^^^^^^^^^^^
UnboundLocalError: cannot access local variable 'ccr_workspace_key' where it is not associated with a value
```
NanoClaw container logs showed the SDK's 10 `system/api_retry` events
then surfacing `API Error: 500 Internal Server Error` as the assistant
result.
**After the patch** (applied in place to the installed file, service
restarted):
```
$ systemctl --user restart headroom
$ TOKEN=$(jq -r .claudeAiOauth.accessToken ~/.claude/.credentials.json)
$ curl -sS -m 30 -o /tmp/r -w "HTTP %{http_code}\n" -X POST http://localhost:8787/v1/messages \
-H "Authorization: Bearer $TOKEN" -H "anthropic-version: 2023-06-01" \
-H "content-type: application/json" \
-d '{"model":"claude-opus-4-8","max_tokens":20,"messages":[{"role":"user","content":"reply with just the word pong"}]}'
HTTP 429
$ cat /tmp/r
{"type":"error","error":{"type":"rate_limit_error","message":"Error"},"request_id":"req_011Cc9PSZHi4QssEKLhZX5uq"}
```
The local 500 is gone — the proxy now forwards cleanly and surfaces
upstream's real response (here a 429 because the retry storm had been
hammering the account for hours; the shape of the response, and the
presence of an `anthropic-request_id`, confirms the proxy is no longer
crashing on its own code path).
Then `journalctl --user -u headroom --since "5 min ago" | grep -iE
'unbound|traceback'` returned no new occurrences after the restart at
12:30 PDT.
**What I did *not* test:**
- The `_bypass=True` path (same fix protects it, but I did not exercise
it end-to-end).
- The CCR-inject-on path — relied on the existing
`tests/test_proxy_anthropic_cache_stability.py` and
`tests/test_proxy_system_prompt_immutable.py` suites passing (they do;
ran `uv run pytest tests/test_proxy_anthropic_cache_stability.py
tests/test_anthropic_pre_upstream_backpressure.py
tests/test_anthropic_stage_timings.py
tests/test_provider_proxy_routes.py
tests/test_proxy_system_prompt_immutable.py` → 68 passed).
## Test plan
- [x] `uv run pytest tests/test_anthropic_ccr_workspace_unbound.py` —
fails on `main`, passes on this branch.
- [x] `uv run pytest tests/test_proxy_anthropic_cache_stability.py
tests/test_anthropic_pre_upstream_backpressure.py
tests/test_anthropic_stage_timings.py
tests/test_provider_proxy_routes.py
tests/test_proxy_system_prompt_immutable.py` — 68 passed.
- [x] `uv run ruff check` / `uv run ruff format --check` on modified
files — clean.
- [x] Live proxy verified against the configuration that reproduced the
bug.
Co-authored-by: Adam Barnum <adamleebarnum@gmail.com>
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
|
||
|
|
772adc93b2
|
fix(scripts): rename .releaseetadata to .releasemetadata (#1246)
## Description Fixes a typo in the release metadata filename written by `scripts/version-sync.py`. The file was being created as `.releaseetadata` (double `e`) instead of `.releasemetadata`. Any downstream tooling or developer looking for the artifact by its correct name would not find it. Closes # ## 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 - `scripts/version-sync.py`: corrected the filename in `write_release_metadata()` — both the docstring and the `metadata_path` assignment. - `scripts/tests/test_version_sync.py`: updated 3 test assertions to reference `.releasemetadata`. ## Testing - [x] 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 $ uv run python -m pytest scripts/tests/test_version_sync.py -q ============================= test session starts ============================== platform linux -- Python 3.14.0, pytest-9.1.1, pluggy-1.6.0 rootdir: /home/sepurisaikrishna/Documents/calude-here/headroom configfile: pyproject.toml collected 6 items scripts/tests/test_version_sync.py ...... [100%] =============================== warnings summary =============================== PytestConfigWarning: Unknown config option: asyncio_mode -- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html ========================= 6 passed, 1 warning in 1.00s ========================= $ git diff --check origin/main..HEAD # no output; command exited 0 ``` ## Real Behavior Proof - Environment: Linux, Python 3.14.0, uv-managed .venv, branch based on current origin/main. - Exact command / steps: grep -r "releaseetadata" scripts/ before the fix returns hits; after the fix returns nothing. Confirmed .releasemetadata is written correctly by test_release_metadata_written. - Observed result: all 6 test_version_sync.py tests pass with the corrected filename. - Not tested: full repository pytest, ruff, and mypy — this is a one-line spelling fix with no logic changes. ## 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 - [ ] 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 - The typo was consistent across implementation and tests, so all tests passed before this fix with the wrong name. The fix corrects both the code and the test expectations together. - No production behaviour changes the file is written but not yet consumed by any workflow step. |
||
|
|
9be727de68
|
fix(litellm): inherit CustomLogger so future hooks don't crash proxy (#1114) (#1391)
## Summary Fixes #1114 — LiteLLM 1.89.x added `async_post_call_success_hook` and started calling it after every successful completion. `HeadroomCallback` was a plain `object` subclass with no such method, causing: ``` AttributeError: type object 'HeadroomCallback' has no attribute 'async_post_call_success_hook' ``` This crashed the LiteLLM proxy on every successful API call. ### Root cause ```python class HeadroomCallback: # plain object — no-op hooks not inherited ... ``` ### Fix Inherit from `litellm.integrations.custom_logger.CustomLogger` which provides no-op defaults for every hook it defines. Future additions to `CustomLogger` will be covered automatically. ```python try: from litellm.integrations.custom_logger import CustomLogger as _CustomLogger except ImportError: _CustomLogger = object # litellm not installed — graceful fallback class HeadroomCallback(_CustomLogger): ... def __init__(self, ...): super().__init__() ... ``` ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) ## Changes Made - `headroom/integrations/litellm_callback.py` — inherit `CustomLogger`; add `super().__init__()` - `tests/test_litellm_callback.py` — 7 tests: instantiation, `async_post_call_success_hook` present + callable + no-op, all current hooks present, pre-call hook still works ## Real behavior proof **Setup:** Python 3.13, litellm 1.89.1, headroom-ai 0.27.0-dev **Steps after patch:** ```bash python3 -c " from headroom.integrations.litellm_callback import HeadroomCallback import asyncio cb = HeadroomCallback() # Simulate what litellm proxy calls on success asyncio.run(cb.async_post_call_success_hook(data={}, user_api_key_dict={}, response=None)) print('OK — no AttributeError') " ``` **After-fix evidence:** Runs without exception. Before fix: `AttributeError: type object 'HeadroomCallback' has no attribute 'async_post_call_success_hook'`. **What I did not test:** Live LiteLLM proxy with YAML config (no LiteLLM proxy running in test env); tested via unit tests and direct Python instantiation. ## Test Results ``` tests/test_litellm_callback.py 7/7 passed ``` 🤖 Generated with [Claude Code](https://claude.com/claude-code) |
||
|
|
7836aea2be
|
fix(proxy): preserve upstream 5xx status on retry exhaustion (#1570)
## What When the upstream returns a retryable 5xx (529 Overloaded, 503), `_retry_request` retried up to the cap and then raised, which the caller collapsed into a generic 502. That hides the retryable signal: clients see a 502 and give up instead of applying their own overload backoff. On exhaustion, return the last upstream response (preserving its status and body) when one is available. Connection and timeout errors still raise — only an `HTTPStatusError` carrying a real upstream response is surfaced. ## Why this scope `_retry_request` is provider-agnostic, so this fix applies uniformly to all providers (no per-handler change needed). It is purely a returned-status correctness fix and does not touch request accounting — a separate change handles counting an exhausted 5xx as a failed request across all provider handlers. ## Verification `tests/test_retry_preserve_upstream_status.py`: 529/503 status+body preservation, 4xx no-retry, connect-error still raises, success passthrough. Against unpatched main the 503-preservation test fails (collapses to 502); with the fix all pass. Addresses #1568. |
||
|
|
e365ad7152
|
fix(proxy): count exhausted upstream 5xx as failed across all providers (#1571)
## What A companion to the retry-exhaustion change that returns the real upstream 5xx (e.g. 529 Overloaded) instead of a synthetic 502. Once the real 5xx is returned, it flows through the success funnel and is recorded via `record_request`, feeding the savings/cost stats and inflating the save-rate. `RequestOutcome` now carries the upstream `status_code` (default 200). In `emit_request_outcome`, a `status_code >= 500` records a failed request and returns before the savings/cost/log success path. 4xx stay on the normal funnel (client errors the proxy still served). The real status is threaded onto the retry-fed `RequestOutcome` at every provider site: Anthropic (message, batch, passthrough), OpenAI (chat, responses, passthrough), Gemini (generateContent, all-non-text path, countTokens). Sites that cannot carry a 5xx keep the default 200: local cache hits, backend-routed paths that early-return on error, websocket units (no HTTP status), and streaming generators that early-return on `>= 400`. ## Scope notes - **429**: an exhausted 429 (rate limit) currently stays on the success funnel since it is < 500. Extending the failed-accounting to exhausted-429 is a separate follow-up, kept out of this 5xx-scoped change. - **Streaming**: streaming responses return before the retry-exhaustion logic, so they do not receive an exhausted 5xx through this path; `from_stream` is unchanged. ## Verification `tests/test_outcome_records_5xx_as_failed.py` exercises the `>= 500` funnel guard; `tests/test_5xx_accounting_all_providers.py` pins the per-provider contract for each wired site. Against unpatched main the guard test fails (no `status_code` field); with the change all pass. Addresses #1568. Builds on #1570 (preserve-5xx-status): the 503 accounting takes effect once that lands; the 429/529 accounting is independent. |
||
|
|
75fff43eca
|
deps: bump @types/node from 25.5.2 to 26.1.1 in /docs (#1683)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 25.5.2 to 26.1.1. <details> <summary>Commits</summary> <ul> <li>See full diff in <a href="https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node">compare view</a></li> </ul> </details> <br /> Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
e8b66a27e1
|
deps: bump fumadocs-typescript from 4.0.14 to 5.3.0 in /docs (#1684)
Bumps [fumadocs-typescript](https://github.com/fuma-nama/fumadocs) from 4.0.14 to 5.3.0. <details> <summary>Release notes</summary> <p><em>Sourced from <a href="https://github.com/fuma-nama/fumadocs/releases">fumadocs-typescript's releases</a>.</em></p> <blockquote> <h2>fumadocs-typescript@5.3.0</h2> <h3>Default to Base UI</h3> <p>Internal packages & templates now use Base UI rather than Radix UI.</p> <h2>fumadocs-typescript@5.2.7</h2> <h3>Migrate to <code>cnfast</code></h3> <p>Drop <code>tailwind-merge</code>.</p> </blockquote> </details> <details> <summary>Commits</summary> <ul> <li><a href=" |
||
|
|
350daeba73
|
deps: bump @types/node from 22.19.15 to 26.1.1 in /plugins/openclaw (#1685)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 22.19.15 to 26.1.1. <details> <summary>Commits</summary> <ul> <li>See full diff in <a href="https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node">compare view</a></li> </ul> </details> <br /> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
87151952ee
|
deps: bump @types/node from 22.20.0 to 26.1.1 in /plugins/opencode (#1688)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 22.20.0 to 26.1.1. <details> <summary>Commits</summary> <ul> <li>See full diff in <a href="https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node">compare view</a></li> </ul> </details> <br /> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
8879c50dbe
|
fix(adaptive-sizer): char bigrams for spaceless CJK items (#1748)
## Description `compute_unique_bigram_curve` — the adaptive sizer's coverage-curve builder, mirrored in Rust and Python — word-splits each item on whitespace to form word bigrams. A spaceless CJK item has no whitespace, so it collapsed into one `(whole_string, "")` pseudo-bigram: the coverage curve then grew ~1 per item, the kneedle knee detector found no knee, and CJK lists under-compressed. Spaceless CJK items now use character bigrams, producing a real coverage curve. Mirrored byte-exactly in Rust and Python (identical reference-test curve values). Non-CJK items — anything whitespace-bearing or spaceless-ASCII — are byte-identical to before, so the `smart_crusher` parity fixtures are unchanged. ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) ## Changes Made - `crates/headroom-core/src/transforms/adaptive_sizer.rs` + `headroom/transforms/adaptive_sizer.py`: add `is_cjk_char`/`_is_cjk_char` (identical code-point ranges) and a spaceless-CJK character-bigram branch in `compute_unique_bigram_curve`. - Rust unit tests + `tests/test_adaptive_sizer.py`: CJK curve, single-char CJK, ASCII-unchanged, empty-item — the Rust and Python reference values are identical. ## Testing - [x] Unit tests pass (`cargo test` + `pytest`) - [x] Linting passes (`cargo clippy` / `cargo fmt` / `ruff` / `mypy`) - [x] New tests added for new functionality - [x] Manual testing performed (see Real Behavior Proof) ### Test Output ```text $ cargo test -p headroom-core --lib adaptive_sizer test result: ok. 35 passed; 0 failed $ .venv/bin/python -m pytest tests/test_adaptive_sizer.py 20 passed $ .venv/bin/python -m pytest -k "smart_crusher and parity" 18 passed, 6 skipped # non-CJK fixtures unchanged ``` ## Real Behavior Proof - Environment: macOS (Darwin), Rust via cargo, Python in a uv venv, branch `feat/adaptive-sizer-cjk` off `main`. - Exact command / steps: called `compute_unique_bigram_curve` on a CJK list and on ASCII lists, in both implementations. - Observed result: `compute_unique_bigram_curve(["数据库连接失败", "数据库连接成功"])` returns `[6, 8]` in **both** Rust and Python (before: ~`[1, 2]` — one pseudo-bigram per item, no coverage signal). ASCII curves are unchanged: `["the cat", "the dog", "a fish"]` → `[1, 2, 3]`. The `smart_crusher` parity suite (all-ASCII fixtures) stays green, confirming non-CJK output is byte-identical. - Byte-exact parity: the Rust reference test (`vec![6, 8]`) and the Python test (`[6, 8]`) use the same inputs and the same expected values, so the two implementations are pinned to agree. ## 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 — N/A (internal sizing heuristic) - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective - [x] New and existing unit tests pass locally with my changes - [ ] I have updated the CHANGELOG.md — N/A: internal sizing-heuristic fix, no user-facing surface change ## Additional Notes - This is a parity-locked function (Rust and Python must agree byte-for-byte). The fix is CJK-gated, so non-CJK output is byte-identical and the `smart_crusher` parity fixtures need no re-recording. |
||
|
|
985621d60e
|
fix(search-compressor): CJK-aware relevance + harden Rust/Python parity (#1749)
## Description
The search compressor's relevance scorer (`score_matches`, present in
both the Rust runtime path and the Python legacy mirror) split the query
on whitespace. A spaceless CJK query therefore matched a result line
only when the WHOLE query was a literal substring of that line — partial
overlaps never boosted relevant lines, so correct matches got dropped
when the result set was over budget.
This adds CJK character bigrams to the query match set, so a longer CJK
query boosts lines that share a substring. It also fixes two latent
Rust/Python parity divergences the ASCII-only fixtures had masked:
- **Length filter**: Rust counted word length in BYTES (`w.len()`),
Python in codepoints (`len(w)`), so a CJK word crossed the `> 2`
threshold differently. Rust now uses `chars().count()`.
- **Dedup**: Rust collected words into a `Vec` (no dedup), Python into a
`set`, so a repeated query word double-counted in Rust. Rust now uses a
`BTreeSet`.
Both scorers are byte-exact now; non-CJK output is unchanged (the 53
existing tests and the parity fixtures stay green).
## Type of Change
- [x] Bug fix (non-breaking change that fixes an issue)
## Changes Made
- `crates/headroom-core/src/transforms/search_compressor.rs` +
`headroom/transforms/search_compressor.py`: add
`is_cjk_char`/`_is_cjk_char` and `cjk_bigrams`/`_cjk_bigrams` (identical
ranges + logic), union CJK bigrams into the query match set, and align
the Rust word set to Python (`chars().count()` length, `BTreeSet`
dedup).
- `tests/test_search_compressor_cjk.py` + a Rust unit test: CJK bigram
extraction (same input/expected in both languages) and a CJK query
boosting a partially-overlapping line.
- Corrected a stale `_score_matches` docstring that referenced a
non-existent parity assertion; it now states honestly how the two sides
are pinned (test-equal for word-overlap + CJK bigrams; a few error-boost
keywords still diverge, fixed only Rust-side).
## Testing
- [x] Unit tests pass (`cargo test` + `pytest`)
- [x] Linting passes (`cargo clippy` / `cargo fmt` / `ruff` / `mypy`)
- [x] New tests added for new functionality
- [x] Manual testing performed (see Real Behavior Proof)
### Test Output
```text
$ cargo test -p headroom-core --lib search_compressor
test result: ok. 16 passed; 0 failed
$ .venv/bin/python -m pytest tests/test_search_compressor_cjk.py \
tests/test_transforms_search_compressor.py tests/test_search_compressor.py
55 passed # 2 new CJK tests + 53 existing (no regression)
```
## Real Behavior Proof
- Environment: macOS (Darwin), Rust via cargo, Python in a uv venv
(`_core` rebuilt on this branch), branch `feat/search-compressor-cjk`
off `main`.
- Exact command / steps: scored a CJK content line against a longer CJK
query whose whole form is not a substring of the line.
- Observed result: for content `src/a.py:10:认证令牌已过期需要重新登录` and query
`认证令牌缓存淘汰策略` (the whole query is NOT a substring of the line, but its
bigrams are), the line now scores `> 0` (bigrams 认证 / 证令 / 令牌 match);
before, it scored `0`. An ASCII-only line still scores `0`. All 53
existing search-compressor tests are unchanged. `cjk_bigrams("认证令牌")`
returns `{认证, 证令, 令牌}` in **both** Rust and Python.
## 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 — N/A
(internal relevance scoring)
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective
- [x] New and existing unit tests pass locally with my changes
- [ ] I have updated the CHANGELOG.md — N/A: internal relevance-scoring
fix, no user-facing surface change
## Additional Notes
- The two parity divergences (byte-vs-codepoint length, `Vec`-vs-`set`
dedup) were pre-existing and only reachable with non-ASCII or
repeated-word queries — the all-ASCII fixtures never exercised them.
This PR brings both sides back to byte-exact for the word-overlap +
CJK-bigram scoring. The remaining error-boost keyword divergence is
pre-existing (fixed only Rust-side in the 3e.1 port) and is now
documented in the code rather than glossed over.
|
||
|
|
c85731dc23
|
fix(mcp): correct default Claude Code config path in ClaudeRegistrar (#1859)
## Description <!-- Briefly explain the change and why it is needed. --> `ClaudeRegistrar` originally assumed Claude Code's modern per-user MCP config lives at `~/.claude/.claude.json`. On a real Claude Code 2.1.202 install with `CLAUDE_CONFIG_DIR` unset, the actual file is `~/.claude.json`, directly under the home directory. Whenever `claude mcp add` failed for any transient reason and the registrar fell back to writing the config file directly, it wrote to a path Claude Code never reads — registration reported success with no error, but the server silently never became available, and once that wrong file existed, `get_server()` kept reading it back as already-registered, so the registrar never retried. While fixing the path, several related correctness and test-isolation issues in the same file were found and fixed: - Three tests instantiated `ClaudeRegistrar(claude_cli=None)` without `home_dir`, so the legacy config path resolved to the developer's real `~/.claude/mcp.json` — one test was actually deleting a `headroom` entry from it. - `detect()` only checked the legacy `~/.claude` directory, so installs where the `claude` CLI is absent from `PATH` and only the modern `~/.claude.json` exists were treated as not detected, skipping registration entirely. - `unregister_server()` returned early on CLI success without cleaning the legacy config file, so a stale legacy entry could survive a successful `claude mcp remove` and `get_server()` would keep reporting the server as registered. - `_read_server_entry`, `_remove_from_file`, and `_register_via_file` assumed `mcpServers` was always a dict once present; a hand-edited or corrupted config with `mcpServers` set to `null`, a list, or a string crashed with an unhandled `AttributeError`/`TypeError`. Closes # ## 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 - `_resolve_claude_config_dir` now defaults to `home` (the modern config is `~/.claude.json`), keeping the `CLAUDE_CONFIG_DIR` and explicit `config_dir` overrides intact; the legacy `.claude` directory is pinned to `home / ".claude"` independently of where the modern config resolves. - `detect()` also recognizes an install via `self._modern_config.exists()`, not just the legacy directory. - `unregister_server()` always cleans both the modern and legacy config files, even after a successful CLI removal. - `_read_server_entry`, `_remove_from_file`, and `_register_via_file` now validate `mcpServers` is a dict before indexing into it, degrading gracefully instead of crashing on malformed config. - Module and constructor docstrings now state the current config-path facts plainly (paths, and what `CLAUDE_CONFIG_DIR` relocates); the constructor docstring also clarifies that `home_dir`/`config_dir` isolate file-based reads/writes but not CLI subprocess calls. - `tests/test_mcp_registry/test_claude_registrar.py`: corrected path expectations, isolated the three previously-unisolated tests from the real home directory, and added coverage for the modern-config-only detect case, CLI-success-with-stale-legacy-entry, and non-dict `mcpServers` values. - Filed #1861 for a related, currently-unexercised gap: CLI subprocess calls don't honor `home_dir`/`config_dir` overrides. ## 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_mcp_registry/test_claude_registrar.py ============================== 35 passed in 0.08s ============================== $ ruff check headroom/mcp_registry/claude.py tests/test_mcp_registry/test_claude_registrar.py All checks passed! $ mypy headroom/mcp_registry/claude.py Success: no issues found in 1 source file ``` ## Real Behavior Proof - Environment: Claude Code 2.1.202 (macOS, Darwin 25.5.0), `CLAUDE_CONFIG_DIR` unset. - Exact command / steps: Injected a uniquely-named probe server into **only** `~/.claude/.claude.json` (old assumed path) and ran `claude mcp list`; then injected a different probe into **only** `~/.claude.json` (corrected path) and ran `claude mcp list` again; restored both files afterward. 1. `ZZZ_nested_probe` written to `~/.claude/.claude.json` only → `claude mcp list`. 2. `ZZZ_flat_probe` written to `~/.claude.json` only → `claude mcp list`. - Observed result: The nested-path probe (`ZZZ_nested_probe`) was **not** recognized by `claude mcp list` — Claude Code ignores `~/.claude/.claude.json`. The flat-path probe (`ZZZ_flat_probe`) **was** recognized and listed. This confirms `~/.claude.json` is the file Claude Code actually reads. Both config files were restored to their original state after the test. - Not tested: older Claude Code versions (< 2.1.202); Windows/Linux path resolution (logic is platform-agnostic via `pathlib`, but only macOS was exercised); the CLI-subprocess env-isolation gap tracked in #1861. ## 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 — CLI/config-path change with no UI surface. ## Additional Notes - The `CLAUDE_CONFIG_DIR=~/.claude` configuration still correctly resolves the modern config to `~/.claude/.claude.json` via the unchanged env override — only the default (env unset) changed. - Any machine that hit the original bug may have a stale `~/.claude/.claude.json` written by the old fallback; it is harmless and can be deleted. - #1861 tracks a related gap (CLI subprocess env isolation) that isn't exercised by any current production call site. |