Commit graph

2001 commits

Author SHA1 Message Date
Abhay Singh
27ddde1f5e
fix(transforms/code): coerce language aliases instead of raising (#1975)
## Description

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

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

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

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

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

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

## Fix

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

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

## Type of Change

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

## Changes Made

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

## Testing

- [x] New regression tests added
(`tests/test_code_compressor_language_alias.py`)
- [x] Linting passes (`ruff check`) and formatting is clean (`ruff
format --check`)
- [ ] Full `pytest` deferred to CI (local-OOM reason below).

```text
$ uv run ruff check headroom/transforms/code_compressor.py tests/test_code_compressor_language_alias.py
All checks passed!
```

## Real Behavior Proof

- Environment: Windows 11, Python 3.10, headroom from this branch.
Importing `headroom` pulls in the torch/transformers stack and a full
`pytest` gets OOM-killed on this box, so I verified the coercion logic
with a dependency-free script (replicating the enum + helper) and left
the full pytest to CI.
- Exact command / steps: ran the common aliases and the canonical values
through both the old `CodeLanguage(value.lower())` construction and the
new `coerce_language()`.
- Observed result: the old construction raises `ValueError` on every
alias (the crash / silent-skip); the new helper maps them and never
raises:

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

- Not tested: running a full mixed-content document with ` ```js `
fences through a booted compression pipeline (needs the heavy stack).
The unit tests exercise the coercion directly and the
`compress(language="js")` entry point. Full local `pytest` deferred to
CI (OOM, per above).

## Review Readiness

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

## Checklist

- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes — ran
lint + a standalone logic check; full pytest deferred to CI (local OOM,
disclosed above)
- [x] I have updated the CHANGELOG.md if applicable

## Additional Notes

- No new dependencies; a small lookup table plus a helper and a
call-site change.
- @JerrettDavis tagging you — this one silently disables code-aware
compression for the most common fence tags (`js`/`ts`/`py`), so it may
be worth a look when you have a moment.
2026-07-10 23:57:33 -05:00
JD Davis
69fd2189a3
Extract request limit policy (#1982)
## Description

Extracts request/stream limit validation from `helpers.py` into
`headroom.proxy.request_limit_policy`. The helpers still read
environment variables at request time, but validation of SSE event size
and body-too-large status values is now pure and directly tested.

Closes #

## Type of Change

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

## Changes Made

- Added `request_limit_policy.py` for resolving SSE event max bytes and
body-too-large HTTP status values.
- Kept `helpers.get_sse_event_max_bytes` and
`helpers.get_body_too_large_status` reading env vars and delegating to
the pure policy.
- Added direct tests for defaults, valid override values, and invalid
values.
- Carried forward the LiteLLM callback compatibility shim needed for
current mypy on `main`.

## Testing

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

### Test Output

```text
python -m pytest tests\test_request_limit_policy.py
10 passed in 0.17s

python -m ruff check .
All checks passed!

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

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

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

## Real Behavior Proof

- Environment: Windows, Python 3.13.13, branch
`jd/architecture-slice-31`.
- Exact command / steps: ran focused request-limit policy tests, ruff,
ruff format check, mypy, and staged gitleaks scan.
- Observed result: limit validation behavior is directly covered and
local lint/type/security checks pass.
- Not tested: live proxy request rejection; existing helper entry points
remain intact.

## Review Readiness

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

## Checklist

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

## Screenshots (if applicable)

N/A

## Additional Notes

Documentation and changelog updates are N/A for this internal
architecture-only refactor. The push reported existing default-branch
Dependabot alerts; no staged secret leaks were found for this PR.
2026-07-10 23:55:11 -05:00
JD Davis
094a53c047
refactor(proxy): isolate output effort policy (#1961)
## Description

Extracts provider-neutral output effort decisions into a pure
`output_effort_policy` module. `output_shaper` still owns request
mutation and labels, while the rank comparisons, legacy thinking clamp,
and OpenAI text verbosity eligibility now live behind small
deterministic functions.

Closes #

## Type of Change

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

## Changes Made

- Added `headroom.proxy.output_effort_policy` for effort lowering,
legacy thinking budget clamping, and OpenAI text verbosity decisions.
- Updated `output_shaper` to delegate those pure decisions while
preserving existing labels and request mutation behavior.
- Added focused policy tests for effort rank transitions, thinking clamp
boundaries, and verbosity creation/lowering.
- Included the current LiteLLM callback signature compatibility shim
required for repo-wide mypy on main-based slices.

## Testing

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

### Test Output

```text
python -m pytest tests/test_output_effort_policy.py tests/test_output_shaper.py tests/test_litellm_callback.py -q
56 passed in 6.34s

python -m ruff check .
All checks passed!

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

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

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

## Real Behavior Proof

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

## Review Readiness

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

## Checklist

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

## Screenshots (if applicable)

N/A

## Additional Notes

Documentation and changelog updates are not applicable for this internal
architecture slice. PR-specific GHAS checks will be monitored after
opening.
2026-07-10 23:53:00 -05:00
JD Davis
b1e871d51c
refactor(proxy): isolate memory rank policy (#1960)
## Description

Extracts the proxy memory ranking formulas into a pure
`memory_rank_policy` module and keeps `MemoryCandidate` /
`RecencyBoostRanker` as the public adapter-facing API. Also preserves
backend memory IDs when ranked candidates are rebuilt, so downstream
memory update/delete handles survive the ranking boundary.

Closes #

## Type of Change

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

## Changes Made

- Added `headroom.proxy.memory_rank_policy` for timestamp parsing,
recency factor calculation, and score boosting.
- Updated `RecencyBoostRanker` to delegate policy math while preserving
the existing public API.
- Preserved `MemoryCandidate.id` when rank output candidates are
rebuilt.
- Added focused policy tests plus an ID-preservation regression test.
- Included the current LiteLLM callback signature compatibility shim
required for repo-wide mypy on main-based slices.

## Testing

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

### Test Output

```text
python -m pytest tests/test_memory_rank_policy.py tests/test_memory_ranker.py tests/test_litellm_callback.py -q
32 passed in 6.22s

python -m ruff check .
All checks passed!

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

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

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

## Real Behavior Proof

- Environment: Windows, Python 3.13.13, local worktree based on
`headroomlabs/main`.
- Exact command / steps: targeted pytest, ruff, ruff format check,
repo-wide mypy, staged gitleaks scan.
- Observed result: memory rank policy/ranker/callback tests pass; static
checks pass; no staged secrets detected.
- Not tested: full provider/API integration; this slice only changes
pure policy delegation and candidate shape preservation.

## Review Readiness

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

## Checklist

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

## Screenshots (if applicable)

N/A

## Additional Notes

Documentation and changelog updates are not applicable for this internal
architecture slice. PR-specific GHAS checks will be monitored after
opening.
2026-07-10 23:51:51 -05:00
JD Davis
1c1e360112
refactor(proxy): isolate project attribution policy (#1957)
## Description

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

Closes #

## Type of Change

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

## Changes Made

- Added `headroom.proxy.project_policy` for pure project attribution
header/path/base-URL helpers.
- Updated `headroom.proxy.project_context` to re-export the pure helpers
and retain only request context binding and ASGI scope mutation.
- Added direct tests for the extracted project attribution policy
boundary.
- Kept the LiteLLM callback compatibility shim required for repo-wide
type checking on fresh branches.

## Testing

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

### Test Output

```text
python -m pytest tests/test_project_policy.py tests/test_proxy_project_savings.py tests/test_litellm_callback.py tests/test_compress_api.py::TestLiteLLMCallback -q
29 passed in 13.70s

python -m ruff check .
All checks passed!

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

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

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

## Real Behavior Proof

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

## Review Readiness

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

## Checklist

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

## Screenshots (if applicable)

N/A

## Additional Notes

Documentation and changelog updates are not applicable for this internal
refactor. GitHub reported existing Dependabot alerts on the default
branch during push; this PR does not change dependencies, and the staged
secret scan is clean.
2026-07-10 23:50:27 -05:00
JD Davis
740fb9bc16
refactor(cache): isolate semantic key policy (#1953)
## Description

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

Closes #

## Type of Change

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

## Changes Made

- Added `headroom.proxy.semantic_cache_key` for pure cache-control
stripping and semantic cache key construction.
- Updated `SemanticCache._compute_key` to delegate to the extracted
policy while preserving the local `_strip_cache_control` compatibility
alias.
- Added direct tests for the extracted semantic-cache key policy.
- Kept the LiteLLM callback compatibility shim required for repo-wide
type checking on fresh branches.

## Testing

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

### Test Output

```text
python -m pytest tests/test_proxy_semantic_cache_key_policy.py tests/test_proxy_semantic_cache_key.py tests/test_proxy_semantic_cache_key_integration.py tests/test_proxy_openai_cache_key_integration.py tests/test_litellm_callback.py tests/test_compress_api.py::TestLiteLLMCallback -q
46 passed in 11.83s

python -m ruff check .
All checks passed!

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

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

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

## Real Behavior Proof

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

## Review Readiness

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

## Checklist

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

## Screenshots (if applicable)

N/A

## Additional Notes

Documentation and changelog updates are not applicable for this internal
refactor. GitHub reported existing Dependabot alerts on the default
branch during push; this PR does not change dependencies, and the staged
secret scan is clean.
2026-07-10 23:49:27 -05:00
JD Davis
ea1951508b
refactor(proxy): isolate rate limit policy (#1954)
## Description

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

Closes #

## Type of Change

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

## Changes Made

- Added `headroom.proxy.rate_limit_policy` for pure token-bucket
calculations.
- Updated `TokenBucketRateLimiter` to delegate refill, consume, and
stale-key selection to the extracted policy.
- Added direct tests for the rate-limit policy boundary.
- Kept the LiteLLM callback compatibility shim required for repo-wide
type checking on fresh branches.

## Testing

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

### Test Output

```text
python -m pytest tests/test_rate_limit_policy.py tests/test_proxy_healthchecks.py tests/test_litellm_callback.py tests/test_compress_api.py::TestLiteLLMCallback -q
27 passed in 17.82s

python -m ruff check .
All checks passed!

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

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

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

## Real Behavior Proof

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

## Review Readiness

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

## Checklist

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

## Screenshots (if applicable)

N/A

## Additional Notes

Documentation and changelog updates are not applicable for this internal
refactor. GitHub reported existing Dependabot alerts on the default
branch during push; this PR does not change dependencies, and the staged
secret scan is clean.
2026-07-10 23:47:33 -05:00
JD Davis
c20f3b1c04
refactor(memory): isolate injection decision policy (#1952)
## Description

Extracts the memory injection decision precedence and skip-reason tag
stamping into a pure policy module while preserving the public
`MemoryDecision.decide` API used by handlers. This keeps the frozen
decision value type separate from the gate policy it wraps.

Closes #

## Type of Change

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

## Changes Made

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

## Testing

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

### Test Output

```text
python -m pytest tests/test_memory_decision_policy.py tests/test_memory_decision.py tests/test_memory_invariants.py tests/test_litellm_callback.py tests/test_compress_api.py::TestLiteLLMCallback -q
37 passed in 6.74s

python -m ruff check .
All checks passed!

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

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

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

## Real Behavior Proof

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

## Review Readiness

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

## Checklist

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

## Screenshots (if applicable)

N/A

## Additional Notes

Documentation and changelog updates are not applicable for this internal
refactor. GitHub reported existing Dependabot alerts on the default
branch during push; this PR does not change dependencies, and the staged
secret scan is clean.
2026-07-10 23:45:44 -05:00
JD Davis
235c986c9c
refactor(memory): isolate query construction policy (#1950)
## Description

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

Closes #

## Type of Change

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

## Changes Made

- Added `headroom.proxy.memory_query_policy` for pure retrieval query
source extraction and rendering.
- Updated `MemoryQuery.to_embedding_input` and
`MemoryQuery.from_messages` to delegate to the extracted policy.
- Added direct tests for the policy boundary.
- Kept the LiteLLM callback compatibility shim required for repo-wide
type checking on fresh branches.

## Testing

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

### Test Output

```text
python -m pytest tests/test_memory_query_policy.py tests/test_memory_query.py tests/test_memory_invariants.py tests/test_litellm_callback.py tests/test_compress_api.py::TestLiteLLMCallback -q
30 passed in 6.69s

python -m ruff check .
All checks passed!

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

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

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

## Real Behavior Proof

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

## Review Readiness

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

## Checklist

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

## Screenshots (if applicable)

N/A

## Additional Notes

Documentation and changelog updates are not applicable for this internal
refactor. GitHub reported existing Dependabot alerts on the default
branch during push; this PR does not change dependencies, and the staged
secret scan is clean.
2026-07-10 23:44:27 -05:00
JD Davis
c29b4ba84f
refactor(output): isolate savings policy (#1947)
## Description

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

Closes #

## Type of Change

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

## Changes Made

- Added `headroom.proxy.output_savings_policy` for pure savings policy
helpers.
- Re-exported the moved helpers from `headroom.proxy.output_savings` to
keep callers stable.
- Added direct tests for the extracted policy boundary.
- Kept the LiteLLM callback compatibility shim required for repo-wide
type checking on fresh branches.

## Testing

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

### Test Output

```text
python -m pytest tests/test_output_savings_policy.py tests/test_output_savings.py tests/test_litellm_callback.py tests/test_compress_api.py::TestLiteLLMCallback -q
54 passed in 6.42s

python -m ruff check .
All checks passed!

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

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

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

## Real Behavior Proof

- Environment: Windows, Python 3.13.13, local worktree based on
`headroomlabs/main`.
- Exact command / steps: ran the focused pytest set, Ruff lint/format
checks, mypy over `headroom`, and staged gitleaks scan.
- Observed result: all local checks passed; staged secret scan found no
leaks.
- Not tested: full CI matrix and deployment flows; those are covered by
GitHub Actions.

## Review Readiness

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

## Checklist

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

## Screenshots (if applicable)

N/A

## Additional Notes

Documentation and changelog updates are not applicable for this internal
refactor. GitHub reported existing Dependabot alerts on the default
branch during push; this PR does not change dependencies, and the staged
secret scan is clean.
2026-07-10 23:42:11 -05:00
Abhay Singh
b699bedf95
fix(models): version-boundary longest-prefix match in ModelRegistry.get (#1658)
## Description

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

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

Two concrete failures fall out of that:

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

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

## Fix

The fallback now:

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

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

## Type of Change

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

## Changes Made

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

## Testing

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

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

## Real Behavior Proof

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

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

- Not tested: I did not add explicit registry entries for
`gpt-4.1`/`gpt-4.5` (their real windows) — that's a data addition,
separate from this matching-logic fix; today they fall back to the
caller's default, which is honest for an unregistered model and strictly
better than the previous wrong 8192. Full local `pytest` deferred to CI
(OOM, per above).

## Review Readiness

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

## Checklist

- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes — ran
lint + a standalone logic check; full pytest deferred to CI (local OOM,
disclosed above)
- [x] I have updated the CHANGELOG.md if applicable

## Additional Notes

- No new dependencies; pure logic change in one function plus tests.
- Found via a read-through of the registry while looking at how context
limits drive compression decisions.
2026-07-10 23:07:31 -05:00
sgfdhgfnjgmhgmnfd657-sys
48f06caca7
ci: add Windows wheel build job (win_amd64) (#1086)
### Summary

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

This addresses the long-standing missing Windows wheel.

### Changes

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

### Testing

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

### Notes

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

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

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

Closes #1983

## Type of Change

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

## Changes Made

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

## Testing

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

### Test Output

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

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

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

## Real Behavior Proof

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

## Review Readiness

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

## Checklist

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

## Additional Notes

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

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

Closes #

## Type of Change

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

## Changes Made

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

## Testing

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

### Test Output

```text
python -m pytest tests/test_mixed_content_sections.py tests/test_transforms_content_router.py tests/test_litellm_callback.py tests/test_compress_api.py::TestLiteLLMCallback -q
50 passed in 6.82s

python -m ruff check .
All checks passed!

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

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

## Real Behavior Proof

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

## Review Readiness

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

## Checklist

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

## Screenshots (if applicable)

N/A.

## Additional Notes

- Documentation, changelog, and screenshots are N/A for this internal
refactor.
- Manual UI testing is N/A; this is pure transform parsing logic.
- Comment checklist is unchecked because the extracted functions are
small and covered by direct tests.
2026-07-10 19:28:21 -05:00
JD Davis
5a7265daa8
refactor(proxy): isolate auth classification policy (#1945)
## Description

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

Closes #

## Type of Change

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

## Changes Made

- Added `AuthSignals` as the normalized input model for pure auth/client
policy.
- Moved `AuthMode`, subscription UA prefixes, client UA map, Codex
Responses path, auth-mode classification, client classification, and
Codex stamping rules into `headroom.proxy.auth_policy`.
- Kept `headroom.proxy.auth_mode` public API stable by adapting headers
into `AuthSignals` and delegating to policy functions.
- Added direct pure-policy tests for subscription precedence, OAuth/PAYG
token shapes, explicit client override, and Codex Responses stamping.
- Included the LiteLLM callback hook compatibility shim needed for
repo-wide mypy on branches based on `main`.

## Testing

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

### Test Output

```text
python -m pytest tests/test_auth_policy.py tests/test_auth_mode.py tests/test_codex_client_stamp.py tests/test_litellm_callback.py tests/test_compress_api.py::TestLiteLLMCallback -q
48 passed in 6.63s

python -m ruff check .
All checks passed!

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

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

## Real Behavior Proof

- Environment: Windows, Python 3.13.13, local worktree
`C:\git\headroom-pr-slice9`.
- Exact command / steps: Ran the focused pytest suite plus repo-wide
Ruff, format check, and mypy commands listed above.
- Observed result: Existing adapter behavior remains covered by
`tests/test_auth_mode.py` and `tests/test_codex_client_stamp.py`, while
the extracted pure policy is covered by `tests/test_auth_policy.py`.
- Not tested: Full test suite locally; CI will run the full matrix.

## Review Readiness

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

## Checklist

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

## Screenshots (if applicable)

N/A

## Additional Notes

Documentation and changelog updates are not applicable for this internal
refactor. The LiteLLM shim is repeated here because this branch is
intentionally independent from the other open architecture slices and
must stay green against current `main`.
2026-07-10 19:27:08 -05:00
JD Davis
b5aa8a358e
refactor(cache): isolate compression strategy outcomes (#1938)
## Description

Extracts local compression strategy accounting out of
`CompressionFeedback` into a pure cache-domain object. This keeps
strategy counters, retrieval-rate math, pruning, and best-strategy
selection independently testable while preserving the existing
`LocalToolPattern` public API.

Closes #

## Type of Change

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

## Changes Made

- Added `CompressionStrategyOutcomes` as the strategy-outcome domain for
compression/retrieval counters, pruning, retrieval rates, and
recommendation selection.
- Updated `LocalToolPattern` and `CompressionFeedback` to delegate
strategy accounting to that domain while keeping existing fields and
methods intact.
- Added direct unit coverage for strategy outcome math and bounded
pruning behavior.
- Updated the LiteLLM callback hook signature to remain compatible with
current LiteLLM typing and the existing three-argument call shape.

## Testing

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

### Test Output

```text
python -m ruff check .
All checks passed!

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

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

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

## Real Behavior Proof

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

## Review Readiness

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

## Checklist

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

## Screenshots (if applicable)

N/A.

## Additional Notes

- Documentation and changelog are N/A for this internal refactor.
- Manual UI testing is N/A; this is cache feedback and integration
callback logic.
- Comment checklist is unchecked because the extracted object is
intentionally straightforward and covered by tests.
2026-07-10 19:21:47 -05:00
Rod Boev
41af39d769
fix(proxy): preserve terminal tool on Codex Responses (#2000)
## Description

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

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

Closes #1946

## Type of Change

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

## Changes Made

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

## Testing

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

### Test Output

```text
uv sync --extra dev
OK

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

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

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

## Real Behavior Proof

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

## Review Readiness

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

## Checklist

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

## Screenshots (if applicable)

N/A

## Additional Notes

The change is scoped to the exact `terminal` function name in OpenAI
Responses tool-search deferral. It does not change ContentRouter policy,
Anthropic tool deferral, tool schema compaction, or unrelated function
names. Live endpoint acceptance is still an external proof item and is
called out in Real Behavior Proof.
2026-07-10 16:17:25 -07:00
Dima Solodukha
75d786117a
fix(proxy): cache_savings_usd silently zeroes when litellm is unavailable (#2005)
## Description

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

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

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

## Type of Change

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

## Changes Made

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

## Testing

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

### Test Output

```text
$ pytest tests/test_proxy_savings_history.py -k "cache_savings" -q
========================= 3 passed, 1 warning in 0.34s =========================

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

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

## Real Behavior Proof

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

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

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

## Review Readiness

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

## Checklist

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

## Additional Notes

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

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

Closes #

## Type of Change

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

## Changes Made

- Added `ForwardedHeaderInputs` and `ForwardedHeaderResolution` as pure
policy value objects.
- Moved CIDR parsing, IP normalization, trust membership, header
splitting, and forwarded-header resolution into
`headroom.proxy.forwarded_policy`.
- Kept `headroom.proxy.forwarded_headers` as the request adapter with
the same public API and compatibility helper names.
- Added direct tests for trusted, rejected, direct-client, IPv4-mapped
IPv6, and leftmost `X-Forwarded-For` policy behavior.
- Included the LiteLLM callback hook compatibility shim needed for
repo-wide mypy on branches based on `main`.

## Testing

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

### Test Output

```text
python -m pytest tests/test_forwarded_policy.py tests/test_forwarded_headers.py tests/test_litellm_callback.py tests/test_compress_api.py::TestLiteLLMCallback -q
51 passed in 6.36s

python -m ruff check .
All checks passed!

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

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

## Real Behavior Proof

- Environment: Windows, Python 3.13.13, local worktree
`C:\git\headroom-pr-slice8`.
- Exact command / steps: Ran the focused pytest suite plus repo-wide
Ruff, format check, and mypy commands listed above.
- Observed result: The existing request-facing forwarded-header behavior
remains covered by `tests/test_forwarded_headers.py`, while the
extracted pure policy is covered by `tests/test_forwarded_policy.py`.
- Not tested: Full test suite locally; CI will run the full matrix.

## Review Readiness

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

## Checklist

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

## Screenshots (if applicable)

N/A

## Additional Notes

Documentation and changelog updates are not applicable for this internal
refactor. The LiteLLM shim is repeated here because this branch is
intentionally independent from the other open architecture slices and
must stay green against current `main`.
2026-07-10 17:41:38 -05:00
JD Davis
0ce09fb63f
refactor(output): isolate verbosity steering (#1940)
## Description

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

Closes #

## Type of Change

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

## Changes Made

- Added `headroom.proxy.output_steering` for Anthropic system steering
and OpenAI Responses instruction steering.
- Kept existing `headroom.proxy.output_shaper` imports compatible by
re-exporting the moved helpers.
- Added direct tests for replacement, cache-prefix preservation, and
idempotent OpenAI Responses steering.
- Included the LiteLLM callback hook compatibility shim needed for
repo-wide mypy on branches based on `main`.

## Testing

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

### Test Output

```text
python -m pytest tests/test_output_steering.py tests/test_output_shaper.py tests/test_litellm_callback.py tests/test_compress_api.py::TestLiteLLMCallback -q
57 passed in 6.17s

python -m ruff check .
All checks passed!

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

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

## Real Behavior Proof

- Environment: Windows, Python 3.13.13, local worktree
`C:\git\headroom-pr-slice7`.
- Exact command / steps: Ran the focused pytest suite plus repo-wide
Ruff, format check, and mypy commands listed above.
- Observed result: Steering behavior remains covered through the
existing `output_shaper` tests and the new direct `output_steering`
tests.
- Not tested: Full test suite locally; CI will run the full matrix.

## Review Readiness

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

## Checklist

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

## Screenshots (if applicable)

N/A

## Additional Notes

Documentation and changelog updates are not applicable for this internal
refactor. The LiteLLM shim is repeated here because this branch is
intentionally independent from the other open architecture slices and
must stay green against current `main`.
2026-07-10 17:39:39 -05:00
JD Davis
fd5b9e75ad
refactor(ccr): isolate tool call classification (#1937)
## Description
Extracts provider-shaped CCR tool-call extraction and classification
into `headroom.ccr.tool_calls`. `CCRResponseHandler` now delegates
detection/parsing to a pure domain module and stays focused on retrieval
execution and continuation orchestration.

Closes #

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

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

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

### Test Output
```text
python -m pytest tests/test_ccr_tool_calls.py tests/test_ccr_response_handler.py tests/test_ccr_response_handler_extra.py tests/test_ccr_response_handler_openai_responses.py tests/test_ccr_batch_processor.py::TestCCRToolCallDetectionInBatch -q
============================= 64 passed in 0.57s =============================

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

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

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

git commit -m "refactor(ccr): isolate tool call classification"
Sync plugin versions.....................................................Passed
check for merge conflicts................................................Passed
ruff.....................................................................Passed
ruff-format..............................................................Passed
mypy.....................................................................Passed
```

## Real Behavior Proof
- Environment: Windows PowerShell, Python 3.13.13, branch
`jd/architecture-slice-4` based on `headroomlabs/main`.
- Exact command / steps: Ran CCR tool-call tests, existing CCR response
handler tests, OpenAI Responses CCR tests, CCR batch detection tests,
focused ruff, targeted mypy, compileall, and commit hooks.
- Observed result: Existing handler behavior remains covered while
provider-shaped CCR classification is now directly testable as a pure
module.
- Not tested: Full pytest suite, live upstream provider traffic, and
manual streaming clients.

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

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

## Screenshots (if applicable)
N/A

## Additional Notes
Documentation and CHANGELOG updates are N/A for this internal refactor.
Full pytest was not run; validation is focused on CCR tool-call
detection/parsing and response-handler compatibility.
2026-07-10 17:36:24 -05:00
JD Davis
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.
2026-07-10 17:35:31 -05:00
JD Davis
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.
2026-07-10 17:27:26 -05:00
Fabien Culpo
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.
2026-07-10 11:15:36 -07:00
Abhay Singh
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.
2026-07-10 11:47:31 -04:00
Abhay Singh
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.
2026-07-10 11:45:58 -04:00
Tejas Chopra
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>
2026-07-10 11:38:08 -04:00
Tejas Chopra
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
2026-07-10 06:44:29 -07:00
JD Davis
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.
2026-07-10 06:43:04 -07:00
JD Davis
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.
2026-07-10 00:40:34 -04:00
OrbisAI Security
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>
2026-07-09 21:20:13 -07:00
panamarob30-jpg
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>
2026-07-09 21:47:59 -05:00
Matt Van Horn
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 `45934b94`.
- Exact command / steps: Merged current `headroomlabs/main`, then ran
the targeted SSE pytest command and Ruff check shown above.
- Observed result: Targeted SSE tests passed with 12 tests, and
unknown/server_tool_use content blocks round-trip verbatim in
`content_block_start`; before this change the same input raised
`ValueError: Unsupported Anthropic content block type for SSE
conversion: 'server_tool_use'` after the full generation had already
been buffered, surfacing to the client as a 502 and a full multi-minute
retry.
- Not tested: End-to-end against a live upstream that emits server-side
tool blocks.

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

## Screenshots (if applicable)

N/A.

## Additional Notes

The prior `test_response_to_sse_rejects_unknown_content_block` is
replaced by `test_response_to_sse_emits_unknown_content_block_verbatim`;
current main's newer buffered CCR/thinking tests are preserved after the
merge from main.

---------

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-09 21:45:40 -05:00
Rod Boev
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>
2026-07-09 21:43:17 -05:00
JD Davis
98ff203f98
ci: allow PyPI deps during CPU torch install (#1930)
## Description

Fixes the CI failure exposed on the `8527b910` push run:

- Run:
https://github.com/headroomlabs-ai/headroom/actions/runs/29063619700
- Failed job: `test (3)` / job `86271743483`
- Failed step: `Install (CPU torch + prebuilt wheel + dev deps, no cargo
rebuild)`

The failing command used the PyTorch CPU index as the only package
index:

```bash
pip install torch --index-url https://download.pytorch.org/whl/cpu
```

That index does not provide transitive dependencies such as
`typing-extensions`, so pip backtracked across torch CPU wheels and
failed before tests ran. This PR keeps the PyTorch CPU index primary for
`torch` while allowing PyPI for dependencies.

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

- Adds `--extra-index-url https://pypi.org/simple` to each CI `pip
install torch --index-url https://download.pytorch.org/whl/cpu` command.
- Leaves the CPU torch index as the primary index so CI still installs
CPU torch wheels.

## Testing

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

### Test Output

```text
git diff --check
# passed

rg -n "pip install torch" .github/workflows/ci.yml
# all four torch install commands now include --extra-index-url https://pypi.org/simple
```

## Real Behavior Proof

- Environment: Local worktree `C:\git\headroom-ci-fix`, branch
`jd/fix-ci-torch-install-index`.
- Exact command / steps: Inspected failed GitHub Actions log for run
`29063619700` job `86271743483`, then updated matching torch install
commands in `.github/workflows/ci.yml`.
- Observed result: The workflow no longer uses the PyTorch CPU index as
the only index for torch installs; PyPI is available for transitive
dependencies such as `typing-extensions`.
- Not tested: Full CI run locally; GitHub Actions is the authoritative
verification for the affected install 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
- [ ] 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

This is a fix-forward PR. Reverting `8527b910` would reintroduce the
Ruff failure it fixed; the failed job did not reach tests and failed
only during dependency installation.
2026-07-09 19:36:26 -07:00
JerrettDavis
8527b910dc test(litellm): remove unused pytest import 2026-07-09 20:59:12 -05:00
JerrettDavis
2d418335a1 ci: preserve merge labels while state is unknown 2026-07-09 19:51:41 -05:00
JerrettDavis
595b709a5b ci: keep ready label off changes-requested PRs 2026-07-09 19:49:55 -05:00
ahhdammm
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>
2026-07-09 17:29:40 -05:00
Sepuri Sai Krishna
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.
2026-07-09 17:29:17 -05:00
Peter Lodri
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)
2026-07-09 17:09:33 -05:00
Rocker Zhang
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.
2026-07-09 17:07:26 -05:00
Rocker Zhang
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.
2026-07-09 17:07:08 -05:00
dependabot[bot]
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>
2026-07-09 17:02:59 -05:00
dependabot[bot]
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 &amp; 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="9a269030df"><code>9a26903</code></a>
Version Packages</li>
<li><a
href="3e33f4362f"><code>3e33f43</code></a>
perf(satteri): reduce clones</li>
<li><a
href="3597c9d1e6"><code>3597c9d</code></a>
perf(satteri): persist results</li>
<li><a
href="0f389cf3de"><code>0f389cf</code></a>
feat(satteri): decouple imports/exports from <code>compile()</code></li>
<li><a
href="4611f97d49"><code>4611f97</code></a>
feat(satteri): full rehype-toc functionality</li>
<li><a
href="d095300760"><code>d095300</code></a>
fix(satteri): workaround common issues</li>
<li><a
href="0297e25477"><code>0297e25</code></a>
configure pretrust</li>
<li><a
href="02c242b0da"><code>02c242b</code></a>
chore(satteri): clean code</li>
<li><a
href="3d80b8b242"><code>3d80b8b</code></a>
fix(mdx): ensure satteri integration is optional</li>
<li><a
href="0ec19af868"><code>0ec19af</code></a>
feat(satteri): more tests &amp; move remark-include</li>
<li>Additional commits viewable in <a
href="https://github.com/fuma-nama/fumadocs/compare/fumadocs-typescript@4.0.14...fumadocs-typescript@5.3.0">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>
2026-07-09 17:02:44 -05:00
dependabot[bot]
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>
2026-07-09 17:02:22 -05:00
dependabot[bot]
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>
2026-07-09 17:02:04 -05:00
Zhenjia ZHOU
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.
2026-07-09 17:01:29 -05:00
Zhenjia ZHOU
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.
2026-07-09 17:00:30 -05:00
Sakamoto, Kazunori
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.
2026-07-09 16:58:20 -05:00