headroom/tests/test_transforms
xspawnnn 3fc2a78a5e
fix(kompress): never block the request path on the cold-cache model download (#1161)
Closes #1146.

## Problem

On a cold cache, the first request that reaches the Kompress deep
compressor triggers an inline `hf_hub_download` of the 274 MB
`chopratejas/kompress-v2-base` ONNX model **on the request thread**.
That download races the proxy's compression budget
(`HEADROOM_COMPRESSION_TIMEOUT_SECONDS`, default 30s — the
`compression_first_stage` timeout): the fetch is cancelled mid-transfer,
**nothing finalizes in the HF cache**, and the request fails open
(uncompressed). Because the partial blob never lands, every subsequent
request repeats the same ~30s hang + fail-open, so the deep compressor
never actually becomes available through the proxy.

This is a **distinct root cause from #946** (which concerns the timeout
itself). Here the model must simply never be fetched synchronously on a
latency-sensitive request.

## Fix

Make the request path cache-only and move the one-time download
off-thread.

**`kompress_compressor.py`**
- `compress(..., allow_download=False)` — new keyword (default `True`,
so the direct API and `compress_batch` are unchanged) that resolves the
model cache-only; on a cold cache it raises `KompressModelNotCached` and
passes through instead of blocking on the network.
- `is_ready()` — lockless cache-membership check, safe to call on the
hot path.
- `ensure_background_download(model_id, device)` — starts at most one
daemon thread per model to pull the artifact down out of band (a
finished/failed thread is replaced, so a transient failure can be
retried by a later request). The compression timeout does not bound this
thread.

**`content_router.py`** — gate the deep path on readiness:
- not ready → return passthrough immediately and kick off the background
download;
- ready → `compress(allow_download=False)` (cache-only, no network on
the request thread).

Net effect: the cold-cache deep path returns in ~0 ms (passthrough)
instead of hanging ~30 s; the model downloads once in the background;
subsequent requests transparently use the deep compressor once it is
cached.

## Verification

Clean install of `headroom-ai==0.26.0` (main `@9f7f3ad` + this patch),
HF cache empty:

- **Cold-cache router request**: returns in **56 ms**, passthrough
(output == input), fetch handed to a background daemon thread — vs. the
pre-fix inline hang.
- **A/B on the same `compress()` call** with a slow-fetch stand-in:
pre-fix (`allow_download=True`) blocked **24.00 s** on the request
thread; post-fix (`allow_download=False`) returned **59 ms**.
- **4 new regression tests** in
`tests/test_kompress_request_nonblocking.py` (cache-only passthrough;
one-thread-per-model background download; router skips deep path when
not ready; router stays cache-only when ready) — `4 passed`.

## Files
- `headroom/transforms/kompress_compressor.py` — non-blocking load +
cache-only `compress`
- `headroom/transforms/content_router.py` — gate deep path on
`is_ready()`; background fetch when cold
- `tests/test_kompress_request_nonblocking.py` — regression coverage

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 11:33:05 -05:00
..
__init__.py Initial commit: Headroom SDK - LLM context optimization toolkit 2026-01-06 23:16:58 -08:00
test_code_compressor.py Fix CI test, bump to 0.5.7 2026-03-26 12:04:30 -07:00
test_content_router.py fix(kompress): never block the request path on the cold-cache model download (#1161) 2026-06-19 11:33:05 -05:00
test_diff_compressor.py feat(rust): retire python diff_compressor, ship rust-only via pyo3 2026-04-26 09:15:37 -07:00
test_diff_compressor_rust_parity.py feat(rust): retire python diff_compressor, ship rust-only via pyo3 2026-04-26 09:15:37 -07:00
test_html_extractor.py fix(tests): skip HTML extractor tests when trafilatura not installed 2026-01-31 15:39:55 -08:00
test_kompress_compressor.py fix(proxy): make Kompress eager preload cache-only so a cold cache can't block startup (#783) 2026-06-11 12:53:03 -05:00
test_read_lifecycle.py feat(transforms): attribute read_lifecycle + smart_crush tags (#249) 2026-06-11 11:51:26 -05:00
test_smart_crusher_attribution.py feat(transforms): attribute read_lifecycle + smart_crush tags (#249) 2026-06-11 11:51:26 -05:00
test_smart_crusher_bugs.py feat(rust): retire python smart_crusher, ship rust-only via pyo3 2026-04-27 00:52:21 -07:00
test_smart_crusher_ccr_roundtrip.py chore(rust): SmartCrusher CCR marker injection + walker unification 2026-04-27 20:25:22 -07:00
test_smart_crusher_lossless_default.py feat(rust): SmartCrusher PR4 — lossless-first default + CCR-Dropped restoration 2026-04-27 16:30:22 -07:00
test_smart_crusher_rust_parity.py feat(rust): SmartCrusher PR4 — lossless-first default + CCR-Dropped restoration 2026-04-27 16:30:22 -07:00
test_tag_protector.py fix: A9 — tag protector discards wrap on placeholder loss 2026-05-02 18:01:24 -07:00
test_tree_sitter_thread_safety.py fix(transforms): use thread-local tree-sitter parsers to prevent pyo3 Unsendable panic (#604) 2026-06-10 18:30:00 -05:00