2026-01-06 23:16:58 -08:00
|
|
|
"""Shared pytest fixtures for Headroom tests."""
|
|
|
|
|
|
Replace legacy memory system with HierarchicalMemory
Major refactor of the memory module:
- Add hierarchical scoping (user → session → agent → turn)
- Add temporal versioning with supersession support
- Add pluggable adapters (SQLite store, HNSW vectors, FTS5 text search)
- Add protocol interfaces (ports) for all memory components
- Update LRUMemoryCache to implement async MemoryCache protocol
- Update wrapper.py to use HierarchicalMemory backend
- Preserve with_memory() one-liner API with zero-latency inline extraction
New files:
- adapters/: sqlite.py, hnsw.py, fts5.py, cache.py, embedders.py
- core.py: HierarchicalMemory orchestrator
- models.py: Memory, MemoryCategory, ScopeLevel
- ports.py: Protocol interfaces (MemoryStore, VectorIndex, etc.)
- config.py: MemoryConfig with backend selection
- factory.py: Component creation from config
Removed legacy files:
- store.py, fast_store.py, extractor.py, worker.py, fast_wrapper.py
Breaking change: Removes legacy memory API (pre-0.3.0)
2026-01-22 23:28:21 -08:00
|
|
|
# CRITICAL: Must be set before ANY imports that could trigger sentence_transformers
|
|
|
|
|
# The Rust tokenizers use parallelism that deadlocks with pytest-asyncio
|
|
|
|
|
import os
|
|
|
|
|
|
|
|
|
|
os.environ["TOKENIZERS_PARALLELISM"] = "false"
|
|
|
|
|
|
2026-01-06 23:16:58 -08:00
|
|
|
import json
|
|
|
|
|
import tempfile
|
|
|
|
|
from datetime import datetime
|
|
|
|
|
from pathlib import Path
|
2026-01-10 15:33:44 -08:00
|
|
|
from unittest.mock import Mock
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
|
fix(ccr): skip Anthropic marker emission when tool injection is deferred (#1273)
## Description
Anthropic request-side CCR can still compress a turn into retrieval
markers after the frozen-prefix cache guard suppresses
`headroom_retrieve` registration. That leaves the model with marker-only
context it cannot redeem, so the proxy silently drops recoverable data
on exactly the turns where cache preservation deferred tool injection.
This change couples the Anthropic request-side CCR path to tool
availability so a turn never emits retrieve-only markers without the
retrieval tool, even when token mode or cache-mode prefix replay could
otherwise reuse already-compressed marker text. Closes #1006
After a collaborator merged current `main` into this branch, CI also
picked up unrelated offline-memory failures from the merged base. Those
follow-up changes are test-only: they keep the offline Hugging Face
cache lanes skipping cleanly instead of failing in memory tests that are
outside the CCR runtime path.
## 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
- Couple Anthropic request-side CCR compression to the same
frozen-prefix guard that already defers `headroom_retrieve`
registration.
- Keep the existing cache-preservation behavior: frozen-prefix turns
stop emitting CCR retrieval markers instead of forcing tool injection
into the cached prefix.
- Make the skip decision use the effective frozen prefix after
token-mode reclamping, so turns that genuinely reclamp to zero still
keep normal reversible CCR behavior.
- Bypass cached marker reuse in both token mode and cache-mode prefix
replay when tool injection is deferred.
- Add focused regressions for the Anthropic request-path seams under
this bug:
- frozen-prefix turns do not emit marker-only payloads
- unfrozen turns still keep normal reversible CCR behavior
- token-mode reclamp back to zero still compresses normally
- existing `headroom_retrieve` tools keep reversible CCR on frozen turns
- cache-mode delta reuse and exact-prefix replay both forward original
content when retrieval is unavailable
- Add a `CHANGELOG.md` entry because the proxy's user-visible Anthropic
CCR behavior changes.
- Add a shared test skip helper for offline Hugging Face cache misses
and apply it to the merged `main` memory tests that were failing only in
the offline CI shards after the branch picked up current `main`.
## Testing
- [x] Unit tests pass (`uv run pytest
tests/test_proxy/test_anthropic_ccr_deferred_injection.py`)
- [x] Linting passes (`uv run ruff check . && uv run ruff format .
--check`)
- [ ] Type checking passes (`uv run mypy headroom`)
- [x] New tests added for new functionality when applicable
- [ ] Manual testing performed
### Test Output
```text
uv run pytest tests/test_proxy/test_anthropic_ccr_deferred_injection.py
14 passed, 1 warning in 34.19s
uv run pytest tests/test_memory/test_skip_helpers.py tests/test_memory/test_embedder_mps_serialization.py::test_cpu_uses_shared_executor tests/test_memory/test_hierarchical.py::TestLocalEmbedder::test_embed_single tests/test_memory_bridge.py::TestMemoryBridgeImport::test_import_claude_code_memory tests/test_memory_handler_concurrent_init.py::test_real_localbackend_initializes_via_public_entrypoint tests/test_memory_system.py::TestLocalBackend::test_save_memory_basic -q
4 passed, 5 skipped, 11 warnings in 7.65s
uv run ruff check .
All checks passed!
uv run ruff format . --check
968 files already formatted
```
## Real Behavior Proof
- Environment: local FastAPI `TestClient` for the Anthropic request
path, plus Windows Python 3.12 offline-memory repros with
`TRANSFORMERS_OFFLINE=1`
- Exact command / steps: Run the focused CCR regression command and the
offline-memory repro subset below on the merged branch state.
- `uv run pytest
tests/test_proxy/test_anthropic_ccr_deferred_injection.py`
- `uv run pytest tests/test_memory/test_skip_helpers.py
tests/test_memory/test_embedder_mps_serialization.py::test_cpu_uses_shared_executor
tests/test_memory/test_hierarchical.py::TestLocalEmbedder::test_embed_single
tests/test_memory_bridge.py::TestMemoryBridgeImport::test_import_claude_code_memory
tests/test_memory_handler_concurrent_init.py::test_real_localbackend_initializes_via_public_entrypoint
tests/test_memory_system.py::TestLocalBackend::test_save_memory_basic
-q`
- Scenario coverage from the CCR pytest command:
- frozen prefix with deferred tool injection and cached marker text
available in token mode
- unfrozen turn with normal CCR marker emission
- token mode where the tracked frozen prefix reclamps back to zero
- frozen prefix where the client already supplied `headroom_retrieve`
- cache-mode append-only delta reuse with a previously forwarded
compressed prefix
- cache-mode exact-prefix replay where the previous forwarded prefix
already contained a marker
- Scenario coverage from the offline-memory repro command:
- direct local embedder startup on CPU with no cached HF model
- hierarchical memory embedder startup with offline model cache missing
- bridge import through `LocalBackend`
- `MemoryHandler` public init warmup path
- `LocalBackend` save path under the offline lane
- Observed result: The CCR regression keeps marker-free forwarding on
frozen turns without tool availability, and the merged-`main`
offline-memory lanes now skip cleanly instead of failing unrelated CI
shards.
- frozen-prefix Anthropic turns without tool availability forward the
original long transcript across both token-mode and cache-mode reuse
paths, while unfrozen turns, reclamped token-mode turns, and frozen
turns that already advertise `headroom_retrieve` keep the reversible CCR
marker path
- the merged-`main` offline-memory regressions now skip cleanly when the
Hugging Face cache is unavailable instead of failing unrelated CI shards
- Not tested: live Zed session cache-hit behavior, provider latency
under real Anthropic upstreams, and online Hugging Face download lanes
## Review Readiness
- [x] I have performed a self-review
- [x] This PR is ready for human review
## Checklist
- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] I have updated the CHANGELOG.md if applicable
## Additional Notes
- Runtime scope is still intentionally narrow to Anthropic request-side
CCR. OpenAI, Gemini, streaming, and response-side CCR behavior are
unchanged.
- The only non-CCR diff is the test-only offline-memory follow-up
required after current `main` was merged into the branch.
- The focused proxy pytest run still emits the Windows-local
`StarletteDeprecationWarning` from `fastapi.testclient`'s `httpx`
bridge. The offline-memory repro command also emits existing datetime
deprecation warnings and a pytest teardown warning around skipped
offline lanes; none of those warnings were introduced by the CCR runtime
change.
2026-06-23 13:48:05 -04:00
|
|
|
from tests._skip_helpers import external_model_skip_reason
|
2026-01-30 16:37:34 -08:00
|
|
|
|
fix(proxy): dedupe Codex WS request logging for accurate mixed-provider dashboards (#2189)
## Description
Running Claude Code (Anthropic) and Codex (OpenAI) against the **same**
Headroom proxy instance on one port produced incorrect, unstable
dashboard data. The proxy core is provider-isolated and
multi-provider-safe by design; the defect was in the observability
layer. The Codex `/v1/responses` **WebSocket** handler was the only path
in the proxy that wrote to the request logger by hand instead of through
the unified `emit_request_outcome` funnel, and it did so twice per
session close: the per-turn funnel record plus an unconditional
cumulative session-summary `RequestLog`. This PR removes the duplicate
summary log so Codex WS emits exactly one request log per turn, matching
the HTTP provider paths.
## 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
- Dropped the duplicate cumulative session-summary `RequestLog` in the
Codex WS handler while preserving the per-turn `emit_request_outcome`
path.
- Preserved gated `request_messages` and `turn_id` on residual outcomes
so dashboard telemetry keeps the useful attribution without
double-counting tokens.
- Ensured explicit `--anyllm-provider` wins over a leaked
`HEADROOM_ANYLLM_PROVIDER` environment variable.
- Registered retry delay settings that had drifted out of the settings
registry.
- Hardened tests against developer-shell `HEADROOM_*` /
`ANTHROPIC_CUSTOM_HEADERS` leakage and stabilized several focused
proxy/wrap test fixtures.
## Testing
- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [ ] Type checking passes (`mypy headroom`)
- [x] New tests added for new functionality
- [x] Manual testing performed
### Test Output
```text
$ .venv/bin/pytest tests/ -q -p no:cacheprovider
8529 passed, 537 skipped, 5831 warnings in 276.25s (0:04:36)
$ .venv/bin/ruff check <touched files>
All checks passed!
```
## Real Behavior Proof
- Environment: macOS (Darwin 25.4.0), Python 3.13.14, pytest 9.0.3, ruff
via project venv, branch `fix/multi-provider-runtime`.
- Exact command / steps: Ran the full test suite without pytest cache
provider and Ruff on all touched files; used `git stash` to confirm the
stale fake-config failures pre-existed this change.
- Observed result: Full suite passed with no failures; Ruff passed;
Codex WS now routes end-of-session logging through
`emit_request_outcome`, emitting one request log per turn with the same
accounting model as Anthropic HTTP turns.
- Not tested: Live simultaneous Claude + Codex dashboard run. `mypy
headroom` was not run to completion; a scoped run reported one
pre-existing `settings_store.py:470` coercion error outside this diff.
## 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 - server-side observability fix; no UI markup changed.
## Additional Notes
- The proxy's multi-provider routing, header/auth isolation, and
per-model cache keying are already correct and unchanged here; only the
WS observability write path was double-counting.
- Architectural assessment:
`plans/reports/research-260714-0004-multi-provider-upstream-compression-report.md`;
root-cause + resolution trail:
`plans/reports/debug-assessment-260714-0011-dashboard-instability-mixed-claude-codex-report.md`.
- No live simultaneous Claude + Codex dashboard run was performed;
validation is from test coverage and code review of the WS logging path.
---------
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-16 01:18:34 +07:00
|
|
|
|
|
|
|
|
# A live `headroom` dev session exports HEADROOM_* into the shell (and the
|
|
|
|
|
# Claude wrap adds ANTHROPIC_CUSTOM_HEADERS). Click `envvar=` options pick
|
|
|
|
|
# those up inside CliRunner, so assertions would see the developer's proxy
|
|
|
|
|
# config instead of the test's. Scrub them so local runs match CI; tests
|
|
|
|
|
# that need a value set it explicitly via monkeypatch or CliRunner env.
|
2026-08-13 09:52:22 -07:00
|
|
|
@pytest.fixture(autouse=True)
|
|
|
|
|
def _skip_proxy_dependency_gate_unless_exercised(
|
|
|
|
|
request: pytest.FixtureRequest, monkeypatch: pytest.MonkeyPatch
|
|
|
|
|
) -> None:
|
|
|
|
|
"""Most CLI tests run without headroom-ai[proxy] extras installed."""
|
|
|
|
|
if request.node.get_closest_marker("proxy_dependency_gate") is not None:
|
|
|
|
|
return
|
|
|
|
|
try:
|
|
|
|
|
from headroom.cli import proxy
|
|
|
|
|
except ModuleNotFoundError:
|
|
|
|
|
# Native-wrapper jobs intentionally install only pytest and exercise the
|
|
|
|
|
# installer scripts without importing Headroom's runtime dependencies.
|
|
|
|
|
return
|
|
|
|
|
monkeypatch.setattr(proxy, "ensure_proxy_dependencies", lambda: None)
|
|
|
|
|
|
|
|
|
|
|
fix(proxy): dedupe Codex WS request logging for accurate mixed-provider dashboards (#2189)
## Description
Running Claude Code (Anthropic) and Codex (OpenAI) against the **same**
Headroom proxy instance on one port produced incorrect, unstable
dashboard data. The proxy core is provider-isolated and
multi-provider-safe by design; the defect was in the observability
layer. The Codex `/v1/responses` **WebSocket** handler was the only path
in the proxy that wrote to the request logger by hand instead of through
the unified `emit_request_outcome` funnel, and it did so twice per
session close: the per-turn funnel record plus an unconditional
cumulative session-summary `RequestLog`. This PR removes the duplicate
summary log so Codex WS emits exactly one request log per turn, matching
the HTTP provider paths.
## 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
- Dropped the duplicate cumulative session-summary `RequestLog` in the
Codex WS handler while preserving the per-turn `emit_request_outcome`
path.
- Preserved gated `request_messages` and `turn_id` on residual outcomes
so dashboard telemetry keeps the useful attribution without
double-counting tokens.
- Ensured explicit `--anyllm-provider` wins over a leaked
`HEADROOM_ANYLLM_PROVIDER` environment variable.
- Registered retry delay settings that had drifted out of the settings
registry.
- Hardened tests against developer-shell `HEADROOM_*` /
`ANTHROPIC_CUSTOM_HEADERS` leakage and stabilized several focused
proxy/wrap test fixtures.
## Testing
- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [ ] Type checking passes (`mypy headroom`)
- [x] New tests added for new functionality
- [x] Manual testing performed
### Test Output
```text
$ .venv/bin/pytest tests/ -q -p no:cacheprovider
8529 passed, 537 skipped, 5831 warnings in 276.25s (0:04:36)
$ .venv/bin/ruff check <touched files>
All checks passed!
```
## Real Behavior Proof
- Environment: macOS (Darwin 25.4.0), Python 3.13.14, pytest 9.0.3, ruff
via project venv, branch `fix/multi-provider-runtime`.
- Exact command / steps: Ran the full test suite without pytest cache
provider and Ruff on all touched files; used `git stash` to confirm the
stale fake-config failures pre-existed this change.
- Observed result: Full suite passed with no failures; Ruff passed;
Codex WS now routes end-of-session logging through
`emit_request_outcome`, emitting one request log per turn with the same
accounting model as Anthropic HTTP turns.
- Not tested: Live simultaneous Claude + Codex dashboard run. `mypy
headroom` was not run to completion; a scoped run reported one
pre-existing `settings_store.py:470` coercion error outside this diff.
## 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 - server-side observability fix; no UI markup changed.
## Additional Notes
- The proxy's multi-provider routing, header/auth isolation, and
per-model cache keying are already correct and unchanged here; only the
WS observability write path was double-counting.
- Architectural assessment:
`plans/reports/research-260714-0004-multi-provider-upstream-compression-report.md`;
root-cause + resolution trail:
`plans/reports/debug-assessment-260714-0011-dashboard-instability-mixed-claude-codex-report.md`.
- No live simultaneous Claude + Codex dashboard run was performed;
validation is from test coverage and code review of the WS logging path.
---------
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-16 01:18:34 +07:00
|
|
|
@pytest.fixture(autouse=True)
|
|
|
|
|
def _scrub_developer_headroom_env(monkeypatch):
|
|
|
|
|
for key in list(os.environ):
|
|
|
|
|
if key.startswith("HEADROOM_"):
|
|
|
|
|
monkeypatch.delenv(key, raising=False)
|
|
|
|
|
monkeypatch.delenv("ANTHROPIC_CUSTOM_HEADERS", raising=False)
|
|
|
|
|
|
fix(proxy): repair main lint (ruff-format drift + mypy host_header) (#2268)
## Description
`main`'s `lint` CI job is currently **red** (latest main `eac49656` →
`lint: failure`), which blocks every open PR. Two causes, both from
recent merges that were green in isolation but combined into a red
`main`:
- **ruff-format drift** on 7 files — committed with formatter output
that ruff `0.15.17` (the CI-pinned version) rewrites.
- **mypy error** in `server.py`:
`_request_has_same_origin_or_no_provenance(request, host_header)` —
`host_header` is `request.headers.get("host")` (`str | None`) but the
function requires `str`.
These passed per-PR because each PR's checks ran against an older base;
the serialized `main` state is what went red — a logical-merge /
tool-version gap that per-PR CI doesn't catch without a strict merge
queue.
## Type of Change
- [x] Bug fix (CI/lint repair)
## Changes Made
- `ruff format` (0.15.17) the 7 drifted files — formatting only, no
logic changes: `cli/proxy.py`, `proxy/forwarded_headers.py`,
`proxy/savings_tracker.py`, `proxy/server.py`, `tests/conftest.py`,
`tests/test_persistent_metrics_persistence.py`,
`tests/test_proxy_loopback_gating.py`.
- Add `assert host_header is not None` after the
`is_ip_literal_host_header()` guard (which already rejects a missing
Host), narrowing the type for the same-origin check.
## Testing
- [x] `ruff check .` — clean
- [x] `ruff format --check .` — clean (tracked)
- [x] `mypy headroom --ignore-missing-imports` — clean
### Test Output
```text
$ mypy headroom --ignore-missing-imports → Success: no issues found in 504 source files
$ ruff check . → All checks passed (tracked)
$ ruff format --check . → clean (tracked)
```
## Real Behavior Proof
- Environment: branch off current `main` (`eac49656`), ruff 0.15.17 +
mypy 1.20.2 (CI-pinned).
- Confirmed `lint: failure` on main's latest CI run; after this change
all three lint steps pass locally.
- Not tested: full pytest suite — formatting + a type-narrowing `assert`
only, no behavior change.
## 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 (this *is* the
style fix)
- [x] I have performed a self-review of my code
- [x] My changes generate no new warnings
- [ ] Tests added (N/A — no behavior change)
- [x] New and existing unit tests pass locally
- [ ] CHANGELOG (N/A)
## Additional Notes
The 7 files were touched by recent merges (#2198, #2247) whose local
ruff differed from the pinned `0.15.17`. Merging this unblocks the
`lint` gate for all open PRs (including #2207).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-07-15 23:18:50 -07:00
|
|
|
|
fix(telemetry): anonymous compression stats — no prompts, no data (#2728)
## In one line
Headroom starts reporting **how well compression is working** — counters
and percentages only. **No prompts. No code. No file paths. Nothing
about what you're building.**
## Why
Right now nobody knows whether compression actually helps real users.
You can see your own numbers in `/stats`, but that's it — there's no way
to tell whether a given workload compresses well, or why it sometimes
doesn't. This closes that loop so we can make compression better for
everyone.
## Exactly what gets sent
One message per session, and every 5 minutes while you're active:
```json
{
"session": { "id": "random", "turns": 47, "duration_s": 4210, "seq": 3 },
"tokens": { "original": 890000, "attempted": 410000, "saved": 320000,
"tool_saved": 48000, "cache_read": 210000 },
"rates": { "saved_pct": 35.96, "eligible_pct": 46.07, "yield_pct": 78.05,
"cache_read_pct": 23.60, "overhead_pct": 1.96 },
"compression": { "transforms": {"crush": 47}, "passthrough_turns": 0 },
"skips": {},
"sources": { "proxy": 47 },
"providers": ["anthropic"],
"models": ["claude-sonnet-4-5-20250929"],
"failures": 2
}
```
Plus a random install ID, the Headroom version, and OS/architecture
(`darwin`, `arm64`).
That's the whole thing. A full example lives at
`deploy/beacon/sample-event.json`.
## What is never sent
- Your prompts or the model's responses
- Your code
- File paths, project names, repo names
- Tool names or MCP server names
- Hostname, username, or IP address
- Custom or fine-tuned model names (an id like `ft:gpt-4o:acme-corp:…`
contains a company name, so only models in a public registry are
reported)
**This is structural, not a pinky-swear.** Every value in the payload is
a number, a fixed word, or a random ID — there is no free-text field
anywhere for content to hide in. The receiver
(`deploy/beacon/worker.js`, in this repo so you can read it) drops
anything not on an explicit allowlist before storing.
## Turning it off
Any one of these:
```bash
HEADROOM_BEACON=off # or
DO_NOT_TRACK=1 # or
# offline mode
```
It's on by default, and Headroom says so at startup:
```
Telemetry: anonymous compression stats — never prompts, code, or file paths.
Helps us improve compression | Off: HEADROOM_BEACON=off
```
`HEADROOM_TELEMETRY` is a **separate** switch that still only affects
local stats. If you had turned that on, this change does not start
uploading anything — you answered a different question, and upgrading
should not change the answer.
## Why the percentages, not just "tokens saved"
"We saved 36%" hides the interesting part. In the example above only
**46% of tokens were eligible** for compression at all — the rest is
frozen cache prefix and system prompts we deliberately do not touch. Of
what we *could* touch, we removed **78%**.
Those are two separate problems. Raising eligibility is proxy work;
raising yield is compressor work. A single number cannot tell us which
to fix.
## Coverage
`emit_request_outcome` is a single chokepoint —
`handler.metrics.record_request` is called from exactly one place,
inside the funnel — so all 30 `RequestOutcome` construction sites are
covered: Anthropic, OpenAI, Gemini, Bedrock, batch, streaming, and the
long-lived Codex Responses-WS path.
The `headroom_compress` MCP path bypassed that funnel and is now wired
in separately. It has a different shape (no provider, no upstream
latency, and everything handed to the tool is eligible by construction),
so `sources` counts turns by origin — MCP turns always read
`eligible_pct: 100` and must not drag the proxy's real eligibility
ceiling upward.
**Subagents.** All subagent traffic through the proxy merges into one
session, which is correct for savings and retention but means `turns`
conflates fan-out with depth. Fan-out is still derivable —
`compression.latency_ms_total / session.duration_s` gives the
concurrency ratio (~1x serial, ~4x for four parallel agents), so no
extra field is needed. Verified no lost updates under 6-way concurrency
(1,200 turns).
**Known gap:** `--workers N` gives each process its own aggregator, so
one user session becomes up to N. Token totals and fleet rates stay
correct; session counts inflate. This matches the existing documented
limitation that TOIN state, CostTracker, and the prefix tracker are all
per-process.
## Notes for reviewers
- **Cumulative snapshots, not deltas.** Every report restates running
totals under one session ID, so the highest `seq` per `(install,
session)` is the complete session. Dedupe is a window function, and a
lost report costs nothing.
- **Never breaks the proxy.** Every path swallows its own exceptions;
uploads go out on a daemon thread so nothing blocks the request loop.
- **Explicit User-Agent is load-bearing.** urllib's default is blocked
by Cloudflare (error 1010). Combined with fire-and-forget error
handling, that would have failed every upload while looking perfectly
healthy.
- **The exit flush was broken and is fixed.** `atexit` handed the POST
to a daemon thread, and daemon threads are killed before they finish
during interpreter shutdown — so nothing was sent. That silently dropped
*every session shorter than the 5-minute heartbeat*, plus all
short-lived subagent MCP processes. The exit path now posts
synchronously with a 2s timeout.
- Receiver and query tooling are in `deploy/beacon/`.
## Testing
- `python -m headroom.telemetry.session` self-check: dedupe, cumulative
totals, dropped-report recovery, payload contains no model id or
prompt-derived string, allowlist coverage
- 175 telemetry/outcome tests pass; 6 new ones cover the opt-out notice
- Verified end to end against a live deployment: client → receiver →
storage → query
## Still to do before release
The default endpoint currently points at a temporary `workers.dev` URL.
It needs to move to a Headroom-owned hostname before this ships in a
tagged release — noted inline at `DEFAULT_ENDPOINT`.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 05:43:25 -07:00
|
|
|
# The scrub above deletes every HEADROOM_* var — which includes HEADROOM_BEACON,
|
|
|
|
|
# and the beacon defaults to ON. So scrubbing for hermeticity is precisely what
|
|
|
|
|
# switches it on, and with HEADROOM_TELEMETRY_ENDPOINT scrubbed too it falls back
|
|
|
|
|
# to the real production endpoint. Every test that reaches the outcome funnel
|
|
|
|
|
# then POSTs a session event for real: observed writing into the live corpus
|
|
|
|
|
# during a local run, and CI would do the same on every push.
|
|
|
|
|
#
|
|
|
|
|
# Depends on the scrub fixture so it is guaranteed to run after it rather than
|
|
|
|
|
# relying on declaration order. A test that wants the beacon on just sets the
|
|
|
|
|
# var itself — monkeypatch inside the test wins over this.
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
|
|
|
def _disable_telemetry_beacon(monkeypatch, _scrub_developer_headroom_env):
|
|
|
|
|
monkeypatch.setenv("HEADROOM_BEACON", "off")
|
|
|
|
|
|
|
|
|
|
|
fix(wrap/serena): stop creating serena_config.yml, unbricking Serena on fresh installs (#2676)
## Description
`_ensure_serena_dashboard_disabled()` wrote a one-key bootstrap config
(`web_dashboard_open_on_launch: false`) into
`~/.serena/serena_config.yml` when the file was absent, assuming Serena
fills in any key it omits.
Verified against **Serena 1.6.2.dev0**
(`serena/config/serena_config.py`), that holds for every field except
one. Serena autogenerates its own complete config **only when the path
does not exist**:
```python
if not os.path.exists(config_file_path):
cls._generate_config_file(config_file_path)
```
Once any file is present it validates instead. Every other field falls
back to a dataclass default via `get_value_or_default`, but a missing
`projects` key is fatal (~line 1064):
```
SerenaConfigError: `projects` key not found in Serena configuration.
```
So Headroom's own bootstrap file killed Serena on **every machine
without a pre-existing Serena config**. The MCP server exited during
handshake — surfacing as `connection closed: initialize response` on
Codex and a bare `MCP error -32000: Connection closed` on OpenCode
(#2674) — and `serena project index` failed identically.
Headroom now leaves that file to Serena. That is immune to Serena adding
required keys later; guessing the schema is what caused the outage. The
popup never needed the file anyway: `build_serena_spec` passes
`--open-web-dashboard False`, which Serena applies *after* loading the
config (`serena/mcp.py:361` — `config.web_dashboard_open_on_launch =
open_web_dashboard`), so the flag wins regardless of what is on disk.
An **existing** config is still edited in place — dashboard key flipped,
`projects: []` backfilled to repair machines an affected version already
wrote — preserving a populated `projects` list, other keys and comments.
Closes #2674
## 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
- [x] Code refactoring (no functional changes)
## Changes Made
- `_ensure_serena_dashboard_disabled()` never creates
`serena_config.yml`; it only edits an existing one, and backfills
`projects: []` there to repair already-broken machines.
- Dropped `_scope_serena_languages` + `_detect_repo_languages` +
`_EXT_TO_SERENA_LANGUAGE` (**−133 lines**). Dead weight: Serena
determines languages itself in `ProjectConfig.autogenerate`
(`_determine_project_language_servers`) and records them under
`language_servers` — `languages`, which Headroom wrote, is a legacy name
Serena migrates via `RENAMED_FIELDS`. Serena's generated file uses a
block-style list, so our single-line-flow regex never matched it: on any
Serena-generated `project.yml` the function was a **verified no-op**.
The only case where it acted was creating the file — the same
partial-config trap — which also skipped the `project.local.yml` sidecar
Serena writes alongside.
- **Test isolation:** the MCP install ledger defaults to
`~/.headroom/mcp_installs.json`, so any test registering a server wrote
into the developer's real ledger (observed adding a live `claude/serena`
entry during a local run). `conftest.py` now redirects it per-test.
- **Repo config:** `.serena/project.yml` carried a stale `project_name`
(`"feature-opencode-wrap"`) and listed only `typescript`, so Serena's
symbol index skipped 1331 Python and 194 Rust files for every
contributor.
## 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_wrap_code_memory.py tests/test_cli/test_wrap_serena_boost.py \
tests/test_cli/test_serena_migrate.py tests/test_cli/test_serena_disable.py -q
35 passed, 1 skipped in 0.84s
$ SERENA_SRC=<serena checkout> pytest tests/test_wrap_code_memory.py -q
13 passed in 0.62s # the skipped test runs when a Serena source tree is available
$ ruff check headroom/ tests/ --exclude headroom/dashboard/templates
All checks passed!
$ mypy headroom/cli/wrap.py
Success: no issues found in 1 source file
```
New tests. The key one asserts the invariant rather than our own key
list, so it stays correct even if Serena adds a required key — a test
pinning `projects: []` would keep passing while users broke again:
- `test_serena_config_is_never_created_by_headroom` — Headroom must not
pre-empt Serena's bootstrap
- `test_serena_dashboard_disabled_repairs_config_missing_projects` —
heals a config an affected version wrote
- `test_serena_dashboard_disabled_preserves_registered_projects` — never
clobbers the real registry; comments kept, no duplicate key
- `test_serena_dashboard_disabled_is_idempotent`
- `test_serena_config_required_keys_match_serena_source` — reads
Serena's real source and pins the two facts this fix rests on
(bootstrap-only-when-absent, `projects` is the sole fatal omission).
Skipped unless `SERENA_SRC` is set; deliberately **not** named
`HEADROOM_*` because `conftest.py` scrubs that namespace, which would
make it silently always-skip.
## Real Behavior Proof
- **Environment:** macOS 15.4 (darwin 25.4.0), Python 3.12.6, Serena
1.6.2.dev0 via `uvx --from git+https://github.com/oraios/serena`, Codex
CLI 0.146.0.
- **Exact command / steps:** a probe doing a real JSON-RPC `initialize`
handshake against the exact command `headroom wrap` registers — i.e.
what Codex/OpenCode actually do — in a throwaway `HOME` per case. (A)
pre-seeded with the one-line config an affected version wrote; (B) no
config; (C) real `headroom wrap codex --prepare-only`, then handshake.
- **Observed result:**
```text
=== A. BROKEN: single-key config (Headroom 0.33.0) ===
MCP handshake: FAIL — no initialize response (exit=1). stderr tail:
File ".../serena/config/serena_config.py", line 1064, in from_config_file
raise SerenaConfigError("`projects` key not found in Serena configuration. ...")
serena.config.serena_config.SerenaConfigError: `projects` key not found ...
config after run: 1 lines, has 'projects': False
=== B. FIXED: no config, Serena bootstraps it ===
MCP handshake: PASS — initialize OK — serverInfo.name='Serena'
config after run: 213 lines, has 'projects': True
=== C. FULL FLOW: real `headroom wrap codex` then handshake ===
Serena: no serena_config.yml yet — letting Serena generate it
Serena MCP: registered (restart OpenAI Codex CLI if it was already running)
Serena: project pre-indexed (symbol cache warmed)
serena_config.yml: 213 lines, written by Serena (correct)
MCP handshake: PASS — initialize OK — serverInfo.name='Serena'
--- verdict ---
A (broken config) started: False <- expected False
B (fixed, no config) started: True <- expected True
C (after real wrap) started: True <- expected True
```
A second `wrap` in the same HOME flips the dashboard without damage:
`true` → `false`, `projects` intact, all 153 comment lines intact. The
writer was isolated against a pristine 213-line Serena config: **delta 0
newlines**.
- **Not tested:** Windows and Linux (macOS only); Serena versions other
than 1.6.2.dev0; the JetBrains language backend. The probe needs network
+ `uvx` (~2 min) so it is a manual verification tool, not wired into CI.
## Review Readiness
- [x] I have performed a self-review
- [x] This PR is ready for human review
## Checklist
- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] I did **not** edit `CHANGELOG.md` — it is generated by
release-please from my Conventional Commit PR title (a CI guard enforces
this)
## Additional Notes
- **Docs:** N/A — no user-facing docs described the `serena_config.yml`
bootstrap or the language scoping.
- Also fixes the OpenCode report (#2674). The Codex-side report of the
same root cause quotes the `SerenaConfigError` verbatim; OpenCode only
surfaces the generic `-32000`, which is why it read as two different
bugs.
- Users already broken by an affected version are repaired automatically
on their next `headroom wrap` — no manual `serena_config.yml` edit
needed.
- A stacked PR removing the rtk/lean-ctx CLI context tools is based on
this branch; this one is deliberately small so it can land first.
2026-07-30 20:55:47 -07:00
|
|
|
# The MCP install ledger defaults to ``~/.headroom/mcp_installs.json``, so any
|
|
|
|
|
# test that registers a server (directly or through `wrap`) writes into the
|
|
|
|
|
# developer's REAL ledger — observed adding a live `claude/serena` entry during a
|
|
|
|
|
# local run. Since the scrub above deletes HEADROOM_WORKSPACE_DIR, the default is
|
|
|
|
|
# always the real home. Redirect the ledger per-test instead: every writer
|
|
|
|
|
# (`record_install` / `clear_install` / `headroom_installed_matching`) resolves it
|
|
|
|
|
# through this module-global, so one patch covers them all. Patched here rather
|
|
|
|
|
# than pointing workspace_dir() at a tmp path, which would break the tests that
|
|
|
|
|
# assert the default workspace layout.
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
|
|
|
def _isolate_mcp_ledger(monkeypatch, tmp_path_factory):
|
|
|
|
|
# Same guard as _reset_copilot_routing_flag below: the macos/windows-native-
|
|
|
|
|
# wrapper CI jobs install only pytest and drive the installer shell scripts
|
|
|
|
|
# via subprocess, so headroom isn't importable and there is no ledger to
|
|
|
|
|
# redirect. Skip there instead of erroring at setup.
|
|
|
|
|
try:
|
|
|
|
|
from headroom.mcp_registry import ledger
|
|
|
|
|
except ModuleNotFoundError:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
ledger_file = tmp_path_factory.mktemp("mcp-ledger") / "mcp_installs.json"
|
|
|
|
|
monkeypatch.setattr(ledger, "ledger_path", lambda: ledger_file)
|
|
|
|
|
|
|
|
|
|
|
feat(proxy): label GitHub Copilot traffic as "copilot" in the outcome… (#2377)
## Description
Requests routed to the GitHub Copilot API travel on the OpenAI or
Anthropic
wire, so the proxy handlers stamp the *wire* provider (`openai` /
`anthropic`)
on the outcome. As a result, Copilot traffic is attributed to
OpenAI/Claude in
the dashboard's per-request provider stats, hiding the real upstream.
(This is
distinct from the existing **Copilot Quota** panel, which is separate
from
per-request provider attribution.)
This labels Copilot traffic as `copilot` in the single outcome funnel.
`build_copilot_upstream_url()` is already the one routing chokepoint
every
Copilot surface goes through (OpenAI `/chat/completions` + `/responses`
and the
Anthropic `/v1/messages` route all build their upstream URL there), so
it flags
the request via a task-local `ContextVar`; `emit_request_outcome()`
reads the
flag and relabels the provider. The relabel runs before the `>= 500`
failed
guard, so a failed Copilot request is attributed to `copilot` too.
Non-Copilot
traffic never sets the flag and is untouched.
## Type of Change
- [x] New feature (non-breaking change that adds functionality)
## Changes Made
- `headroom/copilot_auth.py`: add a task-local
`_request_routed_to_copilot`
`ContextVar` with `mark_request_routed_to_copilot()` /
`request_routed_to_copilot()`
helpers; set the flag in `build_copilot_upstream_url()` whenever the
base is a
Copilot API URL (the existing `is_copilot_api_url` check). `/v1` path
normalization is unchanged.
- `headroom/proxy/outcome.py`: in `emit_request_outcome()`, when the
request was
routed to Copilot and the wire provider is `openai`/`anthropic`, relabel
the
outcome provider to `copilot` (before the 5xx guard).
- `tests/test_copilot_provider_label.py`: new tests for the chokepoint
marking
and the outcome relabel.
## Testing
- [x] Unit tests pass (`pytest`)
- [ ] Linting passes (`ruff check .`) — ran on the changed files only
(clean)
- [ ] Type checking passes (`mypy headroom`) — ran on the changed files
only (clean)
- [x] New tests added for new functionality
- [ ] Manual testing performed
### Test Output
```text
$ python -m pytest tests/test_copilot_provider_label.py tests/test_outcome_records_5xx_as_failed.py -q
tests/test_copilot_provider_label.py ..... [ 71%]
tests/test_outcome_records_5xx_as_failed.py .. [100%]
7 passed
$ python -m pytest tests/test_copilot_auth.py -k "build_copilot_upstream_url or copilot_api_url" -q
8 passed, 58 deselected # existing /v1-stripping behavior preserved
$ python -m ruff check headroom/copilot_auth.py headroom/proxy/outcome.py tests/test_copilot_provider_label.py
All checks passed!
$ python -m mypy headroom/copilot_auth.py headroom/proxy/outcome.py
Success: no issues found in 2 source files
```
## Real Behavior Proof
- Environment: Python 3.11, headroom installed with the `proxy` extra.
- Exact command / steps: the unit tests above drive
`build_copilot_upstream_url()`
followed by `emit_request_outcome()` in an isolated context and assert
the
recorded provider.
- Observed result: an `anthropic`/`openai` outcome for a request routed
to
`https://api.githubcopilot.com` is recorded as provider `copilot`; a
request
not routed to Copilot is recorded under its wire provider unchanged.
- Not tested: end-to-end against a live Copilot subscription (no live
seat in the
test environment).
## Review Readiness
- [x] I have performed a self-review
- [x] This PR is ready for human review
## Additional Notes
- The flag is a `ContextVar` (task-local), so it cannot bleed across
concurrent
requests; each request that is not routed to Copilot simply reads the
`False`
default.
- No `CHANGELOG.md` edits (release-please generates it from the
Conventional
Commit PR title).
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-18 18:54:06 +02:00
|
|
|
# The Copilot "routed to Copilot" flag is a module-global ContextVar that
|
|
|
|
|
# build_copilot_upstream_url() sets as a side effect. Unit tests that call that
|
|
|
|
|
# builder directly (or otherwise run in the shared root context) would leave it
|
|
|
|
|
# set and mislabel a later test's request outcome as "copilot". Reset it around
|
|
|
|
|
# every test so build-time side effects can't leak between tests.
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
|
|
|
def _reset_copilot_routing_flag():
|
2026-07-18 20:42:09 -07:00
|
|
|
# The macos/windows-native-wrapper CI jobs run the installer tests with only
|
|
|
|
|
# pytest installed (no headroom): they drive the installer shell scripts via
|
|
|
|
|
# subprocess, so headroom isn't importable and there's no routing flag to
|
|
|
|
|
# reset. Skip the reset there instead of erroring at setup.
|
|
|
|
|
try:
|
|
|
|
|
from headroom.copilot_auth import reset_request_routed_to_copilot
|
|
|
|
|
except ModuleNotFoundError:
|
|
|
|
|
yield
|
|
|
|
|
return
|
feat(proxy): label GitHub Copilot traffic as "copilot" in the outcome… (#2377)
## Description
Requests routed to the GitHub Copilot API travel on the OpenAI or
Anthropic
wire, so the proxy handlers stamp the *wire* provider (`openai` /
`anthropic`)
on the outcome. As a result, Copilot traffic is attributed to
OpenAI/Claude in
the dashboard's per-request provider stats, hiding the real upstream.
(This is
distinct from the existing **Copilot Quota** panel, which is separate
from
per-request provider attribution.)
This labels Copilot traffic as `copilot` in the single outcome funnel.
`build_copilot_upstream_url()` is already the one routing chokepoint
every
Copilot surface goes through (OpenAI `/chat/completions` + `/responses`
and the
Anthropic `/v1/messages` route all build their upstream URL there), so
it flags
the request via a task-local `ContextVar`; `emit_request_outcome()`
reads the
flag and relabels the provider. The relabel runs before the `>= 500`
failed
guard, so a failed Copilot request is attributed to `copilot` too.
Non-Copilot
traffic never sets the flag and is untouched.
## Type of Change
- [x] New feature (non-breaking change that adds functionality)
## Changes Made
- `headroom/copilot_auth.py`: add a task-local
`_request_routed_to_copilot`
`ContextVar` with `mark_request_routed_to_copilot()` /
`request_routed_to_copilot()`
helpers; set the flag in `build_copilot_upstream_url()` whenever the
base is a
Copilot API URL (the existing `is_copilot_api_url` check). `/v1` path
normalization is unchanged.
- `headroom/proxy/outcome.py`: in `emit_request_outcome()`, when the
request was
routed to Copilot and the wire provider is `openai`/`anthropic`, relabel
the
outcome provider to `copilot` (before the 5xx guard).
- `tests/test_copilot_provider_label.py`: new tests for the chokepoint
marking
and the outcome relabel.
## Testing
- [x] Unit tests pass (`pytest`)
- [ ] Linting passes (`ruff check .`) — ran on the changed files only
(clean)
- [ ] Type checking passes (`mypy headroom`) — ran on the changed files
only (clean)
- [x] New tests added for new functionality
- [ ] Manual testing performed
### Test Output
```text
$ python -m pytest tests/test_copilot_provider_label.py tests/test_outcome_records_5xx_as_failed.py -q
tests/test_copilot_provider_label.py ..... [ 71%]
tests/test_outcome_records_5xx_as_failed.py .. [100%]
7 passed
$ python -m pytest tests/test_copilot_auth.py -k "build_copilot_upstream_url or copilot_api_url" -q
8 passed, 58 deselected # existing /v1-stripping behavior preserved
$ python -m ruff check headroom/copilot_auth.py headroom/proxy/outcome.py tests/test_copilot_provider_label.py
All checks passed!
$ python -m mypy headroom/copilot_auth.py headroom/proxy/outcome.py
Success: no issues found in 2 source files
```
## Real Behavior Proof
- Environment: Python 3.11, headroom installed with the `proxy` extra.
- Exact command / steps: the unit tests above drive
`build_copilot_upstream_url()`
followed by `emit_request_outcome()` in an isolated context and assert
the
recorded provider.
- Observed result: an `anthropic`/`openai` outcome for a request routed
to
`https://api.githubcopilot.com` is recorded as provider `copilot`; a
request
not routed to Copilot is recorded under its wire provider unchanged.
- Not tested: end-to-end against a live Copilot subscription (no live
seat in the
test environment).
## Review Readiness
- [x] I have performed a self-review
- [x] This PR is ready for human review
## Additional Notes
- The flag is a `ContextVar` (task-local), so it cannot bleed across
concurrent
requests; each request that is not routed to Copilot simply reads the
`False`
default.
- No `CHANGELOG.md` edits (release-please generates it from the
Conventional
Commit PR title).
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-18 18:54:06 +02:00
|
|
|
|
|
|
|
|
reset_request_routed_to_copilot()
|
|
|
|
|
yield
|
|
|
|
|
reset_request_routed_to_copilot()
|
|
|
|
|
|
|
|
|
|
|
fix(proxy): cache litellm model resolution to stop repeated Provider List spam
## Description
The proxy repeatedly prints LiteLLM's `Provider List:
https://docs.litellm.ai/docs/providers` banner during normal operation,
with no explanation or way to suppress it (#2851).
Root cause: `_resolve_litellm_model()` in
`headroom/proxy/savings_tracker.py` runs on every savings-tracking
update (i.e. every request). For any model LiteLLM can't price (a
custom/local/gateway model name — e.g. the reporter's local oMLX setup),
the uncached fallback path calls `litellm.cost_per_token(...)` purely to
probe resolvability. When that probe fails, LiteLLM prints the banner as
an internal side effect before raising, and since the probe was never
cached, it re-fires on every single request for the same unresolvable
model.
**Update:** review flagged that the first version of this fix cached
into a plain, unbounded `dict` keyed by the (client-controlled) model
name — a memory-retention path on a request-facing proxy, since a caller
can grow it without limit by sending a new model string on every
request. Replaced with a bounded `functools.lru_cache`; see Changes Made
below.
Closes #2851
## 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`: `_resolve_litellm_model()` is now
decorated with `@lru_cache(maxsize=256)` instead of backing onto a
hand-rolled unbounded `dict`. An evicted model name simply re-probes
LiteLLM on next use — never a correctness issue, only whether the noisy
failure banner reruns for that specific name.
- `tests/conftest.py`: added a global `autouse` fixture,
`_reset_litellm_model_resolution_cache`, that clears the cache before
and after every test. It's process-lifetime and module-global, and
several existing tests monkeypatch `savings_tracker.litellm` with
different behavior per test while reusing common model names like
`"gpt-4o"` — without a reset, whichever test resolves a name first
silently wins that cache slot for the rest of the run and later tests
stop exercising their own fake.
- `tests/test_savings_tracker_litellm_resolution_cache.py` (new):
regression tests for the three properties that actually matter —
repeated resolution of one unknown model only probes LiteLLM once,
resolving far more distinct names than the bound never grows the cache
past it, and an evicted name is transparently re-probed rather than
reusing a slot it no longer owns.
- No behavior change for models LiteLLM can already price (fast path via
`model_cost` lookup) — only the noisy uncached probe path is memoized,
same as before.
## Testing
- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [ ] Type checking passes (`mypy headroom`) — not run; `mypy` isn't
installed in this environment
- [x] New tests added for new functionality
- [x] Manual testing performed
### Test Output
```text
$ python3 -m pytest tests/test_proxy_savings_history.py tests/test_savings_tracker_zero_price.py \
tests/test_savings_tracker_litellm_resolution_cache.py -q
tests/test_proxy_savings_history.py .................................... [ 73%]
... [ 79%]
tests/test_savings_tracker_zero_price.py ....... [ 93%]
tests/test_savings_tracker_litellm_resolution_cache.py ... [100%]
49 passed, 1 warning in 1.26s
# Re-run in reversed file order to check for the exact order-dependence the
# review flagged — same 49 passed, no failures either direction:
$ python3 -m pytest tests/test_savings_tracker_litellm_resolution_cache.py \
tests/test_savings_tracker_zero_price.py tests/test_proxy_savings_history.py -q
49 passed, 1 warning in 1.11s
$ python3 -m ruff check headroom/proxy/savings_tracker.py tests/conftest.py \
tests/test_savings_tracker_litellm_resolution_cache.py
All checks passed!
```
## Real Behavior Proof
- Environment: macOS, Python 3.12.3, this repo checked out locally.
- What changed since the last review pass: I got the compiled
`headroom._core` Rust extension in hand (by installing the published
`headroom-ai[all]` wheel into a separate venv and copying its
`_core.abi3.so` next to this local source tree — same Python ABI,
pure-Python edits in `savings_tracker.py` don't touch the compiled
boundary). That unblocked the full test files this fix touches,
including `tests/test_proxy_savings_history.py`, which was previously
reported as untestable here.
- Exact command / steps: three properties asserted directly against the
real (now-bounded) cache in
`tests/test_savings_tracker_litellm_resolution_cache.py`:
1. Resolve the same unresolvable model 5 times → assert the underlying
`litellm.cost_per_token` probe fired exactly once.
2. Resolve `_MODEL_RESOLUTION_CACHE_MAXSIZE + 50` distinct model names →
assert `_resolve_litellm_model.cache_info().currsize` stays at exactly
`_MODEL_RESOLUTION_CACHE_MAXSIZE` (256), never higher — this is the
actual memory-retention fix the review asked for.
3. Resolve one model, push exactly `maxsize` other distinct names
through to evict it via LRU, then resolve it again → assert it re-probed
(call count went 1 → 2), proving eviction is real and not just an
untested cache_info number.
- Observed result: all three pass; full affected-file suite (49 tests)
passes in both forward and reversed run order, confirming the new
`conftest.py` fixture actually fixes the cross-test leakage risk
(verified by literally reordering the files, not just by inspection).
- Not tested: a live HTTP request against a running `headroom proxy`
process specifically re-exercising this bounded-cache commit — the
earlier "20 simulated requests" proof against the previous
(unbounded-dict) version of this fix was via a standalone script, not a
real server; I have not repeated that specific live-server pass against
this commit. The unit-level proof above exercises the exact same
function (`_resolve_litellm_model`) the real proxy calls per-request
from `headroom/proxy/server.py`, so I'm confident it generalizes, but
flagging the gap rather than implying I re-ran it live.
## 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
— the bound/eviction rationale is commented above
`_resolve_litellm_model`, and the cross-test leakage rationale is
commented above the new `conftest.py` fixture
- [ ] I have made corresponding changes to the documentation — N/A,
internal implementation detail with no user-facing API/doc surface
- [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 did **not** edit `CHANGELOG.md`
## Additional Notes
- `mypy` still hasn't been run — not installed in this sandbox, and I
didn't want to widen the PR further by installing/configuring it just
for this. Flagging rather than silently skipping.
- The earlier "Additional Notes" gap about
`test_proxy_savings_history.py` being untestable in this environment is
resolved (see Real Behavior Proof) — it now runs and passes, including
the pre-existing
`test_litellm_resolution_and_savings_estimation_fallbacks` test that
exercises `_resolve_litellm_model` with a mutated `model_cost` dict
across several assertions in one test.
- Deliberately did not also bound
`headroom/pricing/litellm_pricing.py`'s sibling `_resolved_model_cache`
— same shape of cache, arguably the same exposure — since it's outside
this PR's diff and touching it wasn't asked for. Flagging in case a
maintainer wants it as a fast follow-up rather than silently leaving it
unmentioned.
---------
Co-authored-by: connectsudhindra-gif <connectsudhindra-gif@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-11 11:55:24 -05:00
|
|
|
# `savings_tracker._resolve_litellm_model` is an `lru_cache`d, module-global,
|
|
|
|
|
# process-lifetime cache keyed by model name (bounded — see #2860). Many test
|
|
|
|
|
# files monkeypatch `savings_tracker.litellm` to a fake with different
|
|
|
|
|
# `model_cost`/`cost_per_token` behavior per test, but reuse common model
|
|
|
|
|
# names like "gpt-4o" across them. Without a reset, whichever test resolves
|
|
|
|
|
# "gpt-4o" first "wins" the cache entry for the rest of the run, and later
|
|
|
|
|
# tests silently stop exercising their own fake — a real-not-hypothetical
|
|
|
|
|
# order-dependence bug once the cache is process-lifetime instead of per-call.
|
|
|
|
|
# Clear before AND after so a test's own within-test resolutions never leak
|
|
|
|
|
# in from, or leak out to, a neighboring test either.
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
|
|
|
def _reset_litellm_model_resolution_cache():
|
|
|
|
|
try:
|
|
|
|
|
from headroom.proxy.savings_tracker import _resolve_litellm_model
|
|
|
|
|
except ModuleNotFoundError:
|
|
|
|
|
yield
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
_resolve_litellm_model.cache_clear()
|
|
|
|
|
yield
|
|
|
|
|
_resolve_litellm_model.cache_clear()
|
|
|
|
|
|
|
|
|
|
|
2026-01-30 16:37:34 -08:00
|
|
|
# =============================================================================
|
|
|
|
|
# Global test hooks
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.hookimpl(hookwrapper=True)
|
|
|
|
|
def pytest_runtest_call(item):
|
fix(ccr): skip Anthropic marker emission when tool injection is deferred (#1273)
## Description
Anthropic request-side CCR can still compress a turn into retrieval
markers after the frozen-prefix cache guard suppresses
`headroom_retrieve` registration. That leaves the model with marker-only
context it cannot redeem, so the proxy silently drops recoverable data
on exactly the turns where cache preservation deferred tool injection.
This change couples the Anthropic request-side CCR path to tool
availability so a turn never emits retrieve-only markers without the
retrieval tool, even when token mode or cache-mode prefix replay could
otherwise reuse already-compressed marker text. Closes #1006
After a collaborator merged current `main` into this branch, CI also
picked up unrelated offline-memory failures from the merged base. Those
follow-up changes are test-only: they keep the offline Hugging Face
cache lanes skipping cleanly instead of failing in memory tests that are
outside the CCR runtime path.
## 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
- Couple Anthropic request-side CCR compression to the same
frozen-prefix guard that already defers `headroom_retrieve`
registration.
- Keep the existing cache-preservation behavior: frozen-prefix turns
stop emitting CCR retrieval markers instead of forcing tool injection
into the cached prefix.
- Make the skip decision use the effective frozen prefix after
token-mode reclamping, so turns that genuinely reclamp to zero still
keep normal reversible CCR behavior.
- Bypass cached marker reuse in both token mode and cache-mode prefix
replay when tool injection is deferred.
- Add focused regressions for the Anthropic request-path seams under
this bug:
- frozen-prefix turns do not emit marker-only payloads
- unfrozen turns still keep normal reversible CCR behavior
- token-mode reclamp back to zero still compresses normally
- existing `headroom_retrieve` tools keep reversible CCR on frozen turns
- cache-mode delta reuse and exact-prefix replay both forward original
content when retrieval is unavailable
- Add a `CHANGELOG.md` entry because the proxy's user-visible Anthropic
CCR behavior changes.
- Add a shared test skip helper for offline Hugging Face cache misses
and apply it to the merged `main` memory tests that were failing only in
the offline CI shards after the branch picked up current `main`.
## Testing
- [x] Unit tests pass (`uv run pytest
tests/test_proxy/test_anthropic_ccr_deferred_injection.py`)
- [x] Linting passes (`uv run ruff check . && uv run ruff format .
--check`)
- [ ] Type checking passes (`uv run mypy headroom`)
- [x] New tests added for new functionality when applicable
- [ ] Manual testing performed
### Test Output
```text
uv run pytest tests/test_proxy/test_anthropic_ccr_deferred_injection.py
14 passed, 1 warning in 34.19s
uv run pytest tests/test_memory/test_skip_helpers.py tests/test_memory/test_embedder_mps_serialization.py::test_cpu_uses_shared_executor tests/test_memory/test_hierarchical.py::TestLocalEmbedder::test_embed_single tests/test_memory_bridge.py::TestMemoryBridgeImport::test_import_claude_code_memory tests/test_memory_handler_concurrent_init.py::test_real_localbackend_initializes_via_public_entrypoint tests/test_memory_system.py::TestLocalBackend::test_save_memory_basic -q
4 passed, 5 skipped, 11 warnings in 7.65s
uv run ruff check .
All checks passed!
uv run ruff format . --check
968 files already formatted
```
## Real Behavior Proof
- Environment: local FastAPI `TestClient` for the Anthropic request
path, plus Windows Python 3.12 offline-memory repros with
`TRANSFORMERS_OFFLINE=1`
- Exact command / steps: Run the focused CCR regression command and the
offline-memory repro subset below on the merged branch state.
- `uv run pytest
tests/test_proxy/test_anthropic_ccr_deferred_injection.py`
- `uv run pytest tests/test_memory/test_skip_helpers.py
tests/test_memory/test_embedder_mps_serialization.py::test_cpu_uses_shared_executor
tests/test_memory/test_hierarchical.py::TestLocalEmbedder::test_embed_single
tests/test_memory_bridge.py::TestMemoryBridgeImport::test_import_claude_code_memory
tests/test_memory_handler_concurrent_init.py::test_real_localbackend_initializes_via_public_entrypoint
tests/test_memory_system.py::TestLocalBackend::test_save_memory_basic
-q`
- Scenario coverage from the CCR pytest command:
- frozen prefix with deferred tool injection and cached marker text
available in token mode
- unfrozen turn with normal CCR marker emission
- token mode where the tracked frozen prefix reclamps back to zero
- frozen prefix where the client already supplied `headroom_retrieve`
- cache-mode append-only delta reuse with a previously forwarded
compressed prefix
- cache-mode exact-prefix replay where the previous forwarded prefix
already contained a marker
- Scenario coverage from the offline-memory repro command:
- direct local embedder startup on CPU with no cached HF model
- hierarchical memory embedder startup with offline model cache missing
- bridge import through `LocalBackend`
- `MemoryHandler` public init warmup path
- `LocalBackend` save path under the offline lane
- Observed result: The CCR regression keeps marker-free forwarding on
frozen turns without tool availability, and the merged-`main`
offline-memory lanes now skip cleanly instead of failing unrelated CI
shards.
- frozen-prefix Anthropic turns without tool availability forward the
original long transcript across both token-mode and cache-mode reuse
paths, while unfrozen turns, reclamped token-mode turns, and frozen
turns that already advertise `headroom_retrieve` keep the reversible CCR
marker path
- the merged-`main` offline-memory regressions now skip cleanly when the
Hugging Face cache is unavailable instead of failing unrelated CI shards
- Not tested: live Zed session cache-hit behavior, provider latency
under real Anthropic upstreams, and online Hugging Face download lanes
## Review Readiness
- [x] I have performed a self-review
- [x] This PR is ready for human review
## Checklist
- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] I have updated the CHANGELOG.md if applicable
## Additional Notes
- Runtime scope is still intentionally narrow to Anthropic request-side
CCR. OpenAI, Gemini, streaming, and response-side CCR behavior are
unchanged.
- The only non-CCR diff is the test-only offline-memory follow-up
required after current `main` was merged into the branch.
- The focused proxy pytest run still emits the Windows-local
`StarletteDeprecationWarning` from `fastapi.testclient`'s `httpx`
bridge. The offline-memory repro command also emits existing datetime
deprecation warnings and a pytest teardown warning around skipped
offline lanes; none of those warnings were introduced by the CCR runtime
change.
2026-06-23 13:48:05 -04:00
|
|
|
"""Wrap test execution to skip transient or offline external model failures.
|
2026-01-30 16:37:34 -08:00
|
|
|
|
fix(ccr): skip Anthropic marker emission when tool injection is deferred (#1273)
## Description
Anthropic request-side CCR can still compress a turn into retrieval
markers after the frozen-prefix cache guard suppresses
`headroom_retrieve` registration. That leaves the model with marker-only
context it cannot redeem, so the proxy silently drops recoverable data
on exactly the turns where cache preservation deferred tool injection.
This change couples the Anthropic request-side CCR path to tool
availability so a turn never emits retrieve-only markers without the
retrieval tool, even when token mode or cache-mode prefix replay could
otherwise reuse already-compressed marker text. Closes #1006
After a collaborator merged current `main` into this branch, CI also
picked up unrelated offline-memory failures from the merged base. Those
follow-up changes are test-only: they keep the offline Hugging Face
cache lanes skipping cleanly instead of failing in memory tests that are
outside the CCR runtime path.
## 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
- Couple Anthropic request-side CCR compression to the same
frozen-prefix guard that already defers `headroom_retrieve`
registration.
- Keep the existing cache-preservation behavior: frozen-prefix turns
stop emitting CCR retrieval markers instead of forcing tool injection
into the cached prefix.
- Make the skip decision use the effective frozen prefix after
token-mode reclamping, so turns that genuinely reclamp to zero still
keep normal reversible CCR behavior.
- Bypass cached marker reuse in both token mode and cache-mode prefix
replay when tool injection is deferred.
- Add focused regressions for the Anthropic request-path seams under
this bug:
- frozen-prefix turns do not emit marker-only payloads
- unfrozen turns still keep normal reversible CCR behavior
- token-mode reclamp back to zero still compresses normally
- existing `headroom_retrieve` tools keep reversible CCR on frozen turns
- cache-mode delta reuse and exact-prefix replay both forward original
content when retrieval is unavailable
- Add a `CHANGELOG.md` entry because the proxy's user-visible Anthropic
CCR behavior changes.
- Add a shared test skip helper for offline Hugging Face cache misses
and apply it to the merged `main` memory tests that were failing only in
the offline CI shards after the branch picked up current `main`.
## Testing
- [x] Unit tests pass (`uv run pytest
tests/test_proxy/test_anthropic_ccr_deferred_injection.py`)
- [x] Linting passes (`uv run ruff check . && uv run ruff format .
--check`)
- [ ] Type checking passes (`uv run mypy headroom`)
- [x] New tests added for new functionality when applicable
- [ ] Manual testing performed
### Test Output
```text
uv run pytest tests/test_proxy/test_anthropic_ccr_deferred_injection.py
14 passed, 1 warning in 34.19s
uv run pytest tests/test_memory/test_skip_helpers.py tests/test_memory/test_embedder_mps_serialization.py::test_cpu_uses_shared_executor tests/test_memory/test_hierarchical.py::TestLocalEmbedder::test_embed_single tests/test_memory_bridge.py::TestMemoryBridgeImport::test_import_claude_code_memory tests/test_memory_handler_concurrent_init.py::test_real_localbackend_initializes_via_public_entrypoint tests/test_memory_system.py::TestLocalBackend::test_save_memory_basic -q
4 passed, 5 skipped, 11 warnings in 7.65s
uv run ruff check .
All checks passed!
uv run ruff format . --check
968 files already formatted
```
## Real Behavior Proof
- Environment: local FastAPI `TestClient` for the Anthropic request
path, plus Windows Python 3.12 offline-memory repros with
`TRANSFORMERS_OFFLINE=1`
- Exact command / steps: Run the focused CCR regression command and the
offline-memory repro subset below on the merged branch state.
- `uv run pytest
tests/test_proxy/test_anthropic_ccr_deferred_injection.py`
- `uv run pytest tests/test_memory/test_skip_helpers.py
tests/test_memory/test_embedder_mps_serialization.py::test_cpu_uses_shared_executor
tests/test_memory/test_hierarchical.py::TestLocalEmbedder::test_embed_single
tests/test_memory_bridge.py::TestMemoryBridgeImport::test_import_claude_code_memory
tests/test_memory_handler_concurrent_init.py::test_real_localbackend_initializes_via_public_entrypoint
tests/test_memory_system.py::TestLocalBackend::test_save_memory_basic
-q`
- Scenario coverage from the CCR pytest command:
- frozen prefix with deferred tool injection and cached marker text
available in token mode
- unfrozen turn with normal CCR marker emission
- token mode where the tracked frozen prefix reclamps back to zero
- frozen prefix where the client already supplied `headroom_retrieve`
- cache-mode append-only delta reuse with a previously forwarded
compressed prefix
- cache-mode exact-prefix replay where the previous forwarded prefix
already contained a marker
- Scenario coverage from the offline-memory repro command:
- direct local embedder startup on CPU with no cached HF model
- hierarchical memory embedder startup with offline model cache missing
- bridge import through `LocalBackend`
- `MemoryHandler` public init warmup path
- `LocalBackend` save path under the offline lane
- Observed result: The CCR regression keeps marker-free forwarding on
frozen turns without tool availability, and the merged-`main`
offline-memory lanes now skip cleanly instead of failing unrelated CI
shards.
- frozen-prefix Anthropic turns without tool availability forward the
original long transcript across both token-mode and cache-mode reuse
paths, while unfrozen turns, reclamped token-mode turns, and frozen
turns that already advertise `headroom_retrieve` keep the reversible CCR
marker path
- the merged-`main` offline-memory regressions now skip cleanly when the
Hugging Face cache is unavailable instead of failing unrelated CI shards
- Not tested: live Zed session cache-hit behavior, provider latency
under real Anthropic upstreams, and online Hugging Face download lanes
## Review Readiness
- [x] I have performed a self-review
- [x] This PR is ready for human review
## Checklist
- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] I have updated the CHANGELOG.md if applicable
## Additional Notes
- Runtime scope is still intentionally narrow to Anthropic request-side
CCR. OpenAI, Gemini, streaming, and response-side CCR behavior are
unchanged.
- The only non-CCR diff is the test-only offline-memory follow-up
required after current `main` was merged into the branch.
- The focused proxy pytest run still emits the Windows-local
`StarletteDeprecationWarning` from `fastapi.testclient`'s `httpx`
bridge. The offline-memory repro command also emits existing datetime
deprecation warnings and a pytest teardown warning around skipped
offline lanes; none of those warnings were introduced by the CCR runtime
change.
2026-06-23 13:48:05 -04:00
|
|
|
This handles model-loading failures that occur when:
|
2026-01-30 16:37:34 -08:00
|
|
|
- HuggingFace Hub is slow during model downloads (sentence-transformers)
|
fix(ccr): skip Anthropic marker emission when tool injection is deferred (#1273)
## Description
Anthropic request-side CCR can still compress a turn into retrieval
markers after the frozen-prefix cache guard suppresses
`headroom_retrieve` registration. That leaves the model with marker-only
context it cannot redeem, so the proxy silently drops recoverable data
on exactly the turns where cache preservation deferred tool injection.
This change couples the Anthropic request-side CCR path to tool
availability so a turn never emits retrieve-only markers without the
retrieval tool, even when token mode or cache-mode prefix replay could
otherwise reuse already-compressed marker text. Closes #1006
After a collaborator merged current `main` into this branch, CI also
picked up unrelated offline-memory failures from the merged base. Those
follow-up changes are test-only: they keep the offline Hugging Face
cache lanes skipping cleanly instead of failing in memory tests that are
outside the CCR runtime path.
## 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
- Couple Anthropic request-side CCR compression to the same
frozen-prefix guard that already defers `headroom_retrieve`
registration.
- Keep the existing cache-preservation behavior: frozen-prefix turns
stop emitting CCR retrieval markers instead of forcing tool injection
into the cached prefix.
- Make the skip decision use the effective frozen prefix after
token-mode reclamping, so turns that genuinely reclamp to zero still
keep normal reversible CCR behavior.
- Bypass cached marker reuse in both token mode and cache-mode prefix
replay when tool injection is deferred.
- Add focused regressions for the Anthropic request-path seams under
this bug:
- frozen-prefix turns do not emit marker-only payloads
- unfrozen turns still keep normal reversible CCR behavior
- token-mode reclamp back to zero still compresses normally
- existing `headroom_retrieve` tools keep reversible CCR on frozen turns
- cache-mode delta reuse and exact-prefix replay both forward original
content when retrieval is unavailable
- Add a `CHANGELOG.md` entry because the proxy's user-visible Anthropic
CCR behavior changes.
- Add a shared test skip helper for offline Hugging Face cache misses
and apply it to the merged `main` memory tests that were failing only in
the offline CI shards after the branch picked up current `main`.
## Testing
- [x] Unit tests pass (`uv run pytest
tests/test_proxy/test_anthropic_ccr_deferred_injection.py`)
- [x] Linting passes (`uv run ruff check . && uv run ruff format .
--check`)
- [ ] Type checking passes (`uv run mypy headroom`)
- [x] New tests added for new functionality when applicable
- [ ] Manual testing performed
### Test Output
```text
uv run pytest tests/test_proxy/test_anthropic_ccr_deferred_injection.py
14 passed, 1 warning in 34.19s
uv run pytest tests/test_memory/test_skip_helpers.py tests/test_memory/test_embedder_mps_serialization.py::test_cpu_uses_shared_executor tests/test_memory/test_hierarchical.py::TestLocalEmbedder::test_embed_single tests/test_memory_bridge.py::TestMemoryBridgeImport::test_import_claude_code_memory tests/test_memory_handler_concurrent_init.py::test_real_localbackend_initializes_via_public_entrypoint tests/test_memory_system.py::TestLocalBackend::test_save_memory_basic -q
4 passed, 5 skipped, 11 warnings in 7.65s
uv run ruff check .
All checks passed!
uv run ruff format . --check
968 files already formatted
```
## Real Behavior Proof
- Environment: local FastAPI `TestClient` for the Anthropic request
path, plus Windows Python 3.12 offline-memory repros with
`TRANSFORMERS_OFFLINE=1`
- Exact command / steps: Run the focused CCR regression command and the
offline-memory repro subset below on the merged branch state.
- `uv run pytest
tests/test_proxy/test_anthropic_ccr_deferred_injection.py`
- `uv run pytest tests/test_memory/test_skip_helpers.py
tests/test_memory/test_embedder_mps_serialization.py::test_cpu_uses_shared_executor
tests/test_memory/test_hierarchical.py::TestLocalEmbedder::test_embed_single
tests/test_memory_bridge.py::TestMemoryBridgeImport::test_import_claude_code_memory
tests/test_memory_handler_concurrent_init.py::test_real_localbackend_initializes_via_public_entrypoint
tests/test_memory_system.py::TestLocalBackend::test_save_memory_basic
-q`
- Scenario coverage from the CCR pytest command:
- frozen prefix with deferred tool injection and cached marker text
available in token mode
- unfrozen turn with normal CCR marker emission
- token mode where the tracked frozen prefix reclamps back to zero
- frozen prefix where the client already supplied `headroom_retrieve`
- cache-mode append-only delta reuse with a previously forwarded
compressed prefix
- cache-mode exact-prefix replay where the previous forwarded prefix
already contained a marker
- Scenario coverage from the offline-memory repro command:
- direct local embedder startup on CPU with no cached HF model
- hierarchical memory embedder startup with offline model cache missing
- bridge import through `LocalBackend`
- `MemoryHandler` public init warmup path
- `LocalBackend` save path under the offline lane
- Observed result: The CCR regression keeps marker-free forwarding on
frozen turns without tool availability, and the merged-`main`
offline-memory lanes now skip cleanly instead of failing unrelated CI
shards.
- frozen-prefix Anthropic turns without tool availability forward the
original long transcript across both token-mode and cache-mode reuse
paths, while unfrozen turns, reclamped token-mode turns, and frozen
turns that already advertise `headroom_retrieve` keep the reversible CCR
marker path
- the merged-`main` offline-memory regressions now skip cleanly when the
Hugging Face cache is unavailable instead of failing unrelated CI shards
- Not tested: live Zed session cache-hit behavior, provider latency
under real Anthropic upstreams, and online Hugging Face download lanes
## Review Readiness
- [x] I have performed a self-review
- [x] This PR is ready for human review
## Checklist
- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] I have updated the CHANGELOG.md if applicable
## Additional Notes
- Runtime scope is still intentionally narrow to Anthropic request-side
CCR. OpenAI, Gemini, streaming, and response-side CCR behavior are
unchanged.
- The only non-CCR diff is the test-only offline-memory follow-up
required after current `main` was merged into the branch.
- The focused proxy pytest run still emits the Windows-local
`StarletteDeprecationWarning` from `fastapi.testclient`'s `httpx`
bridge. The offline-memory repro command also emits existing datetime
deprecation warnings and a pytest teardown warning around skipped
offline lanes; none of those warnings were introduced by the CCR runtime
change.
2026-06-23 13:48:05 -04:00
|
|
|
- Required HuggingFace model files were not restored into the offline CI cache
|
2026-01-30 16:37:34 -08:00
|
|
|
- External embedding APIs timeout
|
|
|
|
|
- Network connectivity issues in CI
|
|
|
|
|
"""
|
|
|
|
|
outcome = yield
|
|
|
|
|
|
fix(ccr): skip Anthropic marker emission when tool injection is deferred (#1273)
## Description
Anthropic request-side CCR can still compress a turn into retrieval
markers after the frozen-prefix cache guard suppresses
`headroom_retrieve` registration. That leaves the model with marker-only
context it cannot redeem, so the proxy silently drops recoverable data
on exactly the turns where cache preservation deferred tool injection.
This change couples the Anthropic request-side CCR path to tool
availability so a turn never emits retrieve-only markers without the
retrieval tool, even when token mode or cache-mode prefix replay could
otherwise reuse already-compressed marker text. Closes #1006
After a collaborator merged current `main` into this branch, CI also
picked up unrelated offline-memory failures from the merged base. Those
follow-up changes are test-only: they keep the offline Hugging Face
cache lanes skipping cleanly instead of failing in memory tests that are
outside the CCR runtime path.
## 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
- Couple Anthropic request-side CCR compression to the same
frozen-prefix guard that already defers `headroom_retrieve`
registration.
- Keep the existing cache-preservation behavior: frozen-prefix turns
stop emitting CCR retrieval markers instead of forcing tool injection
into the cached prefix.
- Make the skip decision use the effective frozen prefix after
token-mode reclamping, so turns that genuinely reclamp to zero still
keep normal reversible CCR behavior.
- Bypass cached marker reuse in both token mode and cache-mode prefix
replay when tool injection is deferred.
- Add focused regressions for the Anthropic request-path seams under
this bug:
- frozen-prefix turns do not emit marker-only payloads
- unfrozen turns still keep normal reversible CCR behavior
- token-mode reclamp back to zero still compresses normally
- existing `headroom_retrieve` tools keep reversible CCR on frozen turns
- cache-mode delta reuse and exact-prefix replay both forward original
content when retrieval is unavailable
- Add a `CHANGELOG.md` entry because the proxy's user-visible Anthropic
CCR behavior changes.
- Add a shared test skip helper for offline Hugging Face cache misses
and apply it to the merged `main` memory tests that were failing only in
the offline CI shards after the branch picked up current `main`.
## Testing
- [x] Unit tests pass (`uv run pytest
tests/test_proxy/test_anthropic_ccr_deferred_injection.py`)
- [x] Linting passes (`uv run ruff check . && uv run ruff format .
--check`)
- [ ] Type checking passes (`uv run mypy headroom`)
- [x] New tests added for new functionality when applicable
- [ ] Manual testing performed
### Test Output
```text
uv run pytest tests/test_proxy/test_anthropic_ccr_deferred_injection.py
14 passed, 1 warning in 34.19s
uv run pytest tests/test_memory/test_skip_helpers.py tests/test_memory/test_embedder_mps_serialization.py::test_cpu_uses_shared_executor tests/test_memory/test_hierarchical.py::TestLocalEmbedder::test_embed_single tests/test_memory_bridge.py::TestMemoryBridgeImport::test_import_claude_code_memory tests/test_memory_handler_concurrent_init.py::test_real_localbackend_initializes_via_public_entrypoint tests/test_memory_system.py::TestLocalBackend::test_save_memory_basic -q
4 passed, 5 skipped, 11 warnings in 7.65s
uv run ruff check .
All checks passed!
uv run ruff format . --check
968 files already formatted
```
## Real Behavior Proof
- Environment: local FastAPI `TestClient` for the Anthropic request
path, plus Windows Python 3.12 offline-memory repros with
`TRANSFORMERS_OFFLINE=1`
- Exact command / steps: Run the focused CCR regression command and the
offline-memory repro subset below on the merged branch state.
- `uv run pytest
tests/test_proxy/test_anthropic_ccr_deferred_injection.py`
- `uv run pytest tests/test_memory/test_skip_helpers.py
tests/test_memory/test_embedder_mps_serialization.py::test_cpu_uses_shared_executor
tests/test_memory/test_hierarchical.py::TestLocalEmbedder::test_embed_single
tests/test_memory_bridge.py::TestMemoryBridgeImport::test_import_claude_code_memory
tests/test_memory_handler_concurrent_init.py::test_real_localbackend_initializes_via_public_entrypoint
tests/test_memory_system.py::TestLocalBackend::test_save_memory_basic
-q`
- Scenario coverage from the CCR pytest command:
- frozen prefix with deferred tool injection and cached marker text
available in token mode
- unfrozen turn with normal CCR marker emission
- token mode where the tracked frozen prefix reclamps back to zero
- frozen prefix where the client already supplied `headroom_retrieve`
- cache-mode append-only delta reuse with a previously forwarded
compressed prefix
- cache-mode exact-prefix replay where the previous forwarded prefix
already contained a marker
- Scenario coverage from the offline-memory repro command:
- direct local embedder startup on CPU with no cached HF model
- hierarchical memory embedder startup with offline model cache missing
- bridge import through `LocalBackend`
- `MemoryHandler` public init warmup path
- `LocalBackend` save path under the offline lane
- Observed result: The CCR regression keeps marker-free forwarding on
frozen turns without tool availability, and the merged-`main`
offline-memory lanes now skip cleanly instead of failing unrelated CI
shards.
- frozen-prefix Anthropic turns without tool availability forward the
original long transcript across both token-mode and cache-mode reuse
paths, while unfrozen turns, reclamped token-mode turns, and frozen
turns that already advertise `headroom_retrieve` keep the reversible CCR
marker path
- the merged-`main` offline-memory regressions now skip cleanly when the
Hugging Face cache is unavailable instead of failing unrelated CI shards
- Not tested: live Zed session cache-hit behavior, provider latency
under real Anthropic upstreams, and online Hugging Face download lanes
## Review Readiness
- [x] I have performed a self-review
- [x] This PR is ready for human review
## Checklist
- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] I have updated the CHANGELOG.md if applicable
## Additional Notes
- Runtime scope is still intentionally narrow to Anthropic request-side
CCR. OpenAI, Gemini, streaming, and response-side CCR behavior are
unchanged.
- The only non-CCR diff is the test-only offline-memory follow-up
required after current `main` was merged into the branch.
- The focused proxy pytest run still emits the Windows-local
`StarletteDeprecationWarning` from `fastapi.testclient`'s `httpx`
bridge. The offline-memory repro command also emits existing datetime
deprecation warnings and a pytest teardown warning around skipped
offline lanes; none of those warnings were introduced by the CCR runtime
change.
2026-06-23 13:48:05 -04:00
|
|
|
if outcome.excinfo is not None:
|
2026-01-30 16:37:34 -08:00
|
|
|
exc_type, exc_value, exc_tb = outcome.excinfo
|
fix(ccr): skip Anthropic marker emission when tool injection is deferred (#1273)
## Description
Anthropic request-side CCR can still compress a turn into retrieval
markers after the frozen-prefix cache guard suppresses
`headroom_retrieve` registration. That leaves the model with marker-only
context it cannot redeem, so the proxy silently drops recoverable data
on exactly the turns where cache preservation deferred tool injection.
This change couples the Anthropic request-side CCR path to tool
availability so a turn never emits retrieve-only markers without the
retrieval tool, even when token mode or cache-mode prefix replay could
otherwise reuse already-compressed marker text. Closes #1006
After a collaborator merged current `main` into this branch, CI also
picked up unrelated offline-memory failures from the merged base. Those
follow-up changes are test-only: they keep the offline Hugging Face
cache lanes skipping cleanly instead of failing in memory tests that are
outside the CCR runtime path.
## 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
- Couple Anthropic request-side CCR compression to the same
frozen-prefix guard that already defers `headroom_retrieve`
registration.
- Keep the existing cache-preservation behavior: frozen-prefix turns
stop emitting CCR retrieval markers instead of forcing tool injection
into the cached prefix.
- Make the skip decision use the effective frozen prefix after
token-mode reclamping, so turns that genuinely reclamp to zero still
keep normal reversible CCR behavior.
- Bypass cached marker reuse in both token mode and cache-mode prefix
replay when tool injection is deferred.
- Add focused regressions for the Anthropic request-path seams under
this bug:
- frozen-prefix turns do not emit marker-only payloads
- unfrozen turns still keep normal reversible CCR behavior
- token-mode reclamp back to zero still compresses normally
- existing `headroom_retrieve` tools keep reversible CCR on frozen turns
- cache-mode delta reuse and exact-prefix replay both forward original
content when retrieval is unavailable
- Add a `CHANGELOG.md` entry because the proxy's user-visible Anthropic
CCR behavior changes.
- Add a shared test skip helper for offline Hugging Face cache misses
and apply it to the merged `main` memory tests that were failing only in
the offline CI shards after the branch picked up current `main`.
## Testing
- [x] Unit tests pass (`uv run pytest
tests/test_proxy/test_anthropic_ccr_deferred_injection.py`)
- [x] Linting passes (`uv run ruff check . && uv run ruff format .
--check`)
- [ ] Type checking passes (`uv run mypy headroom`)
- [x] New tests added for new functionality when applicable
- [ ] Manual testing performed
### Test Output
```text
uv run pytest tests/test_proxy/test_anthropic_ccr_deferred_injection.py
14 passed, 1 warning in 34.19s
uv run pytest tests/test_memory/test_skip_helpers.py tests/test_memory/test_embedder_mps_serialization.py::test_cpu_uses_shared_executor tests/test_memory/test_hierarchical.py::TestLocalEmbedder::test_embed_single tests/test_memory_bridge.py::TestMemoryBridgeImport::test_import_claude_code_memory tests/test_memory_handler_concurrent_init.py::test_real_localbackend_initializes_via_public_entrypoint tests/test_memory_system.py::TestLocalBackend::test_save_memory_basic -q
4 passed, 5 skipped, 11 warnings in 7.65s
uv run ruff check .
All checks passed!
uv run ruff format . --check
968 files already formatted
```
## Real Behavior Proof
- Environment: local FastAPI `TestClient` for the Anthropic request
path, plus Windows Python 3.12 offline-memory repros with
`TRANSFORMERS_OFFLINE=1`
- Exact command / steps: Run the focused CCR regression command and the
offline-memory repro subset below on the merged branch state.
- `uv run pytest
tests/test_proxy/test_anthropic_ccr_deferred_injection.py`
- `uv run pytest tests/test_memory/test_skip_helpers.py
tests/test_memory/test_embedder_mps_serialization.py::test_cpu_uses_shared_executor
tests/test_memory/test_hierarchical.py::TestLocalEmbedder::test_embed_single
tests/test_memory_bridge.py::TestMemoryBridgeImport::test_import_claude_code_memory
tests/test_memory_handler_concurrent_init.py::test_real_localbackend_initializes_via_public_entrypoint
tests/test_memory_system.py::TestLocalBackend::test_save_memory_basic
-q`
- Scenario coverage from the CCR pytest command:
- frozen prefix with deferred tool injection and cached marker text
available in token mode
- unfrozen turn with normal CCR marker emission
- token mode where the tracked frozen prefix reclamps back to zero
- frozen prefix where the client already supplied `headroom_retrieve`
- cache-mode append-only delta reuse with a previously forwarded
compressed prefix
- cache-mode exact-prefix replay where the previous forwarded prefix
already contained a marker
- Scenario coverage from the offline-memory repro command:
- direct local embedder startup on CPU with no cached HF model
- hierarchical memory embedder startup with offline model cache missing
- bridge import through `LocalBackend`
- `MemoryHandler` public init warmup path
- `LocalBackend` save path under the offline lane
- Observed result: The CCR regression keeps marker-free forwarding on
frozen turns without tool availability, and the merged-`main`
offline-memory lanes now skip cleanly instead of failing unrelated CI
shards.
- frozen-prefix Anthropic turns without tool availability forward the
original long transcript across both token-mode and cache-mode reuse
paths, while unfrozen turns, reclamped token-mode turns, and frozen
turns that already advertise `headroom_retrieve` keep the reversible CCR
marker path
- the merged-`main` offline-memory regressions now skip cleanly when the
Hugging Face cache is unavailable instead of failing unrelated CI shards
- Not tested: live Zed session cache-hit behavior, provider latency
under real Anthropic upstreams, and online Hugging Face download lanes
## Review Readiness
- [x] I have performed a self-review
- [x] This PR is ready for human review
## Checklist
- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] I have updated the CHANGELOG.md if applicable
## Additional Notes
- Runtime scope is still intentionally narrow to Anthropic request-side
CCR. OpenAI, Gemini, streaming, and response-side CCR behavior are
unchanged.
- The only non-CCR diff is the test-only offline-memory follow-up
required after current `main` was merged into the branch.
- The focused proxy pytest run still emits the Windows-local
`StarletteDeprecationWarning` from `fastapi.testclient`'s `httpx`
bridge. The offline-memory repro command also emits existing datetime
deprecation warnings and a pytest teardown warning around skipped
offline lanes; none of those warnings were introduced by the CCR runtime
change.
2026-06-23 13:48:05 -04:00
|
|
|
reason = external_model_skip_reason(exc_value)
|
|
|
|
|
if reason is not None:
|
|
|
|
|
pytest.skip(reason)
|
2026-01-30 16:37:34 -08:00
|
|
|
|
|
|
|
|
|
2026-08-20 07:02:44 -07:00
|
|
|
@pytest.fixture(autouse=True)
|
|
|
|
|
def _null_binary_pins():
|
|
|
|
|
"""Null the tools.json SHA-256 pins during tests.
|
|
|
|
|
|
|
|
|
|
Installer tests fetch small mock archives, whose digests can't match the
|
|
|
|
|
real published pins. Nulling the pins lets those download/extract mechanics
|
|
|
|
|
tests run (verification then falls back to HTTPS trust); the tests that
|
|
|
|
|
specifically exercise verification set their own pin explicitly. Production
|
|
|
|
|
keeps the real pins (this fixture is test-only) and the tools-hash-refresh
|
|
|
|
|
CI gate guarantees they stay correct.
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
from headroom import binaries
|
|
|
|
|
except Exception:
|
|
|
|
|
# Lean CI environments (e.g. the native-installer jobs) omit heavy deps
|
|
|
|
|
# such as opentelemetry that importing `binaries` pulls in. There are no
|
|
|
|
|
# tool pins to null there, so skip cleanly rather than erroring at setup.
|
|
|
|
|
yield
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
saved = [
|
|
|
|
|
(asset, asset.get("sha256"))
|
|
|
|
|
for tool in binaries._registry().get("tools", {}).values()
|
|
|
|
|
for asset in tool.get("assets", {}).values()
|
|
|
|
|
]
|
|
|
|
|
for asset, _original in saved:
|
|
|
|
|
asset["sha256"] = None
|
|
|
|
|
yield
|
|
|
|
|
for asset, original in saved:
|
|
|
|
|
asset["sha256"] = original
|
|
|
|
|
|
|
|
|
|
|
fix: B1 — retire ICM, RollingWindow, scoring, relevance + dependents
Phase B step 1 of the live-zone-only realignment. Removes ~10K LOC of
"drop messages from history" machinery that became unreachable after
PR-A1 made `/v1/messages` a passthrough on the proxy. Live-zone-only
compression (PR-B2..B7) operates on content blocks within messages;
message-list mutation no longer happens in the pipeline.
Python deletes:
- headroom/transforms/intelligent_context.py (1077 LOC)
- headroom/transforms/rolling_window.py (395 LOC)
- headroom/transforms/progressive_summarizer.py (508 LOC)
- headroom/transforms/scoring.py (459 LOC)
- headroom/transforms/tool_crusher.py (338 LOC)
- 5 corresponding tests/test_transforms/* and tests/test_proxy_intelligent_context.py
Rust deletes:
- crates/headroom-core/src/context/* (manager, config, workspace,
candidate, ccr_drop, strategy/, mod) + safety.rs replaced
- crates/headroom-core/src/scoring/* (mod, score, scorer, traits, weights)
- MessageScorerComparator from crates/headroom-parity (PR #338/#343
becomes deletable; sunk cost stays sunk)
- 13 message_scorer fixtures + record_message_scorer.py
Rust adds (move + rewrite):
- crates/headroom-core/src/transforms/safety.rs — `tool_pair_indices`
preserves the OpenAI/Anthropic tool_use ↔ tool_result pairing rule
the live-zone dispatcher (PR-B2) needs. No IcmConfig dependency.
Surface refactors:
- HeadroomConfig: drop `tool_crusher`, `rolling_window`,
`intelligent_context` fields; hoist `output_buffer_tokens` to top
level (used by client.py).
- ProxyConfig: drop `intelligent_context*` fields.
- `headroom wrap` proxy server: retire IntelligentContextManager
and RollingWindow imports + branch; pipeline is CacheAligner →
ContentRouter (smart_routing) or CacheAligner → SmartCrusher
(legacy).
- CLI: drop `--no-intelligent-context`, `--no-intelligent-scoring`,
`--no-compress-first` flags.
- LangChain memory integration: rename `_apply_rolling_window` →
`_apply_compression`, drop RollingWindowConfig dep. Threshold is
now advisory — B6 will rework the contract.
- TransformPipeline.create_pipeline now takes only cache_aligner_config.
- headroom/__init__.py + headroom/transforms/__init__.py: strip
exports of deleted symbols.
Bug fixes uncovered by full pytest sweep:
- providers/copilot/wrap.py: `environ or os.environ` collapsed
empty-dict to falsy → callers passing `environ={}` accidentally
pulled from os.environ. Use `environ if environ is not None else
os.environ`.
Test correctness fixes:
- _DummyAnthropicHandler._retry_request gains **_kwargs to match
the real handler signature post-A8.
- test_ws_http_fallback extracts JSON from `content=` (post-A3
byte-faithful) rather than the obsolete `json=` kwarg.
- test_ccr_response_handler_extra fixture joins SSE events with
`\n\n` per spec (post-A8 byte-buffer parser requirement).
- test_proxy_responses_phase_preservation: capture via direct
handler attached to the named logger, so the assertion is
order-independent (proxy `_setup_file_logging` flips
`headroom.propagate=False` once any earlier test triggers it).
- conftest.py autouse fixture resets `headroom.propagate=True`
before each test as a defensive measure for the same pollution.
- test_wrap_copilot_translated_backend_still_requires_byok:
monkeypatch.delenv every provider key so the BYOK error
actually fires.
- test_native_installers: skip when system bash < 4.3 (macOS ships 3.2).
- TestGeminiEmbedContent / TestGeminiBatchEmbedContents:
pytest.mark.skip — proxy currently has no :embedContent route;
feature gap, not regression.
Acceptance:
- cargo build --workspace + cargo clippy + cargo fmt --check: green.
- cargo test --workspace --exclude headroom-py: 777 passed.
- pytest: 4892 passed, 240 skipped, 0 failed.
- git grep returns only intentional comments referencing the deletion.
Per-PR-B1 plan: REALIGNMENT/04-phase-B-live-zone.md.
2026-05-02 12:23:17 -07:00
|
|
|
@pytest.fixture(autouse=True)
|
|
|
|
|
def _reset_headroom_logger_propagation():
|
|
|
|
|
"""Keep `headroom.*` log records flowing to pytest's caplog handler.
|
|
|
|
|
|
fix(tests): reset whole headroom logger subtree so caplog stays deterministic (#1117)
## Description
Five `caplog`-based test assertions are order-dependent flakes: they
pass in isolation but fail in full-suite runs.
**Root cause** is a global logging-state leak.
`benchmarks.claude_session_mode_benchmark._disable_headroom_benchmark_logging()`
(exercised by `tests/test_claude_session_mode_benchmark.py`) sets
`propagate = False` + `CRITICAL` on the `headroom`, `headroom.proxy`,
`headroom.transforms` (and `headroom.cache`) loggers and never restores
them. pytest's `caplog` attaches its handler to the **root** logger, so
once any `headroom.*` child is left non-propagating, records from that
subtree silently never reach `caplog` for **every test that runs
afterwards** — which is exactly why these only fail in full-suite order.
The repo already ships a `_reset_headroom_logger_propagation` autouse
fixture for this hazard (its docstring documents the equivalent
`_setup_file_logging` leak), but it only reset the **top** `headroom`
logger, not children like `headroom.proxy`. A non-propagating child
still blocks the record before it reaches root. This PR extends the
existing fixture to reset the whole `headroom.*` subtree before each
test.
Scope is intentionally one file (`tests/conftest.py`) — test-harness
only, no production change.
> Design note: I extended the existing defensive fixture rather than
restoring state inside the benchmark, because (a) the fixture already
exists for exactly this and only needed completing, and (b) the same
`propagate=False` hazard also originates from production
`_setup_file_logging`, so a centralized per-test reset is the more
durable fix. Happy to instead make the benchmark restore its own logging
state if maintainers prefer fixing it at the source.
## 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
- Extend the `_reset_headroom_logger_propagation` autouse fixture in
`tests/conftest.py` to reset `propagate = True` for **every** existing
`headroom.*` logger (previously only the top `headroom` logger), so
`caplog` capture is deterministic regardless of test execution order.
## Testing
- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [ ] Type checking passes (`mypy headroom`) — N/A, change is under
`tests/`
- [ ] New tests added — N/A, this fixes existing tests; they are
themselves the proof
- [x] Manual testing performed
### Test Output
```text
# Causal proof: run the polluter first, then the 5 victims, in one process.
# BEFORE (fixture reset scoped to only "headroom"):
$ pytest tests/test_claude_session_mode_benchmark.py \
tests/test_corrupt_golden_bytes_recovery.py \
tests/test_forwarded_headers.py \
'tests/test_transforms/test_kompress_compressor.py::TestKompressBackendSelection::test_unrecognized_backend_warns_and_falls_back_to_auto'
5 failed, 54 passed
FAILED tests/test_corrupt_golden_bytes_recovery.py::TestCorruptMemoryGoldenBytes::test_corrupt_bytes_logs_error
FAILED tests/test_corrupt_golden_bytes_recovery.py::TestCorruptMemoryGoldenBytes::test_unicode_decode_error_handled
FAILED tests/test_corrupt_golden_bytes_recovery.py::TestCorruptCcrGoldenBytes::test_corrupt_bytes_logs_error
FAILED tests/test_forwarded_headers.py::test_non_allowlisted_peer_ignores_forwarded_and_logs
FAILED tests/test_transforms/test_kompress_compressor.py::...test_unrecognized_backend_warns_and_falls_back_to_auto
# AFTER (this PR — whole headroom.* subtree reset):
$ pytest <same selection>
59 passed
# Full suite (Rust core rebuilt locally):
$ pytest
6251 passed, 496 skipped
$ ruff check .
All checks passed!
$ ruff format --check tests/conftest.py
1 file already formatted
```
## Real Behavior Proof
- Environment: macOS, Python 3.13.3, branch off latest `main`, Rust
`_core` rebuilt locally (`uv pip install -e .`).
- Exact command / steps: ran the polluter
(`test_claude_session_mode_benchmark`) together with the 5 victim tests
in one process to reproduce the order-dependent failure, then toggled
**only** the fixture change to confirm causality; then ran the full
`pytest` suite and `ruff`.
- Observed result: scoping the reset to only `"headroom"` → 5 failed /
54 passed; extending it to the `headroom.*` subtree → 59 passed. Full
suite: 6251 passed, 496 skipped, 0 failed. Lint clean.
- Not tested: behavior under CI's sharded `test (N)` jobs specifically —
but the fix is order-independent (resets before *every* test), so
sharding cannot reintroduce the leak.
## 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
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective — the 5
previously-flaky tests are the proof
- [x] New and existing unit tests pass locally with my changes
- [ ] I have updated the CHANGELOG.md — N/A (test-harness only)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-06-27 01:05:26 +08:00
|
|
|
Two sources disable propagation on the headroom logger tree and never
|
|
|
|
|
restore it, which then makes later `caplog`-based assertions flaky in
|
|
|
|
|
full-suite runs (caplog attaches to root, so a `propagate=False` anywhere
|
|
|
|
|
on the chain silently drops the records):
|
|
|
|
|
|
|
|
|
|
- ``headroom.proxy.helpers._setup_file_logging`` sets
|
|
|
|
|
``getLogger("headroom").propagate = False`` on proxy startup.
|
|
|
|
|
- ``benchmarks.claude_session_mode_benchmark._disable_headroom_benchmark_logging``
|
|
|
|
|
(exercised by ``test_claude_session_mode_benchmark``) sets
|
|
|
|
|
``propagate = False`` + ``CRITICAL`` on ``headroom``, ``headroom.proxy``,
|
|
|
|
|
``headroom.transforms``, ``headroom.cache`` (and children).
|
|
|
|
|
|
|
|
|
|
Resetting only ``"headroom"`` is not enough — a child like
|
|
|
|
|
``"headroom.proxy"`` left non-propagating blocks the record before it
|
|
|
|
|
reaches root. Reset the whole subtree before every test so capture is
|
|
|
|
|
deterministic regardless of run order.
|
fix: B1 — retire ICM, RollingWindow, scoring, relevance + dependents
Phase B step 1 of the live-zone-only realignment. Removes ~10K LOC of
"drop messages from history" machinery that became unreachable after
PR-A1 made `/v1/messages` a passthrough on the proxy. Live-zone-only
compression (PR-B2..B7) operates on content blocks within messages;
message-list mutation no longer happens in the pipeline.
Python deletes:
- headroom/transforms/intelligent_context.py (1077 LOC)
- headroom/transforms/rolling_window.py (395 LOC)
- headroom/transforms/progressive_summarizer.py (508 LOC)
- headroom/transforms/scoring.py (459 LOC)
- headroom/transforms/tool_crusher.py (338 LOC)
- 5 corresponding tests/test_transforms/* and tests/test_proxy_intelligent_context.py
Rust deletes:
- crates/headroom-core/src/context/* (manager, config, workspace,
candidate, ccr_drop, strategy/, mod) + safety.rs replaced
- crates/headroom-core/src/scoring/* (mod, score, scorer, traits, weights)
- MessageScorerComparator from crates/headroom-parity (PR #338/#343
becomes deletable; sunk cost stays sunk)
- 13 message_scorer fixtures + record_message_scorer.py
Rust adds (move + rewrite):
- crates/headroom-core/src/transforms/safety.rs — `tool_pair_indices`
preserves the OpenAI/Anthropic tool_use ↔ tool_result pairing rule
the live-zone dispatcher (PR-B2) needs. No IcmConfig dependency.
Surface refactors:
- HeadroomConfig: drop `tool_crusher`, `rolling_window`,
`intelligent_context` fields; hoist `output_buffer_tokens` to top
level (used by client.py).
- ProxyConfig: drop `intelligent_context*` fields.
- `headroom wrap` proxy server: retire IntelligentContextManager
and RollingWindow imports + branch; pipeline is CacheAligner →
ContentRouter (smart_routing) or CacheAligner → SmartCrusher
(legacy).
- CLI: drop `--no-intelligent-context`, `--no-intelligent-scoring`,
`--no-compress-first` flags.
- LangChain memory integration: rename `_apply_rolling_window` →
`_apply_compression`, drop RollingWindowConfig dep. Threshold is
now advisory — B6 will rework the contract.
- TransformPipeline.create_pipeline now takes only cache_aligner_config.
- headroom/__init__.py + headroom/transforms/__init__.py: strip
exports of deleted symbols.
Bug fixes uncovered by full pytest sweep:
- providers/copilot/wrap.py: `environ or os.environ` collapsed
empty-dict to falsy → callers passing `environ={}` accidentally
pulled from os.environ. Use `environ if environ is not None else
os.environ`.
Test correctness fixes:
- _DummyAnthropicHandler._retry_request gains **_kwargs to match
the real handler signature post-A8.
- test_ws_http_fallback extracts JSON from `content=` (post-A3
byte-faithful) rather than the obsolete `json=` kwarg.
- test_ccr_response_handler_extra fixture joins SSE events with
`\n\n` per spec (post-A8 byte-buffer parser requirement).
- test_proxy_responses_phase_preservation: capture via direct
handler attached to the named logger, so the assertion is
order-independent (proxy `_setup_file_logging` flips
`headroom.propagate=False` once any earlier test triggers it).
- conftest.py autouse fixture resets `headroom.propagate=True`
before each test as a defensive measure for the same pollution.
- test_wrap_copilot_translated_backend_still_requires_byok:
monkeypatch.delenv every provider key so the BYOK error
actually fires.
- test_native_installers: skip when system bash < 4.3 (macOS ships 3.2).
- TestGeminiEmbedContent / TestGeminiBatchEmbedContents:
pytest.mark.skip — proxy currently has no :embedContent route;
feature gap, not regression.
Acceptance:
- cargo build --workspace + cargo clippy + cargo fmt --check: green.
- cargo test --workspace --exclude headroom-py: 777 passed.
- pytest: 4892 passed, 240 skipped, 0 failed.
- git grep returns only intentional comments referencing the deletion.
Per-PR-B1 plan: REALIGNMENT/04-phase-B-live-zone.md.
2026-05-02 12:23:17 -07:00
|
|
|
"""
|
|
|
|
|
import logging as _logging
|
|
|
|
|
|
fix(tests): reset whole headroom logger subtree so caplog stays deterministic (#1117)
## Description
Five `caplog`-based test assertions are order-dependent flakes: they
pass in isolation but fail in full-suite runs.
**Root cause** is a global logging-state leak.
`benchmarks.claude_session_mode_benchmark._disable_headroom_benchmark_logging()`
(exercised by `tests/test_claude_session_mode_benchmark.py`) sets
`propagate = False` + `CRITICAL` on the `headroom`, `headroom.proxy`,
`headroom.transforms` (and `headroom.cache`) loggers and never restores
them. pytest's `caplog` attaches its handler to the **root** logger, so
once any `headroom.*` child is left non-propagating, records from that
subtree silently never reach `caplog` for **every test that runs
afterwards** — which is exactly why these only fail in full-suite order.
The repo already ships a `_reset_headroom_logger_propagation` autouse
fixture for this hazard (its docstring documents the equivalent
`_setup_file_logging` leak), but it only reset the **top** `headroom`
logger, not children like `headroom.proxy`. A non-propagating child
still blocks the record before it reaches root. This PR extends the
existing fixture to reset the whole `headroom.*` subtree before each
test.
Scope is intentionally one file (`tests/conftest.py`) — test-harness
only, no production change.
> Design note: I extended the existing defensive fixture rather than
restoring state inside the benchmark, because (a) the fixture already
exists for exactly this and only needed completing, and (b) the same
`propagate=False` hazard also originates from production
`_setup_file_logging`, so a centralized per-test reset is the more
durable fix. Happy to instead make the benchmark restore its own logging
state if maintainers prefer fixing it at the source.
## 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
- Extend the `_reset_headroom_logger_propagation` autouse fixture in
`tests/conftest.py` to reset `propagate = True` for **every** existing
`headroom.*` logger (previously only the top `headroom` logger), so
`caplog` capture is deterministic regardless of test execution order.
## Testing
- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [ ] Type checking passes (`mypy headroom`) — N/A, change is under
`tests/`
- [ ] New tests added — N/A, this fixes existing tests; they are
themselves the proof
- [x] Manual testing performed
### Test Output
```text
# Causal proof: run the polluter first, then the 5 victims, in one process.
# BEFORE (fixture reset scoped to only "headroom"):
$ pytest tests/test_claude_session_mode_benchmark.py \
tests/test_corrupt_golden_bytes_recovery.py \
tests/test_forwarded_headers.py \
'tests/test_transforms/test_kompress_compressor.py::TestKompressBackendSelection::test_unrecognized_backend_warns_and_falls_back_to_auto'
5 failed, 54 passed
FAILED tests/test_corrupt_golden_bytes_recovery.py::TestCorruptMemoryGoldenBytes::test_corrupt_bytes_logs_error
FAILED tests/test_corrupt_golden_bytes_recovery.py::TestCorruptMemoryGoldenBytes::test_unicode_decode_error_handled
FAILED tests/test_corrupt_golden_bytes_recovery.py::TestCorruptCcrGoldenBytes::test_corrupt_bytes_logs_error
FAILED tests/test_forwarded_headers.py::test_non_allowlisted_peer_ignores_forwarded_and_logs
FAILED tests/test_transforms/test_kompress_compressor.py::...test_unrecognized_backend_warns_and_falls_back_to_auto
# AFTER (this PR — whole headroom.* subtree reset):
$ pytest <same selection>
59 passed
# Full suite (Rust core rebuilt locally):
$ pytest
6251 passed, 496 skipped
$ ruff check .
All checks passed!
$ ruff format --check tests/conftest.py
1 file already formatted
```
## Real Behavior Proof
- Environment: macOS, Python 3.13.3, branch off latest `main`, Rust
`_core` rebuilt locally (`uv pip install -e .`).
- Exact command / steps: ran the polluter
(`test_claude_session_mode_benchmark`) together with the 5 victim tests
in one process to reproduce the order-dependent failure, then toggled
**only** the fixture change to confirm causality; then ran the full
`pytest` suite and `ruff`.
- Observed result: scoping the reset to only `"headroom"` → 5 failed /
54 passed; extending it to the `headroom.*` subtree → 59 passed. Full
suite: 6251 passed, 496 skipped, 0 failed. Lint clean.
- Not tested: behavior under CI's sharded `test (N)` jobs specifically —
but the fix is order-independent (resets before *every* test), so
sharding cannot reintroduce the leak.
## 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
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective — the 5
previously-flaky tests are the proof
- [x] New and existing unit tests pass locally with my changes
- [ ] I have updated the CHANGELOG.md — N/A (test-harness only)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-06-27 01:05:26 +08:00
|
|
|
for _name in ("headroom", *list(_logging.root.manager.loggerDict)):
|
|
|
|
|
if _name == "headroom" or _name.startswith("headroom."):
|
|
|
|
|
logger = _logging.getLogger(_name)
|
|
|
|
|
logger.disabled = False
|
fix(proxy): dedupe Codex WS request logging for accurate mixed-provider dashboards (#2189)
## Description
Running Claude Code (Anthropic) and Codex (OpenAI) against the **same**
Headroom proxy instance on one port produced incorrect, unstable
dashboard data. The proxy core is provider-isolated and
multi-provider-safe by design; the defect was in the observability
layer. The Codex `/v1/responses` **WebSocket** handler was the only path
in the proxy that wrote to the request logger by hand instead of through
the unified `emit_request_outcome` funnel, and it did so twice per
session close: the per-turn funnel record plus an unconditional
cumulative session-summary `RequestLog`. This PR removes the duplicate
summary log so Codex WS emits exactly one request log per turn, matching
the HTTP provider paths.
## 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
- Dropped the duplicate cumulative session-summary `RequestLog` in the
Codex WS handler while preserving the per-turn `emit_request_outcome`
path.
- Preserved gated `request_messages` and `turn_id` on residual outcomes
so dashboard telemetry keeps the useful attribution without
double-counting tokens.
- Ensured explicit `--anyllm-provider` wins over a leaked
`HEADROOM_ANYLLM_PROVIDER` environment variable.
- Registered retry delay settings that had drifted out of the settings
registry.
- Hardened tests against developer-shell `HEADROOM_*` /
`ANTHROPIC_CUSTOM_HEADERS` leakage and stabilized several focused
proxy/wrap test fixtures.
## Testing
- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [ ] Type checking passes (`mypy headroom`)
- [x] New tests added for new functionality
- [x] Manual testing performed
### Test Output
```text
$ .venv/bin/pytest tests/ -q -p no:cacheprovider
8529 passed, 537 skipped, 5831 warnings in 276.25s (0:04:36)
$ .venv/bin/ruff check <touched files>
All checks passed!
```
## Real Behavior Proof
- Environment: macOS (Darwin 25.4.0), Python 3.13.14, pytest 9.0.3, ruff
via project venv, branch `fix/multi-provider-runtime`.
- Exact command / steps: Ran the full test suite without pytest cache
provider and Ruff on all touched files; used `git stash` to confirm the
stale fake-config failures pre-existed this change.
- Observed result: Full suite passed with no failures; Ruff passed;
Codex WS now routes end-of-session logging through
`emit_request_outcome`, emitting one request log per turn with the same
accounting model as Anthropic HTTP turns.
- Not tested: Live simultaneous Claude + Codex dashboard run. `mypy
headroom` was not run to completion; a scoped run reported one
pre-existing `settings_store.py:470` coercion error outside this diff.
## 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 - server-side observability fix; no UI markup changed.
## Additional Notes
- The proxy's multi-provider routing, header/auth isolation, and
per-model cache keying are already correct and unchanged here; only the
WS observability write path was double-counting.
- Architectural assessment:
`plans/reports/research-260714-0004-multi-provider-upstream-compression-report.md`;
root-cause + resolution trail:
`plans/reports/debug-assessment-260714-0011-dashboard-instability-mixed-claude-codex-report.md`.
- No live simultaneous Claude + Codex dashboard run was performed;
validation is from test coverage and code review of the WS logging path.
---------
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-16 01:18:34 +07:00
|
|
|
# The benchmark also raises the level to CRITICAL; children
|
|
|
|
|
# inherit it (effective level), so a WARNING would be filtered
|
|
|
|
|
# at the logger before it can propagate to caplog. Reset to
|
|
|
|
|
# NOTSET so the subtree inherits root's level deterministically.
|
|
|
|
|
logger.setLevel(_logging.NOTSET)
|
fix(tests): reset whole headroom logger subtree so caplog stays deterministic (#1117)
## Description
Five `caplog`-based test assertions are order-dependent flakes: they
pass in isolation but fail in full-suite runs.
**Root cause** is a global logging-state leak.
`benchmarks.claude_session_mode_benchmark._disable_headroom_benchmark_logging()`
(exercised by `tests/test_claude_session_mode_benchmark.py`) sets
`propagate = False` + `CRITICAL` on the `headroom`, `headroom.proxy`,
`headroom.transforms` (and `headroom.cache`) loggers and never restores
them. pytest's `caplog` attaches its handler to the **root** logger, so
once any `headroom.*` child is left non-propagating, records from that
subtree silently never reach `caplog` for **every test that runs
afterwards** — which is exactly why these only fail in full-suite order.
The repo already ships a `_reset_headroom_logger_propagation` autouse
fixture for this hazard (its docstring documents the equivalent
`_setup_file_logging` leak), but it only reset the **top** `headroom`
logger, not children like `headroom.proxy`. A non-propagating child
still blocks the record before it reaches root. This PR extends the
existing fixture to reset the whole `headroom.*` subtree before each
test.
Scope is intentionally one file (`tests/conftest.py`) — test-harness
only, no production change.
> Design note: I extended the existing defensive fixture rather than
restoring state inside the benchmark, because (a) the fixture already
exists for exactly this and only needed completing, and (b) the same
`propagate=False` hazard also originates from production
`_setup_file_logging`, so a centralized per-test reset is the more
durable fix. Happy to instead make the benchmark restore its own logging
state if maintainers prefer fixing it at the source.
## 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
- Extend the `_reset_headroom_logger_propagation` autouse fixture in
`tests/conftest.py` to reset `propagate = True` for **every** existing
`headroom.*` logger (previously only the top `headroom` logger), so
`caplog` capture is deterministic regardless of test execution order.
## Testing
- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [ ] Type checking passes (`mypy headroom`) — N/A, change is under
`tests/`
- [ ] New tests added — N/A, this fixes existing tests; they are
themselves the proof
- [x] Manual testing performed
### Test Output
```text
# Causal proof: run the polluter first, then the 5 victims, in one process.
# BEFORE (fixture reset scoped to only "headroom"):
$ pytest tests/test_claude_session_mode_benchmark.py \
tests/test_corrupt_golden_bytes_recovery.py \
tests/test_forwarded_headers.py \
'tests/test_transforms/test_kompress_compressor.py::TestKompressBackendSelection::test_unrecognized_backend_warns_and_falls_back_to_auto'
5 failed, 54 passed
FAILED tests/test_corrupt_golden_bytes_recovery.py::TestCorruptMemoryGoldenBytes::test_corrupt_bytes_logs_error
FAILED tests/test_corrupt_golden_bytes_recovery.py::TestCorruptMemoryGoldenBytes::test_unicode_decode_error_handled
FAILED tests/test_corrupt_golden_bytes_recovery.py::TestCorruptCcrGoldenBytes::test_corrupt_bytes_logs_error
FAILED tests/test_forwarded_headers.py::test_non_allowlisted_peer_ignores_forwarded_and_logs
FAILED tests/test_transforms/test_kompress_compressor.py::...test_unrecognized_backend_warns_and_falls_back_to_auto
# AFTER (this PR — whole headroom.* subtree reset):
$ pytest <same selection>
59 passed
# Full suite (Rust core rebuilt locally):
$ pytest
6251 passed, 496 skipped
$ ruff check .
All checks passed!
$ ruff format --check tests/conftest.py
1 file already formatted
```
## Real Behavior Proof
- Environment: macOS, Python 3.13.3, branch off latest `main`, Rust
`_core` rebuilt locally (`uv pip install -e .`).
- Exact command / steps: ran the polluter
(`test_claude_session_mode_benchmark`) together with the 5 victim tests
in one process to reproduce the order-dependent failure, then toggled
**only** the fixture change to confirm causality; then ran the full
`pytest` suite and `ruff`.
- Observed result: scoping the reset to only `"headroom"` → 5 failed /
54 passed; extending it to the `headroom.*` subtree → 59 passed. Full
suite: 6251 passed, 496 skipped, 0 failed. Lint clean.
- Not tested: behavior under CI's sharded `test (N)` jobs specifically —
but the fix is order-independent (resets before *every* test), so
sharding cannot reintroduce the leak.
## 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
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective — the 5
previously-flaky tests are the proof
- [x] New and existing unit tests pass locally with my changes
- [ ] I have updated the CHANGELOG.md — N/A (test-harness only)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-06-27 01:05:26 +08:00
|
|
|
logger.propagate = True
|
fix: B1 — retire ICM, RollingWindow, scoring, relevance + dependents
Phase B step 1 of the live-zone-only realignment. Removes ~10K LOC of
"drop messages from history" machinery that became unreachable after
PR-A1 made `/v1/messages` a passthrough on the proxy. Live-zone-only
compression (PR-B2..B7) operates on content blocks within messages;
message-list mutation no longer happens in the pipeline.
Python deletes:
- headroom/transforms/intelligent_context.py (1077 LOC)
- headroom/transforms/rolling_window.py (395 LOC)
- headroom/transforms/progressive_summarizer.py (508 LOC)
- headroom/transforms/scoring.py (459 LOC)
- headroom/transforms/tool_crusher.py (338 LOC)
- 5 corresponding tests/test_transforms/* and tests/test_proxy_intelligent_context.py
Rust deletes:
- crates/headroom-core/src/context/* (manager, config, workspace,
candidate, ccr_drop, strategy/, mod) + safety.rs replaced
- crates/headroom-core/src/scoring/* (mod, score, scorer, traits, weights)
- MessageScorerComparator from crates/headroom-parity (PR #338/#343
becomes deletable; sunk cost stays sunk)
- 13 message_scorer fixtures + record_message_scorer.py
Rust adds (move + rewrite):
- crates/headroom-core/src/transforms/safety.rs — `tool_pair_indices`
preserves the OpenAI/Anthropic tool_use ↔ tool_result pairing rule
the live-zone dispatcher (PR-B2) needs. No IcmConfig dependency.
Surface refactors:
- HeadroomConfig: drop `tool_crusher`, `rolling_window`,
`intelligent_context` fields; hoist `output_buffer_tokens` to top
level (used by client.py).
- ProxyConfig: drop `intelligent_context*` fields.
- `headroom wrap` proxy server: retire IntelligentContextManager
and RollingWindow imports + branch; pipeline is CacheAligner →
ContentRouter (smart_routing) or CacheAligner → SmartCrusher
(legacy).
- CLI: drop `--no-intelligent-context`, `--no-intelligent-scoring`,
`--no-compress-first` flags.
- LangChain memory integration: rename `_apply_rolling_window` →
`_apply_compression`, drop RollingWindowConfig dep. Threshold is
now advisory — B6 will rework the contract.
- TransformPipeline.create_pipeline now takes only cache_aligner_config.
- headroom/__init__.py + headroom/transforms/__init__.py: strip
exports of deleted symbols.
Bug fixes uncovered by full pytest sweep:
- providers/copilot/wrap.py: `environ or os.environ` collapsed
empty-dict to falsy → callers passing `environ={}` accidentally
pulled from os.environ. Use `environ if environ is not None else
os.environ`.
Test correctness fixes:
- _DummyAnthropicHandler._retry_request gains **_kwargs to match
the real handler signature post-A8.
- test_ws_http_fallback extracts JSON from `content=` (post-A3
byte-faithful) rather than the obsolete `json=` kwarg.
- test_ccr_response_handler_extra fixture joins SSE events with
`\n\n` per spec (post-A8 byte-buffer parser requirement).
- test_proxy_responses_phase_preservation: capture via direct
handler attached to the named logger, so the assertion is
order-independent (proxy `_setup_file_logging` flips
`headroom.propagate=False` once any earlier test triggers it).
- conftest.py autouse fixture resets `headroom.propagate=True`
before each test as a defensive measure for the same pollution.
- test_wrap_copilot_translated_backend_still_requires_byok:
monkeypatch.delenv every provider key so the BYOK error
actually fires.
- test_native_installers: skip when system bash < 4.3 (macOS ships 3.2).
- TestGeminiEmbedContent / TestGeminiBatchEmbedContents:
pytest.mark.skip — proxy currently has no :embedContent route;
feature gap, not regression.
Acceptance:
- cargo build --workspace + cargo clippy + cargo fmt --check: green.
- cargo test --workspace --exclude headroom-py: 777 passed.
- pytest: 4892 passed, 240 skipped, 0 failed.
- git grep returns only intentional comments referencing the deletion.
Per-PR-B1 plan: REALIGNMENT/04-phase-B-live-zone.md.
2026-05-02 12:23:17 -07:00
|
|
|
yield
|
|
|
|
|
|
|
|
|
|
|
2026-01-30 16:37:34 -08:00
|
|
|
# =============================================================================
|
|
|
|
|
# Sample messages fixtures
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
2026-01-06 23:16:58 -08:00
|
|
|
|
|
|
|
|
# Sample messages fixtures
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def sample_messages():
|
|
|
|
|
"""Basic conversation messages."""
|
|
|
|
|
return [
|
|
|
|
|
{"role": "system", "content": "You are a helpful assistant."},
|
|
|
|
|
{"role": "user", "content": "Hello, how are you?"},
|
|
|
|
|
{"role": "assistant", "content": "I'm doing well, thank you!"},
|
|
|
|
|
]
|
|
|
|
|
|
2026-01-10 15:33:44 -08:00
|
|
|
|
2026-01-06 23:16:58 -08:00
|
|
|
@pytest.fixture
|
|
|
|
|
def sample_messages_with_tools():
|
|
|
|
|
"""Conversation with tool calls and responses."""
|
|
|
|
|
return [
|
|
|
|
|
{"role": "system", "content": "You are a helpful assistant with tools."},
|
|
|
|
|
{"role": "user", "content": "Search for user 12345"},
|
|
|
|
|
{
|
|
|
|
|
"role": "assistant",
|
|
|
|
|
"content": None,
|
|
|
|
|
"tool_calls": [
|
|
|
|
|
{
|
|
|
|
|
"id": "call_123",
|
|
|
|
|
"type": "function",
|
2026-01-10 15:33:44 -08:00
|
|
|
"function": {"name": "search_user", "arguments": '{"user_id": "12345"}'},
|
2026-01-06 23:16:58 -08:00
|
|
|
}
|
2026-01-10 15:33:44 -08:00
|
|
|
],
|
2026-01-06 23:16:58 -08:00
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"role": "tool",
|
|
|
|
|
"tool_call_id": "call_123",
|
2026-01-10 15:33:44 -08:00
|
|
|
"content": '{"id": "12345", "name": "Alice", "email": "alice@example.com"}',
|
2026-01-06 23:16:58 -08:00
|
|
|
},
|
|
|
|
|
{"role": "assistant", "content": "I found user Alice with ID 12345."},
|
|
|
|
|
]
|
|
|
|
|
|
2026-01-10 15:33:44 -08:00
|
|
|
|
2026-01-06 23:16:58 -08:00
|
|
|
@pytest.fixture
|
|
|
|
|
def sample_tool_output_large():
|
|
|
|
|
"""Large tool output for compression testing (100 items)."""
|
2026-01-10 15:33:44 -08:00
|
|
|
return json.dumps(
|
|
|
|
|
[
|
|
|
|
|
{
|
|
|
|
|
"id": i,
|
|
|
|
|
"name": f"Item {i}",
|
|
|
|
|
"score": i * 0.1,
|
|
|
|
|
"status": "active" if i % 2 == 0 else "inactive",
|
|
|
|
|
}
|
|
|
|
|
for i in range(100)
|
|
|
|
|
]
|
|
|
|
|
)
|
|
|
|
|
|
2026-01-06 23:16:58 -08:00
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def sample_tool_output_with_errors():
|
|
|
|
|
"""Tool output containing error items."""
|
|
|
|
|
items = [{"id": i, "status": "success"} for i in range(20)]
|
|
|
|
|
items[5] = {"id": 5, "status": "error", "message": "Connection refused"}
|
|
|
|
|
items[15] = {"id": 15, "status": "failed", "exception": "TimeoutError"}
|
|
|
|
|
return json.dumps(items)
|
|
|
|
|
|
2026-01-10 15:33:44 -08:00
|
|
|
|
2026-01-06 23:16:58 -08:00
|
|
|
@pytest.fixture
|
|
|
|
|
def sample_system_prompt_with_date():
|
|
|
|
|
"""System prompt containing dynamic date."""
|
|
|
|
|
return "You are a helpful assistant. Current date: 2025-01-06. Help the user with their tasks."
|
|
|
|
|
|
2026-01-10 15:33:44 -08:00
|
|
|
|
2026-01-06 23:16:58 -08:00
|
|
|
@pytest.fixture
|
|
|
|
|
def sample_anthropic_messages():
|
|
|
|
|
"""Anthropic-style messages with content blocks."""
|
|
|
|
|
return [
|
|
|
|
|
{
|
|
|
|
|
"role": "user",
|
|
|
|
|
"content": [
|
|
|
|
|
{"type": "text", "text": "Analyze this image"},
|
2026-01-10 15:33:44 -08:00
|
|
|
{
|
|
|
|
|
"type": "image",
|
|
|
|
|
"source": {"type": "base64", "media_type": "image/png", "data": "..."},
|
|
|
|
|
},
|
|
|
|
|
],
|
2026-01-06 23:16:58 -08:00
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
|
2026-01-10 15:33:44 -08:00
|
|
|
|
2026-01-06 23:16:58 -08:00
|
|
|
# Mock client fixtures
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def mock_openai_response():
|
|
|
|
|
"""Mock OpenAI API response."""
|
|
|
|
|
mock = Mock()
|
|
|
|
|
mock.id = "chatcmpl-123"
|
|
|
|
|
mock.model = "gpt-4o"
|
|
|
|
|
mock.usage = Mock()
|
|
|
|
|
mock.usage.prompt_tokens = 100
|
|
|
|
|
mock.usage.completion_tokens = 50
|
|
|
|
|
mock.usage.total_tokens = 150
|
|
|
|
|
mock.choices = [Mock()]
|
|
|
|
|
mock.choices[0].message = Mock()
|
|
|
|
|
mock.choices[0].message.content = "This is a response."
|
|
|
|
|
mock.choices[0].message.role = "assistant"
|
|
|
|
|
mock.choices[0].finish_reason = "stop"
|
|
|
|
|
return mock
|
|
|
|
|
|
2026-01-10 15:33:44 -08:00
|
|
|
|
2026-01-06 23:16:58 -08:00
|
|
|
@pytest.fixture
|
|
|
|
|
def mock_openai_client(mock_openai_response):
|
|
|
|
|
"""Mock OpenAI client."""
|
|
|
|
|
client = Mock()
|
|
|
|
|
client.chat = Mock()
|
|
|
|
|
client.chat.completions = Mock()
|
|
|
|
|
client.chat.completions.create = Mock(return_value=mock_openai_response)
|
|
|
|
|
return client
|
|
|
|
|
|
2026-01-10 15:33:44 -08:00
|
|
|
|
2026-01-06 23:16:58 -08:00
|
|
|
# Storage fixtures
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def temp_sqlite_db():
|
|
|
|
|
"""Temporary SQLite database path."""
|
|
|
|
|
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
|
|
|
|
|
yield f.name
|
|
|
|
|
Path(f.name).unlink(missing_ok=True)
|
|
|
|
|
|
2026-01-10 15:33:44 -08:00
|
|
|
|
2026-01-06 23:16:58 -08:00
|
|
|
@pytest.fixture
|
|
|
|
|
def temp_jsonl_file():
|
|
|
|
|
"""Temporary JSONL file path."""
|
|
|
|
|
with tempfile.NamedTemporaryFile(suffix=".jsonl", delete=False) as f:
|
|
|
|
|
yield f.name
|
|
|
|
|
Path(f.name).unlink(missing_ok=True)
|
|
|
|
|
|
2026-01-10 15:33:44 -08:00
|
|
|
|
2026-01-06 23:16:58 -08:00
|
|
|
# Provider fixtures
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def openai_provider():
|
|
|
|
|
"""OpenAI provider instance."""
|
|
|
|
|
from headroom.providers.openai import OpenAIProvider
|
2026-01-10 15:33:44 -08:00
|
|
|
|
2026-01-06 23:16:58 -08:00
|
|
|
return OpenAIProvider()
|
|
|
|
|
|
2026-01-10 15:33:44 -08:00
|
|
|
|
2026-01-06 23:16:58 -08:00
|
|
|
@pytest.fixture
|
|
|
|
|
def openai_tokenizer():
|
|
|
|
|
"""OpenAI token counter for gpt-4o."""
|
|
|
|
|
from headroom.providers.openai import OpenAITokenCounter
|
2026-01-10 15:33:44 -08:00
|
|
|
|
2026-01-06 23:16:58 -08:00
|
|
|
return OpenAITokenCounter("gpt-4o")
|
|
|
|
|
|
2026-01-10 15:33:44 -08:00
|
|
|
|
2026-01-06 23:16:58 -08:00
|
|
|
# Config fixtures
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def default_config():
|
|
|
|
|
"""Default HeadroomConfig."""
|
|
|
|
|
from headroom.config import HeadroomConfig
|
2026-01-10 15:33:44 -08:00
|
|
|
|
2026-01-06 23:16:58 -08:00
|
|
|
return HeadroomConfig()
|
|
|
|
|
|
2026-01-10 15:33:44 -08:00
|
|
|
|
2026-01-06 23:16:58 -08:00
|
|
|
@pytest.fixture
|
|
|
|
|
def smart_crusher_config():
|
|
|
|
|
"""SmartCrusher config for testing."""
|
|
|
|
|
from headroom.config import SmartCrusherConfig
|
2026-01-10 15:33:44 -08:00
|
|
|
|
2026-01-06 23:16:58 -08:00
|
|
|
return SmartCrusherConfig(
|
|
|
|
|
enabled=True,
|
|
|
|
|
min_items_to_analyze=3,
|
|
|
|
|
min_tokens_to_crush=0, # Always crush for tests
|
|
|
|
|
max_items_after_crush=10,
|
|
|
|
|
)
|
|
|
|
|
|
2026-01-10 15:33:44 -08:00
|
|
|
|
2026-01-06 23:16:58 -08:00
|
|
|
# Helper for creating RequestMetrics
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def sample_request_metrics():
|
|
|
|
|
"""Sample RequestMetrics for storage tests."""
|
|
|
|
|
from headroom.config import RequestMetrics
|
2026-01-10 15:33:44 -08:00
|
|
|
|
2026-01-06 23:16:58 -08:00
|
|
|
return RequestMetrics(
|
|
|
|
|
request_id="test-123",
|
|
|
|
|
timestamp=datetime(2025, 1, 6, 12, 0, 0),
|
|
|
|
|
model="gpt-4o",
|
|
|
|
|
stream=False,
|
|
|
|
|
mode="audit",
|
|
|
|
|
tokens_input_before=1000,
|
|
|
|
|
tokens_input_after=800,
|
|
|
|
|
tokens_output=200,
|
|
|
|
|
block_breakdown={"system": 100, "user": 200, "assistant": 500},
|
|
|
|
|
waste_signals={"json_bloat": 50},
|
|
|
|
|
stable_prefix_hash="abc123",
|
|
|
|
|
cache_alignment_score=85.0,
|
|
|
|
|
cached_tokens=100,
|
|
|
|
|
transforms_applied=["CacheAligner", "SmartCrusher"],
|
|
|
|
|
tool_units_dropped=1,
|
|
|
|
|
turns_dropped=0,
|
|
|
|
|
messages_hash="def456",
|
|
|
|
|
)
|