mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-27 14:17:10 -04:00
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.
148 lines
5.6 KiB
Python
148 lines
5.6 KiB
Python
"""PR-A8 / P0-7 + P4-44 + P4-47: Responses API converter contract.
|
|
|
|
These tests pin:
|
|
|
|
- The Codex-emitted ``phase`` field (e.g. ``commentary`` /
|
|
``final_answer``) survives a forward + reverse trip through the
|
|
Chat-Completions normalizer. Pre-A8 the field was preserved only
|
|
*accidentally* via ``copy.copy(original)``; one refactor away from
|
|
silent data loss.
|
|
- Multi-text-part rebuild collapses to a single text part on the way
|
|
back instead of doubling content (the P0-7 bug).
|
|
- Unknown item types log a structured warning (P4-47) so operators see
|
|
new Codex item types in flight.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
|
|
from headroom.proxy.responses_converter import (
|
|
messages_to_responses_items,
|
|
responses_items_to_messages,
|
|
)
|
|
|
|
|
|
def test_codex_phase_commentary_preserved_through_compression() -> None:
|
|
"""`phase: commentary` survives forward + reverse conversion."""
|
|
items = [
|
|
{
|
|
"type": "message",
|
|
"id": "msg_codex_1",
|
|
"role": "assistant",
|
|
"phase": "commentary",
|
|
"content": [{"type": "output_text", "text": "Here's my reasoning."}],
|
|
},
|
|
]
|
|
messages, preserved = responses_items_to_messages(items)
|
|
assert preserved == []
|
|
assert len(messages) == 1
|
|
# Pretend the pipeline compressed nothing — feed messages back.
|
|
rebuilt = messages_to_responses_items(messages, items, preserved)
|
|
assert len(rebuilt) == 1
|
|
out = rebuilt[0]
|
|
assert out["phase"] == "commentary"
|
|
assert out["id"] == "msg_codex_1"
|
|
assert out["role"] == "assistant"
|
|
|
|
|
|
def test_codex_phase_final_answer_preserved() -> None:
|
|
"""`phase: final_answer` survives even when content is compressed shorter."""
|
|
items = [
|
|
{
|
|
"type": "message",
|
|
"id": "msg_codex_2",
|
|
"role": "assistant",
|
|
"phase": "final_answer",
|
|
"content": [{"type": "output_text", "text": "Long original answer ..."}],
|
|
},
|
|
]
|
|
messages, preserved = responses_items_to_messages(items)
|
|
# Simulate compression: replace the message content with a shorter string.
|
|
messages[0]["content"] = "Short."
|
|
rebuilt = messages_to_responses_items(messages, items, preserved)
|
|
assert rebuilt[0]["phase"] == "final_answer"
|
|
# Content was replaced inside the original part structure.
|
|
assert rebuilt[0]["content"][0]["text"] == "Short."
|
|
|
|
|
|
def test_unknown_item_type_logs_warning_byte_equal() -> None:
|
|
"""Unknown item types preserve the item AND log a structured warning.
|
|
|
|
Capture is done by attaching a handler directly to the named logger
|
|
rather than relying on `caplog`. Other tests in the suite (proxy
|
|
file-logging setup) flip `headroom.*.propagate = False`, which breaks
|
|
pytest's root-attached caplog handler in unrelated test runs. A
|
|
direct handler is order-independent.
|
|
"""
|
|
items = [
|
|
{
|
|
"type": "apply_patch_v4a",
|
|
"id": "patch_1",
|
|
"patch": "--- a\n+++ b\n",
|
|
},
|
|
]
|
|
target = logging.getLogger("headroom.proxy.responses_converter")
|
|
captured: list[logging.LogRecord] = []
|
|
|
|
class _CaptureHandler(logging.Handler):
|
|
def emit(self, record: logging.LogRecord) -> None:
|
|
captured.append(record)
|
|
|
|
handler = _CaptureHandler(level=logging.WARNING)
|
|
prev_level = target.level
|
|
target.addHandler(handler)
|
|
target.setLevel(logging.WARNING)
|
|
try:
|
|
messages, preserved = responses_items_to_messages(items, request_id="req-xyz")
|
|
finally:
|
|
target.removeHandler(handler)
|
|
target.setLevel(prev_level)
|
|
|
|
# Item is preserved (byte-equal) on the rebuild side.
|
|
assert preserved == [0]
|
|
rebuilt = messages_to_responses_items(messages, items, preserved)
|
|
assert rebuilt == items
|
|
# A structured warning fired with the unknown type.
|
|
matched = [r for r in captured if "unknown_responses_item_type" in r.getMessage()]
|
|
assert matched, "expected unknown_responses_item_type warning log line"
|
|
msg = matched[0].getMessage()
|
|
assert "apply_patch_v4a" in msg
|
|
assert "patch_1" in msg
|
|
assert "req-xyz" in msg
|
|
|
|
|
|
def test_multi_text_part_rebuild_no_doubling() -> None:
|
|
"""Two-text-part input is collapsed to a single part on rebuild — no doubling."""
|
|
items = [
|
|
{
|
|
"type": "message",
|
|
"id": "msg_multi",
|
|
"role": "user",
|
|
"content": [
|
|
{"type": "input_text", "text": "First paragraph."},
|
|
{"type": "input_text", "text": "Second paragraph."},
|
|
],
|
|
},
|
|
]
|
|
messages, preserved = responses_items_to_messages(items)
|
|
# The two text parts join with `\n`.
|
|
assert messages[0]["content"] == "First paragraph.\nSecond paragraph."
|
|
# Pretend compression replaced with a shortened single string.
|
|
messages[0]["content"] = "Para one. Para two."
|
|
rebuilt = messages_to_responses_items(messages, items, preserved)
|
|
out_content = rebuilt[0]["content"]
|
|
# Critical: there is now exactly ONE text part on the rebuild,
|
|
# carrying the compressed string. The previous bug left a
|
|
# second part with the ORIGINAL "Second paragraph." text and
|
|
# the compressed string in part 0 — content doubled on the wire.
|
|
assert isinstance(out_content, list)
|
|
text_parts = [p for p in out_content if p.get("type") == "input_text"]
|
|
assert len(text_parts) == 1, (
|
|
f"expected exactly 1 text part on rebuild, got {len(text_parts)} — "
|
|
f"this is the P0-7 doubling bug regression"
|
|
)
|
|
assert text_parts[0]["text"] == "Para one. Para two."
|
|
# And the round-trip JSON is well-formed.
|
|
assert json.dumps(rebuilt)
|