headroom/tests/test_compression_observability.py

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

461 lines
17 KiB
Python
Raw Permalink Normal View History

"""Per-strategy compression observability tests.
These guard the forcing function: when any compressor runs in
production, a `CompressionObserver` notification fires once per real
compression event, and `PrometheusMetrics` accumulates per-strategy
counters that the test suite asserts on directly.
The TOINSmartCrusher silent disconnect (caught three weeks late by
manual audit) was invisible because no signal distinguished by
strategy. These tests exist so the next regression of that shape
fails the suite the day it lands instead of waiting on an audit.
The counters live ONLY as in-process state on the metrics instance;
chore(telemetry): remove Supabase anonymous beacon; fix contact domain to headroomlabs.ai (#1526) ## Description Removes the anonymous-telemetry **beacon** — the only external, third-party data flow Headroom ever initiated. When telemetry was opted in, it POSTed aggregate `/stats` to a hardcoded **Supabase** REST endpoint (with an embedded anon API key in the source). For enterprise/on-prem deployments this is exactly the kind of vendor-controlled data egress a security review flags, so it's gone entirely — **zero "Supabase" references remain in the codebase.** What stays (by design): the **local** telemetry collector + the `HEADROOM_TELEMETRY` opt-in (it only feeds `/stats` and `/v1/telemetry` — nothing leaves the process), **OpenTelemetry export** (`HEADROOM_OTEL_METRICS_*`, so operators send operational metrics to *their own* collector), and the license usage reporter (your own domain, license-key-gated). Also fixes the contact domain: `headroom.dev` → `headroomlabs.ai` everywhere. Closes # (no tracking issue) ## 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) > Non-breaking: `HEADROOM_TELEMETRY` is still accepted (now gates local collection only). The only behavior change is that no telemetry is ever sent externally. ## Changes Made - **Deleted the Supabase beacon**: `TelemetryBeacon` class, `_SUPABASE_URL`/`_SUPABASE_KEY`/`_TABLE`/`_ENDPOINT`, the JSONB projection helper, the proxy-lifespan beacon wiring, the `SUPABASE_` install env passthrough, and `tests/test_strategy_stats_supabase.py`. - **Kept** the local opt-in predicate (`is_telemetry_enabled` etc.) in `beacon.py` — still used by the local collector + CLI — reworded to "local only". - **Retained** the single-worker-owner file lock (the cc-switch reconciler depends on it); updated its comments to drop the beacon framing. - `/stats` `anon_telemetry_shipping` is now always `False` (nothing ships externally); startup log reworded to "Local telemetry". - Reworded remaining "Supabase" comments in `collector.py`, `context.py`, `prometheus_metrics.py`, and two test docstrings. - Contact domain: `security@headroom.dev` → `security@headroomlabs.ai`, `conduct@headroom.dev` → `conduct@headroomlabs.ai`, FUNDING.yml sponsor URL. ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check`) - [x] Type checking passes (`mypy`) - [x] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text $ grep -rniI "supabase" --include=*.py --include=*.md --include=*.mdx . # (excl .venv/sbom) >>> ZERO Supabase references $ grep -rniI "headroom.dev" . >>> ZERO headroom.dev references $ ruff check <changed files> -> All checks passed! $ ruff format --check <changed files> -> 10 files already formatted $ mypy <changed telemetry files> -> Success: no issues found $ pytest tests/test_telemetry.py tests/test_telemetry_warning.py \ tests/test_proxy_telemetry_env.py tests/test_compression_observability.py \ tests/test_paths.py tests/test_paths_backward_compat.py -q ============================= 173 passed in 6.67s ============================== ``` ## Real Behavior Proof - **Environment:** macOS, Python 3.12 (`.venv`). - **Exact command / steps:** repo-wide grep for `supabase`/`headroom.dev`; `create_app(...)` driven through a full `TestClient` lifespan (startup + shutdown) in `test_proxy_telemetry_env.py`; `/stats` exercised in `test_telemetry_warning.py`. - **Observed result:** zero `supabase`/`headroom.dev` strings remain; the proxy starts and shuts down cleanly with the beacon removed (the worker-owner lock + reconciler still elect a single owner); `/stats.anon_telemetry_shipping` is `False` even with `HEADROOM_TELEMETRY=on`; local collector + OTEL paths unchanged. - **Not tested:** no live network call was ever made (the point — the external POST is gone). OTEL export and the license reporter were not exercised (unchanged by this PR). ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Checklist - [x] My code follows the project's style guidelines - [x] I have performed a self-review of my code - [x] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] I have updated the CHANGELOG.md if applicable ## Additional Notes - The license usage reporter (`reporter.py` → `app.headroomlabs.ai`) is intentionally **kept** — it's license-key-gated (dormant for unlicensed/OSS deployments) and goes to your own domain, not a third party. - Docs/CHANGELOG left unchecked: a couple of docs mention the telemetry beacon and may want a follow-up note that it now collects locally only; happy to add.
2026-06-27 22:48:26 -07:00
they are deliberately NOT exported as new Prometheus metric names
(to avoid unbounded metric-series growth) they remain observable
via /stats. CI-level
observability via these tests is enough to catch silent regressions;
production export waits on a non-column-adding pipeline.
Coverage:
1. `ContentRouter.compress(...)` calls observer once per RoutingDecision.
2. `SmartCrusher.apply(...)` calls observer once per crushed message.
3. Both transforms tolerate an observer that raises (compression must
still succeed).
4. `PrometheusMetrics` correctly satisfies the `CompressionObserver`
protocol `record_compression` increments per-strategy counters
and `tokens_saved_by_strategy` accumulates only positive savings.
5. The Prometheus scrape output (`export()`) does NOT emit any new
metric names the per-strategy state stays internal.
"""
from __future__ import annotations
from dataclasses import dataclass, field
feat(metrics): record per-extension token savings (#2371) ## What Adds `PrometheusMetrics.record_extension_savings(key, saved)` so proxy extensions can report the tokens they save, and surfaces the per-extension totals in the `/stats` payload. ## Why Proxy extensions that perform their own token reduction currently have no supported way to report their savings to the metrics object — there is no method for it, so that telemetry is silently dropped. This adds the recording method and exposes the aggregate alongside the existing per-strategy compression breakdown. ## How - New `extension_savings: dict[str, int]` counter on `PrometheusMetrics`, populated lazily per extension-supplied `key` (no hardcoded list of extensions). - `record_extension_savings(key, saved)` accumulates positive savings per key, mirroring how `record_compression` aggregates `tokens_saved_by_strategy` (lock-free `defaultdict(int)`, atomic under the GIL for these key types); non-positive values are ignored. - Cleared in `reset_runtime()` with the other in-memory counters. - Surfaced in `/stats` as `extension_savings`, next to `compressions_by_strategy` / `tokens_saved_by_strategy`. No new Prometheus series. ## Behavior change None to existing metrics. ## Testing - Two focused tests in `tests/test_compression_observability.py` (per-key accumulation incl. zero/negative ignored; surfaced in `/stats` via `create_app`) → 2 passed (13 in file). - `ruff check` / `ruff format` → clean; `mypy` → clean on changed source.
2026-07-17 21:58:18 -07:00
from pathlib import Path
from typing import Any
import pytest
from headroom.transforms.content_detector import ContentType
from headroom.transforms.content_router import (
CompressionStrategy,
ContentRouter,
ContentRouterConfig,
RouterCompressionResult,
RoutingDecision,
)
from headroom.transforms.observability import CompressionObserver
from headroom.transforms.smart_crusher import SmartCrusher, SmartCrusherConfig
# ─── Test doubles ──────────────────────────────────────────────────────
@dataclass
class SpyObserver:
"""Captures every `record_compression` call for assertion."""
calls: list[tuple[str, int, int]] = field(default_factory=list)
def record_compression(
self,
strategy: str,
original_tokens: int,
compressed_tokens: int,
) -> None:
self.calls.append((strategy, original_tokens, compressed_tokens))
@dataclass
class ExplodingObserver:
"""Raises on every call. Used to assert observer failures don't
propagate out and break compression."""
raised: int = 0
def record_compression(self, *_a: Any, **_kw: Any) -> None:
self.raised += 1
raise RuntimeError("simulated observer outage")
# ─── Protocol conformance ──────────────────────────────────────────────
def test_spy_satisfies_observer_protocol():
spy = SpyObserver()
# `runtime_checkable` Protocol — isinstance check works.
assert isinstance(spy, CompressionObserver)
def test_prometheus_metrics_satisfies_observer_protocol():
from headroom.proxy.prometheus_metrics import PrometheusMetrics
m = PrometheusMetrics()
assert isinstance(m, CompressionObserver)
# ─── ContentRouter wiring ──────────────────────────────────────────────
def test_content_router_records_observer_call_per_routing_decision():
spy = SpyObserver()
router = ContentRouter(ContentRouterConfig(), observer=spy)
# Forge a routing log directly via the result object — the observer
# call site walks `result.routing_log`, so we assert the contract
# without depending on which compressor would actually fire.
result = RouterCompressionResult(
compressed="x",
original="x",
strategy_used=CompressionStrategy.SMART_CRUSHER,
routing_log=[
RoutingDecision(
content_type=ContentType.JSON_ARRAY,
strategy=CompressionStrategy.SMART_CRUSHER,
original_tokens=200,
compressed_tokens=50,
),
RoutingDecision(
content_type=ContentType.SOURCE_CODE,
strategy=CompressionStrategy.CODE_AWARE,
original_tokens=300,
compressed_tokens=300, # passthrough — still recorded
),
],
)
router._observe(result)
assert spy.calls == [
("smart_crusher", 200, 50),
("code_aware", 300, 300),
]
def test_content_router_with_no_observer_is_silent():
router = ContentRouter(ContentRouterConfig()) # observer defaults None
result = RouterCompressionResult(
compressed="x",
original="x",
strategy_used=CompressionStrategy.PASSTHROUGH,
routing_log=[
RoutingDecision(
content_type=ContentType.PLAIN_TEXT,
strategy=CompressionStrategy.TEXT,
original_tokens=10,
compressed_tokens=5,
)
],
)
# Should not raise.
router._observe(result)
def test_content_router_swallows_observer_failures():
boom = ExplodingObserver()
router = ContentRouter(ContentRouterConfig(), observer=boom)
result = RouterCompressionResult(
compressed="x",
original="x",
strategy_used=CompressionStrategy.TEXT,
routing_log=[
RoutingDecision(
content_type=ContentType.PLAIN_TEXT,
strategy=CompressionStrategy.TEXT,
original_tokens=10,
compressed_tokens=5,
)
],
)
# Must not raise — observability failures are not compression failures.
router._observe(result)
assert boom.raised == 1
# ─── SmartCrusher wiring (legacy direct-pipeline path) ─────────────────
def _bigger_array(n: int = 60) -> str:
import json as _json
items = [{"status": "ok", "tag": "x", "n": i} for i in range(n)]
return _json.dumps(items)
@pytest.fixture
def isolated_toin(tmp_path, monkeypatch):
"""Point TOIN at a tempdir for the duration of the test.
SmartCrusher.apply() feeds the global TOIN learning store via
`record_compression`. Its default storage path is
`~/.headroom/toin.json`, which persists across pytest invocations.
On Python 3.11 CI runs the suite twice (regular + coverage); a
pattern written in run #1 changes which rows the lossy sampler
keeps in run #2 and breaks `test_first_last_items_always_preserved`
in `test_evals.py`.
Isolating the TOIN file per test contains the side effect.
"""
from pathlib import Path
from headroom.telemetry.toin import TOIN_PATH_ENV_VAR, reset_toin
storage = str(Path(tmp_path) / "toin.json")
monkeypatch.setenv(TOIN_PATH_ENV_VAR, storage)
reset_toin()
yield
reset_toin()
def test_smart_crusher_apply_records_observer_per_crushed_message(isolated_toin):
"""End-to-end: SmartCrusher.apply() walks messages, crushes the
big tool_result, fires the observer with strategy='smart_crusher'."""
from headroom.providers.openai import OpenAITokenCounter
from headroom.tokenizer import Tokenizer
spy = SpyObserver()
crusher = SmartCrusher(SmartCrusherConfig(), observer=spy)
tok = Tokenizer(OpenAITokenCounter("gpt-4o-mini"), model="gpt-4o-mini")
messages = [
{"role": "user", "content": "what's in the data?"},
{"role": "tool", "content": _bigger_array(60)},
]
result = crusher.apply(messages, tok)
# If the analyzer chose passthrough this run, the observer wasn't
# fired; that's fine for the wiring test — we only assert it WAS
# fired in the case it crushed.
if "smart_crush:" in ",".join(result.transforms_applied):
assert spy.calls, "smart_crusher crushed but observer wasn't notified"
for strategy, original, compressed in spy.calls:
assert strategy == "smart_crusher"
assert original > 0
assert compressed >= 0
def test_smart_crusher_apply_swallows_observer_failures(isolated_toin):
"""Observer raises → compression still completes, returns valid
TransformResult, count of raises matches the crushed_count."""
from headroom.providers.openai import OpenAITokenCounter
from headroom.tokenizer import Tokenizer
boom = ExplodingObserver()
crusher = SmartCrusher(SmartCrusherConfig(), observer=boom)
tok = Tokenizer(OpenAITokenCounter("gpt-4o-mini"), model="gpt-4o-mini")
messages = [{"role": "tool", "content": _bigger_array(60)}]
result = crusher.apply(messages, tok)
# Either the analyzer didn't crush (boom.raised == 0) or it did
# (boom.raised >= 1) — but in both cases compression returned a
# valid TransformResult. No exception escaped.
assert result.messages is not None
# ─── PrometheusMetrics implementation ──────────────────────────────────
def test_prometheus_metrics_accumulates_per_strategy_counters():
from headroom.proxy.prometheus_metrics import PrometheusMetrics
m = PrometheusMetrics()
m.record_compression("smart_crusher", original_tokens=200, compressed_tokens=50)
m.record_compression("smart_crusher", original_tokens=100, compressed_tokens=40)
m.record_compression("diff", original_tokens=80, compressed_tokens=80) # no savings
m.record_compression("code_aware", original_tokens=50, compressed_tokens=70) # negative savings
assert m.compressions_by_strategy == {
"smart_crusher": 2,
"diff": 1,
"code_aware": 1,
}
# Tokens saved is `max(0, original - compressed)` per strategy.
# smart_crusher: 150 + 60 = 210; diff: 0 (no savings, dict entry omitted);
# code_aware: 0 (negative).
assert m.tokens_saved_by_strategy == {"smart_crusher": 210}
feat(metrics): record per-extension token savings (#2371) ## What Adds `PrometheusMetrics.record_extension_savings(key, saved)` so proxy extensions can report the tokens they save, and surfaces the per-extension totals in the `/stats` payload. ## Why Proxy extensions that perform their own token reduction currently have no supported way to report their savings to the metrics object — there is no method for it, so that telemetry is silently dropped. This adds the recording method and exposes the aggregate alongside the existing per-strategy compression breakdown. ## How - New `extension_savings: dict[str, int]` counter on `PrometheusMetrics`, populated lazily per extension-supplied `key` (no hardcoded list of extensions). - `record_extension_savings(key, saved)` accumulates positive savings per key, mirroring how `record_compression` aggregates `tokens_saved_by_strategy` (lock-free `defaultdict(int)`, atomic under the GIL for these key types); non-positive values are ignored. - Cleared in `reset_runtime()` with the other in-memory counters. - Surfaced in `/stats` as `extension_savings`, next to `compressions_by_strategy` / `tokens_saved_by_strategy`. No new Prometheus series. ## Behavior change None to existing metrics. ## Testing - Two focused tests in `tests/test_compression_observability.py` (per-key accumulation incl. zero/negative ignored; surfaced in `/stats` via `create_app`) → 2 passed (13 in file). - `ruff check` / `ruff format` → clean; `mypy` → clean on changed source.
2026-07-17 21:58:18 -07:00
def test_prometheus_metrics_accumulates_extension_savings_per_key() -> None:
from headroom.proxy.prometheus_metrics import PrometheusMetrics
m = PrometheusMetrics()
m.record_extension_savings("tool_router", 120)
m.record_extension_savings("tool_router", 30)
m.record_extension_savings("skill_search", 45)
m.record_extension_savings("skill_search", 0) # no savings, ignored
m.record_extension_savings("noop_ext", -10) # negative, ignored
# Savings accumulate per key; non-positive values never create or
# bump an entry.
assert m.extension_savings == {"tool_router": 150, "skill_search": 45}
def test_extension_savings_surface_in_stats(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
from fastapi.testclient import TestClient
from headroom.proxy.server import ProxyConfig, create_app
monkeypatch.setenv("HEADROOM_SAVINGS_PATH", str(tmp_path / "proxy_savings.json"))
config = ProxyConfig(
cache_enabled=False,
rate_limit_enabled=False,
log_requests=False,
)
app = create_app(config)
with TestClient(app) as client:
proxy = app.state.proxy
proxy.metrics.record_extension_savings("tool_router", 200)
proxy.metrics.record_extension_savings("tool_router", 50)
proxy.metrics.record_extension_savings("skill_search", 75)
stats = client.get("/stats")
assert stats.status_code == 200
assert stats.json()["extension_savings"] == {
"tool_router": 250,
"skill_search": 75,
}
def test_prometheus_metrics_accumulates_codex_ws_unit_and_frame_counters():
from headroom.proxy.prometheus_metrics import PrometheusMetrics
m = PrometheusMetrics()
m.record_codex_ws_unit(
strategy="mixed",
reason_category="applied",
elapsed_ms=1250,
text_bytes=10_000,
tokens_before=2500,
tokens_after=1000,
tokens_saved=1500,
modified=True,
strategy_chain=["mixed", "kompress"],
content_type="text",
text_shape="jsonl_like",
)
m.record_codex_ws_unit(
strategy="passthrough",
reason_category="size_floor",
elapsed_ms=2,
text_bytes=100,
tokens_before=20,
tokens_after=20,
tokens_saved=0,
modified=False,
strategy_chain=["passthrough"],
content_type="unknown",
text_shape="plain_text_like",
)
m.record_codex_ws_frame(
elapsed_ms=1260,
bytes_before=20_000,
bytes_after=8_000,
attempted_tokens=2500,
tokens_saved=1500,
modified=True,
strategy_chain=["mixed", "kompress"],
final_strategies=["mixed"],
)
m.record_codex_ws_frame(
elapsed_ms=30_000,
bytes_before=426_318,
failed=True,
)
assert m.codex_ws_units_total == 2
assert m.codex_ws_units_modified_total == 1
assert m.codex_ws_units_by_strategy == {"mixed": 1, "passthrough": 1}
assert m.codex_ws_units_by_category == {"applied": 1, "size_floor": 1}
assert m.codex_ws_units_by_content_type == {"text": 1, "unknown": 1}
assert m.codex_ws_units_by_text_shape == {"jsonl_like": 1, "plain_text_like": 1}
assert m.codex_ws_units_to_kompress_total == 0
assert m.codex_ws_units_kompress_attempted_total == 1
assert m.codex_ws_unit_elapsed_ms_max == 1250
assert m.codex_ws_unit_tokens_saved_sum == 1500
assert m.codex_ws_frames_attempted_total == 2
assert m.codex_ws_frames_compressed_total == 1
assert m.codex_ws_frames_failed_total == 1
assert m.codex_ws_frames_to_kompress_total == 0
assert m.codex_ws_frames_kompress_attempted_total == 1
assert m.codex_ws_frame_elapsed_ms_max == 30_000
assert m.codex_ws_frame_tokens_saved_sum == 1500
def test_prometheus_export_does_not_leak_per_strategy_metrics():
"""Per-strategy state is tracked in-process only. The Prometheus
chore(telemetry): remove Supabase anonymous beacon; fix contact domain to headroomlabs.ai (#1526) ## Description Removes the anonymous-telemetry **beacon** — the only external, third-party data flow Headroom ever initiated. When telemetry was opted in, it POSTed aggregate `/stats` to a hardcoded **Supabase** REST endpoint (with an embedded anon API key in the source). For enterprise/on-prem deployments this is exactly the kind of vendor-controlled data egress a security review flags, so it's gone entirely — **zero "Supabase" references remain in the codebase.** What stays (by design): the **local** telemetry collector + the `HEADROOM_TELEMETRY` opt-in (it only feeds `/stats` and `/v1/telemetry` — nothing leaves the process), **OpenTelemetry export** (`HEADROOM_OTEL_METRICS_*`, so operators send operational metrics to *their own* collector), and the license usage reporter (your own domain, license-key-gated). Also fixes the contact domain: `headroom.dev` → `headroomlabs.ai` everywhere. Closes # (no tracking issue) ## 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) > Non-breaking: `HEADROOM_TELEMETRY` is still accepted (now gates local collection only). The only behavior change is that no telemetry is ever sent externally. ## Changes Made - **Deleted the Supabase beacon**: `TelemetryBeacon` class, `_SUPABASE_URL`/`_SUPABASE_KEY`/`_TABLE`/`_ENDPOINT`, the JSONB projection helper, the proxy-lifespan beacon wiring, the `SUPABASE_` install env passthrough, and `tests/test_strategy_stats_supabase.py`. - **Kept** the local opt-in predicate (`is_telemetry_enabled` etc.) in `beacon.py` — still used by the local collector + CLI — reworded to "local only". - **Retained** the single-worker-owner file lock (the cc-switch reconciler depends on it); updated its comments to drop the beacon framing. - `/stats` `anon_telemetry_shipping` is now always `False` (nothing ships externally); startup log reworded to "Local telemetry". - Reworded remaining "Supabase" comments in `collector.py`, `context.py`, `prometheus_metrics.py`, and two test docstrings. - Contact domain: `security@headroom.dev` → `security@headroomlabs.ai`, `conduct@headroom.dev` → `conduct@headroomlabs.ai`, FUNDING.yml sponsor URL. ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check`) - [x] Type checking passes (`mypy`) - [x] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text $ grep -rniI "supabase" --include=*.py --include=*.md --include=*.mdx . # (excl .venv/sbom) >>> ZERO Supabase references $ grep -rniI "headroom.dev" . >>> ZERO headroom.dev references $ ruff check <changed files> -> All checks passed! $ ruff format --check <changed files> -> 10 files already formatted $ mypy <changed telemetry files> -> Success: no issues found $ pytest tests/test_telemetry.py tests/test_telemetry_warning.py \ tests/test_proxy_telemetry_env.py tests/test_compression_observability.py \ tests/test_paths.py tests/test_paths_backward_compat.py -q ============================= 173 passed in 6.67s ============================== ``` ## Real Behavior Proof - **Environment:** macOS, Python 3.12 (`.venv`). - **Exact command / steps:** repo-wide grep for `supabase`/`headroom.dev`; `create_app(...)` driven through a full `TestClient` lifespan (startup + shutdown) in `test_proxy_telemetry_env.py`; `/stats` exercised in `test_telemetry_warning.py`. - **Observed result:** zero `supabase`/`headroom.dev` strings remain; the proxy starts and shuts down cleanly with the beacon removed (the worker-owner lock + reconciler still elect a single owner); `/stats.anon_telemetry_shipping` is `False` even with `HEADROOM_TELEMETRY=on`; local collector + OTEL paths unchanged. - **Not tested:** no live network call was ever made (the point — the external POST is gone). OTEL export and the license reporter were not exercised (unchanged by this PR). ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Checklist - [x] My code follows the project's style guidelines - [x] I have performed a self-review of my code - [x] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] I have updated the CHANGELOG.md if applicable ## Additional Notes - The license usage reporter (`reporter.py` → `app.headroomlabs.ai`) is intentionally **kept** — it's license-key-gated (dormant for unlicensed/OSS deployments) and goes to your own domain, not a third party. - Docs/CHANGELOG left unchecked: a couple of docs mention the telemetry beacon and may want a follow-up note that it now collects locally only; happy to add.
2026-06-27 22:48:26 -07:00
scrape output deliberately must NOT emit new metric names (to avoid
unbounded metric-series growth); the state stays observable via
/stats. This test guards that constraint: if a future change adds
the metric to the scrape, this fails and forces a conscious
decision."""
import asyncio
from headroom.proxy.prometheus_metrics import PrometheusMetrics
m = PrometheusMetrics()
m.record_compression("smart_crusher", original_tokens=200, compressed_tokens=50)
m.record_compression("diff", original_tokens=120, compressed_tokens=70)
output = asyncio.run(m.export())
assert "headroom_compressions_total" not in output
assert "headroom_tokens_saved_by_strategy_total" not in output
# ─── End-to-end smoke (router + metrics together) ──────────────────────
def test_router_with_prometheus_observer_increments_counters():
"""Plumbing test: a router wired to a real PrometheusMetrics
instance lights up the per-strategy counters as routing decisions
accumulate. This is the production wiring shape from
`headroom/proxy/server.py`."""
from headroom.proxy.prometheus_metrics import PrometheusMetrics
m = PrometheusMetrics()
router = ContentRouter(ContentRouterConfig(), observer=m)
fake_result = RouterCompressionResult(
compressed="x",
original="x",
strategy_used=CompressionStrategy.MIXED,
routing_log=[
RoutingDecision(
content_type=ContentType.JSON_ARRAY,
strategy=CompressionStrategy.SMART_CRUSHER,
original_tokens=300,
compressed_tokens=80,
),
RoutingDecision(
content_type=ContentType.SOURCE_CODE,
strategy=CompressionStrategy.CODE_AWARE,
original_tokens=200,
compressed_tokens=120,
),
RoutingDecision(
content_type=ContentType.JSON_ARRAY,
strategy=CompressionStrategy.SMART_CRUSHER,
original_tokens=100,
compressed_tokens=40,
),
],
)
router._observe(fake_result)
assert m.compressions_by_strategy == {"smart_crusher": 2, "code_aware": 1}
assert m.tokens_saved_by_strategy == {
"smart_crusher": (300 - 80) + (100 - 40), # 280
"code_aware": (200 - 120), # 80
}
fix(proxy): restore Anthropic compression on token mode (issue #327) Three bugs combined to drive end-to-end compression on the Anthropic backend to ~0% in token mode (the default). User report #327 saw a ~9× drop in dashboard savings from one day to the next on Claude Code traffic; the dashboard headline was technically correct but the underlying compression genuinely was not running. After this change the same Claude Code-shape multi-turn conversation goes from 14987 → 14371 tokens at the request boundary on turn 1 and only recompresses the freshest tool_result on subsequent turns, with the prior turns frozen byte-identical to preserve the upstream prefix cache. Bug 1 — IntelligentContextManager inner ContentRouter has no observer PR #302 (commit cf979958, 2026-04-28) wired CompressionObserver onto the outer ContentRouter in proxy/server.py and onto SmartCrusher. The inner ContentRouter constructed lazily inside IntelligentContextManager._get_content_router (added Jan 18, 2026 in 57b2de5 alongside the COMPRESS_FIRST strategy) was missed. That inner router handles the bulk of Claude Code's tool_result-block compression, so per-strategy counters surfaced by PR #314 in v0.15.0 showed compressions_by_strategy={"text": 6} while summary.compression.total_tokens_removed=1.3M — math-impossible. Fix: add observer= parameter to IntelligentContextManager.__init__, forward it to the inner ContentRouter at intelligent_context.py:525, and pass observer=self.metrics from proxy/server.py. Bug 2 — TTL deferral marks every fresh tool_result as stable should_defer_compression in compression_cache.py returned True on first-sight (added 2026-04-07 in commit 22dad13 with the intent of batching first-time compressions near the 5-min cache TTL boundary to trade many small busts for one). The token-mode walker at anthropic.py:766-787 walks every message past frozen_message_count, calls should_defer_compression on each fresh tool_result, gets True, and advances ttl_frozen += 1 — every iteration. Result: frozen_message_count grows to len(messages), the pipeline freezes the entire request, and nothing reaches a real compressor. The defer-first-sight rationale assumes recurring content within TTL. Real Claude Code traffic produces unique content per turn, so "defer until next sight" defers forever. Compressing fresh content on first sight does not bust any prefix cache because Anthropic has not cached that byte position yet — it's a cache write either way. Fix: should_defer_compression returns False on first-sight (record the timestamp; compress now). Subsequent sightings within TTL still defer (batch window preserved for genuinely repeating content). Updated tests in test_compression_cache.py to assert the corrected semantics and verify _first_seen is recorded on first call. Bug 3 — cross-tokenizer comparison in token-mode inflation guard anthropic.py:634 sets original_tokens = tokenizer.count_messages(...) using the proxy-side EstimatingTokenCounter. The token-mode branch at line 816 set optimized_tokens = result.tokens_after from pipeline, which uses the provider-side AnthropicProvider tiktoken estimator. The two tokenizers disagree by ~25% on the same payload. The inflation guard at line 901 (if optimized_tokens > original_tokens: revert to originals) treats those two numbers as comparable. After a real 12% compression the provider-tokenizer figure was still higher than the proxy-tokenizer baseline, so the guard fired, optimized_messages was reset to the original input, transforms_applied was emptied, and tokens_saved went to 0. The dashboard showed no compression even when the pipeline successfully compressed. Fix: recount optimized_tokens with the proxy tokenizer right after the pipeline returns, so the guard compares apples-to-apples. The recount cost is a few ms on a 50K-token request and is dwarfed by upstream call latency. Verification * 80 targeted tests across test_compression_cache, test_compression_observability, test_proxy_anthropic_cache_stability, test_proxy_intelligent_context pass. * make ci-precheck clean. * End-to-end real-API run against api.anthropic.com via local proxy: - Turn 1 fresh: 14987 → 14371 (4.1%) on a 3-tool-round payload; smart_crusher and diff strategies fired with non-zero savings. - Turn 2 (turn 1 history + 1 new tool_result): 23161 → 21928 (5.3%); only the new tool_result compressed; older turns marked router:protected:user_message; Anthropic returned cache_creation_input_tokens > 0 confirming the prefix was not busted. Two new regression tests in test_compression_observability lock down the inner ContentRouter observer wiring so a future copy of Bug 1 fails the suite the day it lands.
2026-04-30 12:59:19 -07:00
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
# IntelligentContextManager observability tests retired with PR-B1 —
# the manager itself was deleted along with the message-dropping
# strategy. Inner-router observability is now exercised solely
# through ContentRouter, covered by
# `test_content_router_records_observer_call_per_routing_decision`.