headroom/tests/test_compression_batches.py
Jervis 09c66ac212
fix(proxy): batch small Codex Responses tool outputs (#2239)
## Description

Batches small Codex/OpenAI Responses tool-output units through the
existing ContentRouter instead of skipping each unit individually below
the 512-byte floor. This fixes sessions where many small tool outputs
are collectively worth compressing, but no single output clears the
per-unit threshold.

The change keeps larger units on the existing independent compression
path, preserves CCR retrieval markers and protected tags across the
batch envelope, rejects structurally invalid batch output, and leaves
under-floor tails as size-floor passthroughs.

Fixes #2234

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

- Added `headroom/transforms/compression_batches.py` for bounded
compatible-unit batching, batch envelope parsing, tag/CCR marker
preservation, and per-entry result splitting.
- Updated the OpenAI Responses compression adapter to batch small
tool-output text slots while keeping larger units on the existing cached
per-unit path.
- Switched the unit size floor to UTF-8 bytes so CJK and other multibyte
text are measured consistently with the byte threshold.
- Added regression coverage for batching, CJK byte floors, CCR marker
preservation, malformed batch rejection, array output parts, and
under-floor tails.

## Testing

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

### Test Output

```text
$ uv run --with pytest --with fastapi --with httpx --with anyio --with uvicorn --with h2 pytest tests/test_compression_batches.py tests/test_compression_units.py tests/test_openai_responses_compression_units.py -q
47 passed, 1 warning

$ uvx ruff==0.15.17 check headroom/proxy/handlers/openai.py headroom/transforms/compression_batches.py headroom/transforms/compression_units.py tests/test_compression_batches.py tests/test_compression_units.py tests/test_openai_responses_compression_units.py --output-format concise
All checks passed!

$ uvx ruff==0.15.17 format --check headroom/proxy/handlers/openai.py headroom/transforms/compression_batches.py headroom/transforms/compression_units.py tests/test_compression_batches.py tests/test_compression_units.py tests/test_openai_responses_compression_units.py
6 files already formatted

$ uv run --with mypy mypy headroom/transforms/compression_batches.py
Success: no issues found in 1 source file
```

## Real Behavior Proof

- Environment: Windows 11, Python 3.13.3, local checkout of this PR
branch.
- Exact command / steps: ran the focused batching/unit/OpenAI Responses
test suites above, including cases where four individually-small tool
outputs collectively exceed the shared floor and where output arrays
contain multiple text parts plus non-text parts.
- Observed result: small outputs are sent through one router call and
applied back to their original slots; under-floor tails remain
unmodified; non-text parts are preserved; CCR markers are retained or
the entire batch is rejected if moved/corrupted.
- Not tested: a live Codex Responses proxy session against an upstream
model; full-suite collection was not run locally.

## 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
- [x] New and existing unit tests pass locally with my changes
- [ ] I have updated the CHANGELOG.md if applicable

---------

Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-15 19:57:50 +00:00

231 lines
7.8 KiB
Python

from __future__ import annotations
import re
from headroom.transforms.compression_batches import (
CompressionBatchEntry,
build_compression_batches,
compress_batch_with_router,
)
from headroom.transforms.compression_units import CompressionUnit, RoutedCompressionUnit
from headroom.transforms.content_router import CompressionStrategy, RouterCompressionResult
def _entry(index: int, text: str) -> CompressionBatchEntry:
unit = CompressionUnit(
text=text,
provider="openai",
endpoint="responses",
role="tool",
item_type="local_shell_call_output",
cache_zone="live",
mutable=True,
min_bytes=512,
)
return CompressionBatchEntry(
entry_id=f"u{index}",
routed=RoutedCompressionUnit(unit=unit, slot=(index, ("output", None))),
)
def test_small_units_over_floor_form_one_batch():
entries = [_entry(index, "x" * 150) for index in range(4)]
batches, skipped = build_compression_batches(entries, min_batch_bytes=512)
assert len(batches) == 1
assert [entry.entry_id for entry in batches[0].entries] == ["u0", "u1", "u2", "u3"]
assert batches[0].text_bytes == 600
assert skipped == []
class _CharacterCounter:
def count_text(self, text: str) -> int:
return len(text)
class _ShorteningRouter:
def __init__(self) -> None:
self.calls = 0
def compress(self, content: str, **_kwargs) -> RouterCompressionResult:
self.calls += 1
return RouterCompressionResult(
compressed=content.replace("x" * 150, "x"),
original=content,
strategy_used=CompressionStrategy.KOMPRESS,
)
def test_batch_compresses_entries_with_one_router_call():
entries = [_entry(index, "x" * 150) for index in range(4)]
batches, _ = build_compression_batches(entries, min_batch_bytes=512)
router = _ShorteningRouter()
results = compress_batch_with_router(
batches[0],
router=router,
tokenizer=_CharacterCounter(),
)
assert router.calls == 1
assert [slot for slot, _ in results] == [entry.routed.slot for entry in entries]
assert [result.compressed for _, result in results] == ["x"] * 4
assert all(result.modified for _, result in results)
def test_under_floor_tail_is_skipped_without_a_batch():
entries = [_entry(index, "x" * 150) for index in range(3)]
batches, skipped = build_compression_batches(entries, min_batch_bytes=512)
assert batches == []
assert [entry.entry_id for entry in skipped] == ["u0", "u1", "u2"]
def test_sixteen_under_floor_entries_are_skipped_before_a_new_batch_starts():
entries = [_entry(index, "x" * 30) for index in range(17)]
batches, skipped = build_compression_batches(entries, min_batch_bytes=512)
assert batches == []
assert [entry.entry_id for entry in skipped] == [f"u{index}" for index in range(17)]
class _CorruptingRouter:
def compress(self, content: str, **_kwargs) -> RouterCompressionResult:
return RouterCompressionResult(
compressed="missing protected tags",
original=content,
strategy_used=CompressionStrategy.KOMPRESS,
)
def test_missing_batch_tags_passes_through_every_entry():
entries = [_entry(index, "x" * 150) for index in range(4)]
batches, _ = build_compression_batches(entries, min_batch_bytes=512)
results = compress_batch_with_router(
batches[0],
router=_CorruptingRouter(),
tokenizer=_CharacterCounter(),
)
assert [result.compressed for _, result in results] == ["x" * 150] * 4
assert [result.reason for _, result in results] == ["batch_invalid"] * 4
assert not any(result.modified for _, result in results)
def test_many_small_entries_create_no_more_than_sixteen_per_batch():
entries = [_entry(index, "x" * 100) for index in range(381)]
batches, skipped = build_compression_batches(entries, min_batch_bytes=512)
assert skipped == []
assert len(batches) <= 24
assert all(len(batch.entries) <= 16 for batch in batches)
assert all(batch.text_bytes <= 2048 for batch in batches)
class _LossyShellRouter:
def compress(self, content: str, **_kwargs) -> RouterCompressionResult:
return RouterCompressionResult(
compressed=content.replace("line alpha beta gamma\n" * 7, "summary"),
original=content,
strategy_used=CompressionStrategy.KOMPRESS,
)
def test_batch_keeps_structured_shell_output_without_a_ccr_marker():
entries = [_entry(index, "line alpha beta gamma\n" * 7) for index in range(4)]
batches, _ = build_compression_batches(entries, min_batch_bytes=512)
results = compress_batch_with_router(
batches[0],
router=_LossyShellRouter(),
tokenizer=_CharacterCounter(),
)
assert not any(result.modified for _, result in results)
assert [result.reason for _, result in results] == ["lossy_unrecoverable_tool_output"] * 4
class _MarkerStrippingRouter:
def compress(self, content: str, **_kwargs) -> RouterCompressionResult:
return RouterCompressionResult(
compressed=content.replace("word " * 30, "x").replace(
"[100 items compressed to 10. Retrieve more: hash=abc123]", ""
),
original=content,
strategy_used=CompressionStrategy.KOMPRESS,
)
def test_batch_preserves_ccr_markers_when_router_would_remove_them():
marker = "[100 items compressed to 10. Retrieve more: hash=abc123]"
original = f"{'word ' * 30}\n{marker}\n"
entries = [_entry(index, original) for index in range(4)]
batches, _ = build_compression_batches(entries, min_batch_bytes=512)
results = compress_batch_with_router(
batches[0],
router=_MarkerStrippingRouter(),
tokenizer=_CharacterCounter(),
)
assert all(result.modified for _, result in results)
assert all(marker in result.compressed for _, result in results)
class _MarkerMovingRouter:
def compress(self, content: str, **_kwargs) -> RouterCompressionResult:
placeholders = re.findall(r"\[\[HEADROOM_BATCH_CCR_[^]]+\]\]", content)
moved = content.replace(placeholders[0], "", 1).replace(
placeholders[1], f"{placeholders[1]}{placeholders[0]}", 1
)
return RouterCompressionResult(
compressed=moved.replace("word " * 30, "x"),
original=content,
strategy_used=CompressionStrategy.KOMPRESS,
)
def test_batch_rejects_ccr_marker_moved_to_another_entry():
marker = "[100 items compressed to 10. Retrieve more: hash=abc123]"
original = f"{'word ' * 30}\n{marker}\n"
entries = [_entry(index, original) for index in range(4)]
batches, _ = build_compression_batches(entries, min_batch_bytes=512)
results = compress_batch_with_router(
batches[0],
router=_MarkerMovingRouter(),
tokenizer=_CharacterCounter(),
)
assert [result.compressed for _, result in results] == [original] * 4
assert [result.reason for _, result in results] == ["batch_invalid"] * 4
class _CjkShorteningRouter:
def compress(self, content: str, **_kwargs) -> RouterCompressionResult:
return RouterCompressionResult(
compressed=content.replace("" * 150, ""),
original=content,
strategy_used=CompressionStrategy.KOMPRESS,
)
def test_batch_uses_utf8_bytes_for_cjk_small_units():
entries = [_entry(index, "" * 150) for index in range(4)]
batches, skipped = build_compression_batches(entries, min_batch_bytes=512)
results = compress_batch_with_router(
batches[0],
router=_CjkShorteningRouter(),
tokenizer=_CharacterCounter(),
)
assert skipped == []
assert batches[0].text_bytes == 1800
assert all(result.modified for _, result in results)
assert [result.compressed for _, result in results] == [""] * 4