mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-27 14:17:10 -04:00
## Description On the OpenAI Responses API path (used by `headroom wrap copilot` and Codex), fresh file reads were lossy-compressed one turn after production, so the model saw its own just-read file content garbled (Kompress word-dropping) and had to re-read it — the exact turn inflation `HEADROOM_PROTECT_READS` was built to prevent on the chat/Anthropic path. Two gaps combined: 1. Copilot CLI's `view` tool (its file-read tool) was not in `DEFAULT_EXCLUDE_TOOLS` — the set only covered Claude-Code names (`Read`, `Write`, …). 2. `_compress_openai_responses_live_text_units_with_router` (`headroom/proxy/handlers/openai.py`) protected only excluded tool *names* and never implemented the `HEADROOM_PROTECT_READS` read-command detection that `ContentRouter.apply()` has — so `bash` reads like `nl -ba FILE | sed -n '1,75p'` were lossy-compressed even with the `coding` profile's `protect_reads=True`. Fix (design adversarially reviewed with gpt-5.6-sol before implementation; verdict "correct with modifications" — all modifications adopted): - `view` added to **both** `DEFAULT_EXCLUDE_TOOLS` and `DEFAULT_VERBATIM_EXCLUDE_TOOLS` — byte-exact contract: no lossy compression, no lossless JSON rewrite, no cross-turn dedup fold. - Responses units path now ports the read-command guard: the producing command is normalized from both wire shapes (`function_call.arguments`, `local_shell_call.action` argv/string) via the shared `_tool_call_command_text`; each output is content-gated by `_read_output_should_be_protected` (lockfiles/JSON/logs/search stay compressible); protected ids are unioned into the dedup protection set. - Shared `read_protection_enabled()` env helper extracted in `content_router.py`, used by both paths. - Latent debug-path defect fixed (unbound `fold` when an excluded tool's output is a content-part list and debug logging is enabled). Follow-up (not in scope): Rust Responses path (`crates/headroom-core/src/transforms/live_zone.rs`) currently only protects `headroom_retrieve` — needs parity before that runtime becomes default. Closes #3237 ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) ## Changes Made - `headroom/config.py` — `view` in both exclusion sets. - `headroom/proxy/handlers/openai.py` — read-command protection for the Responses units path; dedup shield; debug-path fix. - `headroom/transforms/content_router.py` — shared `read_protection_enabled()` helper (both paths). - `tests/test_openai_responses_read_protection.py` — 16 regression tests. ## 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 # Before fix (first commit on this branch, repro-only): 2 failed, 1 passed # view read compressed; bash nl|sed read compressed despite HEADROOM_PROTECT_READS=1 # After fix: $ uv run pytest tests/test_openai_responses_read_protection.py -q 16 passed (incl. content-gate release, string-form local_shell_call, debug paths, scan robustness) $ uv run pytest tests/test_openai_responses_compression_units.py tests/test_responses_cross_turn_dedup.py \ tests/test_lossless_excluded_compaction.py tests/test_observed_wire_shapes.py \ tests/test_content_router_exclude_tools.py tests/test_content_router_compact_json.py -q 69 passed in 3.85s $ uv run ruff check <changed files> && uv run ruff format --check <changed files> All checks passed! 4 files already formatted $ uv run mypy headroom/config.py headroom/transforms/content_router.py headroom/proxy/handlers/openai.py Success: no issues found ``` ## Real Behavior Proof - Environment: macOS (Darwin), Python 3.13.7, headroom proxy 0.37.0-dev, `HEADROOM_STACK=wrap_copilot`, savings profile `coding` (effective per proxy banner), model `gpt-5.6-luna` via GitHub Copilot API. - Exact command / steps: incident forensics on Copilot CLI session `5487d36f-3e7d-48b0-a56a-a92e4969c17b` (events.jsonl tool results byte-matched to proxy log compression units), then the failing→passing repro above. - Observed result: (pre-fix proxy log `~/.headroom/logs/proxy-8794.log`) ```text 08:46:34 [hr_1787553986_000014] WS /v1/responses slow compression unit … strategy=text … bytes=3857 … tokens_saved=235 08:46:34 [hr_1787553986_000014] … strategy=text … bytes=6852 … tokens_saved=413 08:46:34 [hr_1787553986_000014] … strategy=text … bytes=3079 … tokens_saved=178 08:46:47 [hr_1787554001_000015] … strategy=text … bytes=6924 … tokens_saved=478 08:46:47 [hr_1787554001_000015] … strategy=text … bytes=4418 … tokens_saved=305 ``` Byte sizes match the session's `view` (3857/6852/3079) and `nl|sed` (6924/4418) tool results exactly. Post-fix, those payload shapes are byte-exact through `_compress_openai_responses_live_text_units_with_router` (asserted by the regression tests over the same wire shapes). - Not tested: full `pytest tests/` run (upstream suite has pre-existing order-dependent failures — 7 failed on clean `main` under `-k "content_router or read or protect"` — and a pre-existing `litellm` import error in `tests/test_memory_eval.py`; the 5 additional failures in that selection with my branch pass in isolation and also fail on clean main under the same selection); live end-to-end with a running Copilot wrap (unit-level wire-shape coverage instead); Rust core path (follow-up). ## Runtime Rollout Safety - Rollout-managed feature(s): none - Minimum rollout channel: N/A - Stable/default behavior changed: yes — `view` outputs and `HEADROOM_PROTECT_READS`-covered bash read outputs stay verbatim on the Responses path (fidelity improvement; slightly fewer tokens saved) - Kill switch / disable path: `HEADROOM_PROTECT_READS=0` restores old bash-read behavior; `HEADROOM_EXCLUDE_TOOLS` overrides tool exclusion - Unsafe override required: no - Qualification impact: none - Rollback path: revert the commit; no state/migration ## 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 (N/A, no user-facing docs for this internal guard) - [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`
531 lines
17 KiB
Python
531 lines
17 KiB
Python
"""Regression tests: file reads over the OpenAI Responses API path must stay verbatim.
|
|
|
|
Copilot CLI (and other Responses-native harnesses) read files two ways:
|
|
|
|
1. A first-class ``view`` tool (the Copilot equivalent of Claude Code's ``Read``)
|
|
whose output is raw file content the model will byte-patch against.
|
|
2. Shell reads through ``bash`` (``cat``/``nl``/``sed -n`` …), which the
|
|
chat/Anthropic path protects via ``HEADROOM_PROTECT_READS`` read-command
|
|
detection in ``ContentRouter``.
|
|
|
|
The Responses compression-units path historically protected neither: only
|
|
``DEFAULT_EXCLUDE_TOOLS`` names were honored, and ``HEADROOM_PROTECT_READS``
|
|
was never consulted. Lossy (Kompress) compression of a fresh file read garbles
|
|
exactly the bytes the model needs for line-precise edits, forcing re-reads
|
|
(turn inflation) — the harm read protection exists to prevent.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from types import MethodType, SimpleNamespace
|
|
|
|
from headroom.proxy.handlers.openai import OpenAIHandlerMixin
|
|
from headroom.transforms.content_router import (
|
|
CompressionStrategy,
|
|
ContentRouter,
|
|
RouterCompressionResult,
|
|
)
|
|
|
|
|
|
class TokenCounter:
|
|
def count_text(self, text: str) -> int:
|
|
return len(text.split())
|
|
|
|
|
|
def _handler_with_router(router: ContentRouter) -> OpenAIHandlerMixin:
|
|
handler = OpenAIHandlerMixin()
|
|
handler.openai_pipeline = SimpleNamespace(transforms=[router])
|
|
handler.openai_provider = SimpleNamespace(
|
|
get_token_counter=lambda _model: TokenCounter(),
|
|
)
|
|
return handler
|
|
|
|
|
|
def _lossy_router() -> ContentRouter:
|
|
"""Router whose compress() always 'lossy-compresses' any candidate it sees."""
|
|
|
|
router = ContentRouter()
|
|
|
|
def compress(self, content: str, **_kwargs):
|
|
return RouterCompressionResult(
|
|
compressed="kept words",
|
|
original=content,
|
|
strategy_used=CompressionStrategy.KOMPRESS,
|
|
)
|
|
|
|
router.compress = MethodType(compress, router)
|
|
return router
|
|
|
|
|
|
def _run(handler: OpenAIHandlerMixin, payload: dict):
|
|
return handler._compress_openai_responses_live_text_units_with_router(
|
|
payload,
|
|
model="gpt-5",
|
|
request_id="req_read_protection",
|
|
)
|
|
|
|
|
|
_FILE_CONTENT = "\n".join(
|
|
f"## Section {i}\nSome roadmap prose line {i} with enough words to matter" for i in range(90)
|
|
)
|
|
|
|
_NL_OUTPUT = "\n".join(
|
|
f"{i}\tline {i} of the roadmap file with a handful of words in it" for i in range(1, 110)
|
|
)
|
|
|
|
|
|
def test_responses_view_tool_read_stays_verbatim():
|
|
"""Copilot's `view` tool returns raw file bytes: never lossy-compress them."""
|
|
handler = _handler_with_router(_lossy_router())
|
|
payload = {
|
|
"model": "gpt-5",
|
|
"input": [
|
|
{
|
|
"type": "function_call",
|
|
"call_id": "call_view",
|
|
"name": "view",
|
|
"arguments": '{"path": "/repo/ROADMAP.md"}',
|
|
},
|
|
{
|
|
"type": "function_call_output",
|
|
"call_id": "call_view",
|
|
"output": _FILE_CONTENT,
|
|
},
|
|
],
|
|
}
|
|
|
|
new_payload, _modified, _saved, _t, _u, _s, _a = _run(handler, payload)
|
|
|
|
assert new_payload["input"][1]["output"] == _FILE_CONTENT
|
|
|
|
|
|
def test_responses_bash_read_command_stays_verbatim_when_protect_reads(monkeypatch):
|
|
"""HEADROOM_PROTECT_READS=1 must cover bash file reads on the Responses path too."""
|
|
monkeypatch.setenv("HEADROOM_PROTECT_READS", "1")
|
|
handler = _handler_with_router(_lossy_router())
|
|
payload = {
|
|
"model": "gpt-5",
|
|
"input": [
|
|
{
|
|
"type": "function_call",
|
|
"call_id": "call_bash",
|
|
"name": "bash",
|
|
"arguments": ('{"command": "nl -ba .overlay/ROADMAP.md | sed -n \'1,75p\'"}'),
|
|
},
|
|
{
|
|
"type": "function_call_output",
|
|
"call_id": "call_bash",
|
|
"output": _NL_OUTPUT,
|
|
},
|
|
],
|
|
}
|
|
|
|
new_payload, _modified, _saved, _t, _u, _s, _a = _run(handler, payload)
|
|
|
|
assert new_payload["input"][1]["output"] == _NL_OUTPUT
|
|
|
|
|
|
def test_responses_excluded_read_tool_stays_verbatim_control():
|
|
"""Control: Claude-style `Read` outputs are already protected today."""
|
|
handler = _handler_with_router(_lossy_router())
|
|
payload = {
|
|
"model": "gpt-5",
|
|
"input": [
|
|
{
|
|
"type": "function_call",
|
|
"call_id": "call_read",
|
|
"name": "Read",
|
|
"arguments": '{"file_path": "/repo/ROADMAP.md"}',
|
|
},
|
|
{
|
|
"type": "function_call_output",
|
|
"call_id": "call_read",
|
|
"output": _FILE_CONTENT,
|
|
},
|
|
],
|
|
}
|
|
|
|
new_payload, _modified, _saved, _t, _u, _s, _a = _run(handler, payload)
|
|
|
|
assert new_payload["input"][1]["output"] == _FILE_CONTENT
|
|
|
|
|
|
def test_responses_bash_read_compresses_when_protect_reads_disabled(monkeypatch):
|
|
"""Control: with HEADROOM_PROTECT_READS unset/0, bash reads stay compressible."""
|
|
monkeypatch.delenv("HEADROOM_PROTECT_READS", raising=False)
|
|
handler = _handler_with_router(_lossy_router())
|
|
payload = {
|
|
"model": "gpt-5",
|
|
"input": [
|
|
{
|
|
"type": "function_call",
|
|
"call_id": "call_bash",
|
|
"name": "bash",
|
|
"arguments": '{"command": "cat src/main.py"}',
|
|
},
|
|
{
|
|
"type": "function_call_output",
|
|
"call_id": "call_bash",
|
|
"output": _NL_OUTPUT,
|
|
},
|
|
],
|
|
}
|
|
|
|
new_payload, modified, _s, _t, _u, _c, _a = _run(handler, payload)
|
|
|
|
assert modified is True
|
|
assert new_payload["input"][1]["output"] == "kept words"
|
|
|
|
|
|
def test_responses_non_read_bash_command_still_compresses(monkeypatch):
|
|
"""Protection is type-specific: test/build/search output stays compressible."""
|
|
monkeypatch.setenv("HEADROOM_PROTECT_READS", "1")
|
|
handler = _handler_with_router(_lossy_router())
|
|
payload = {
|
|
"model": "gpt-5",
|
|
"input": [
|
|
{
|
|
"type": "function_call",
|
|
"call_id": "call_test",
|
|
"name": "bash",
|
|
"arguments": '{"command": "uv run pytest tests/ -q"}',
|
|
},
|
|
{
|
|
"type": "function_call_output",
|
|
"call_id": "call_test",
|
|
"output": _NL_OUTPUT,
|
|
},
|
|
],
|
|
}
|
|
|
|
new_payload, modified, _s, _t, _u, _c, _a = _run(handler, payload)
|
|
|
|
assert modified is True
|
|
assert new_payload["input"][1]["output"] == "kept words"
|
|
|
|
|
|
def test_responses_lockfile_read_stays_compressible(monkeypatch):
|
|
"""Lockfiles are tool-regenerated, never byte-patched: the command-level
|
|
carve-out keeps `cat uv.lock` compressible even with protection on."""
|
|
monkeypatch.setenv("HEADROOM_PROTECT_READS", "1")
|
|
handler = _handler_with_router(_lossy_router())
|
|
payload = {
|
|
"model": "gpt-5",
|
|
"input": [
|
|
{
|
|
"type": "function_call",
|
|
"call_id": "call_lock",
|
|
"name": "bash",
|
|
"arguments": '{"command": "cat uv.lock"}',
|
|
},
|
|
{
|
|
"type": "function_call_output",
|
|
"call_id": "call_lock",
|
|
"output": _NL_OUTPUT,
|
|
},
|
|
],
|
|
}
|
|
|
|
new_payload, modified, _s, _t, _u, _c, _a = _run(handler, payload)
|
|
|
|
assert modified is True
|
|
assert new_payload["input"][1]["output"] == "kept words"
|
|
|
|
|
|
def test_responses_local_shell_call_read_stays_verbatim(monkeypatch):
|
|
"""Codex native shell: local_shell_call.action.command (argv) read protected."""
|
|
monkeypatch.setenv("HEADROOM_PROTECT_READS", "1")
|
|
handler = _handler_with_router(_lossy_router())
|
|
payload = {
|
|
"model": "gpt-5",
|
|
"input": [
|
|
{
|
|
"type": "local_shell_call",
|
|
"call_id": "call_lsc",
|
|
"action": {"type": "exec", "command": ["nl", "-ba", "ROADMAP.md"]},
|
|
},
|
|
{
|
|
"type": "local_shell_call_output",
|
|
"call_id": "call_lsc",
|
|
"output": _NL_OUTPUT,
|
|
},
|
|
],
|
|
}
|
|
|
|
new_payload, _modified, _s, _t, _u, _c, _a = _run(handler, payload)
|
|
|
|
assert new_payload["input"][1]["output"] == _NL_OUTPUT
|
|
|
|
|
|
def test_responses_view_output_content_part_array_stays_verbatim():
|
|
"""`view` output shaped as a content-part array is protected byte-exactly,
|
|
including non-text parts."""
|
|
handler = _handler_with_router(_lossy_router())
|
|
parts = [
|
|
{"type": "output_text", "text": _FILE_CONTENT},
|
|
{"type": "refusal", "refusal": "n/a"},
|
|
]
|
|
payload = {
|
|
"model": "gpt-5",
|
|
"input": [
|
|
{
|
|
"type": "function_call",
|
|
"call_id": "call_view",
|
|
"name": "view",
|
|
"arguments": '{"path": "/repo/ROADMAP.md"}',
|
|
},
|
|
{
|
|
"type": "function_call_output",
|
|
"call_id": "call_view",
|
|
"output": parts,
|
|
},
|
|
],
|
|
}
|
|
|
|
new_payload, _modified, _s, _t, _u, _c, _a = _run(handler, payload)
|
|
|
|
assert new_payload["input"][1]["output"] == parts
|
|
|
|
|
|
def test_responses_view_json_shaped_output_stays_byte_exact():
|
|
"""Even JSON-shaped `view` output is verbatim: the byte-exact contract beats
|
|
the lossless JSON minification other excluded tools accept."""
|
|
handler = _handler_with_router(_lossy_router())
|
|
pretty_json = "\n".join(
|
|
["{"] + [f' "key_{i}": {i},' for i in range(120)] + [' "end": true', "}"]
|
|
)
|
|
payload = {
|
|
"model": "gpt-5",
|
|
"input": [
|
|
{
|
|
"type": "function_call",
|
|
"call_id": "call_view",
|
|
"name": "view",
|
|
"arguments": '{"path": "/repo/data.json"}',
|
|
},
|
|
{
|
|
"type": "function_call_output",
|
|
"call_id": "call_view",
|
|
"output": pretty_json,
|
|
},
|
|
],
|
|
}
|
|
|
|
new_payload, _modified, _s, _t, _u, _c, _a = _run(handler, payload)
|
|
|
|
assert new_payload["input"][1]["output"] == pretty_json
|
|
|
|
|
|
def test_responses_malformed_arguments_do_not_break_extraction(monkeypatch):
|
|
"""Malformed function_call arguments yield no command -> normal compression."""
|
|
monkeypatch.setenv("HEADROOM_PROTECT_READS", "1")
|
|
handler = _handler_with_router(_lossy_router())
|
|
payload = {
|
|
"model": "gpt-5",
|
|
"input": [
|
|
{
|
|
"type": "function_call",
|
|
"call_id": "call_bad",
|
|
"name": "bash",
|
|
"arguments": "{not json at all",
|
|
},
|
|
{
|
|
"type": "function_call_output",
|
|
"call_id": "call_bad",
|
|
"output": _NL_OUTPUT,
|
|
},
|
|
],
|
|
}
|
|
|
|
new_payload, modified, _s, _t, _u, _c, _a = _run(handler, payload)
|
|
|
|
assert modified is True
|
|
assert new_payload["input"][1]["output"] == "kept words"
|
|
|
|
|
|
def test_responses_protected_read_survives_cross_turn_dedup(monkeypatch):
|
|
"""A repeated protected read must not be replaced by a [↑…] dedup pointer."""
|
|
monkeypatch.setenv("HEADROOM_PROTECT_READS", "1")
|
|
router = _lossy_router()
|
|
router._cross_turn_dedup_enabled = True
|
|
handler = _handler_with_router(router)
|
|
payload = {
|
|
"model": "gpt-5",
|
|
"input": [
|
|
{
|
|
"type": "function_call",
|
|
"call_id": "call_r1",
|
|
"name": "bash",
|
|
"arguments": '{"command": "nl -ba ROADMAP.md"}',
|
|
},
|
|
{
|
|
"type": "function_call_output",
|
|
"call_id": "call_r1",
|
|
"output": _NL_OUTPUT,
|
|
},
|
|
{
|
|
"type": "function_call",
|
|
"call_id": "call_r2",
|
|
"name": "bash",
|
|
"arguments": '{"command": "nl -ba ROADMAP.md"}',
|
|
},
|
|
{
|
|
"type": "function_call_output",
|
|
"call_id": "call_r2",
|
|
"output": _NL_OUTPUT,
|
|
},
|
|
],
|
|
}
|
|
|
|
new_payload, _modified, _s, _t, _u, _c, _a = _run(handler, payload)
|
|
|
|
assert new_payload["input"][1]["output"] == _NL_OUTPUT
|
|
assert new_payload["input"][3]["output"] == _NL_OUTPUT
|
|
|
|
|
|
def test_responses_debug_path_with_excluded_list_output(monkeypatch):
|
|
"""Regression: debug logging over an excluded tool's content-part output must
|
|
not raise (latent unbound `fold` variable in the list branch)."""
|
|
from headroom.proxy.handlers import openai as openai_handler
|
|
|
|
monkeypatch.setattr(openai_handler, "_log_codex_compression_debug", lambda *a, **k: None)
|
|
handler = _handler_with_router(_lossy_router())
|
|
parts = [{"type": "output_text", "text": _FILE_CONTENT}]
|
|
payload = {
|
|
"model": "gpt-5",
|
|
"input": [
|
|
{
|
|
"type": "function_call",
|
|
"call_id": "call_read",
|
|
"name": "Read",
|
|
"arguments": '{"file_path": "/repo/ROADMAP.md"}',
|
|
},
|
|
{
|
|
"type": "function_call_output",
|
|
"call_id": "call_read",
|
|
"output": parts,
|
|
},
|
|
],
|
|
}
|
|
|
|
new_payload, _modified, _s, _t, _u, _c, _a = _run(handler, payload)
|
|
|
|
assert new_payload["input"][1]["output"] == parts
|
|
|
|
|
|
def test_responses_read_command_with_releasable_json_output_compresses(monkeypatch):
|
|
"""Content gate: a read command whose output is confidently DATA (JSON array)
|
|
is released to compression even with HEADROOM_PROTECT_READS=1."""
|
|
monkeypatch.setenv("HEADROOM_PROTECT_READS", "1")
|
|
handler = _handler_with_router(_lossy_router())
|
|
json_output = "[" + ",".join(f'{{"line": {i}, "text": "value {i}"}}' for i in range(60)) + "]"
|
|
payload = {
|
|
"model": "gpt-5",
|
|
"input": [
|
|
{
|
|
"type": "function_call",
|
|
"call_id": "call_json",
|
|
"name": "bash",
|
|
"arguments": '{"command": "cat data.json"}',
|
|
},
|
|
{
|
|
"type": "function_call_output",
|
|
"call_id": "call_json",
|
|
"output": json_output,
|
|
},
|
|
],
|
|
}
|
|
|
|
new_payload, modified, _s, _t, _u, _c, _a = _run(handler, payload)
|
|
|
|
assert modified is True
|
|
assert new_payload["input"][1]["output"] == "kept words"
|
|
|
|
|
|
def test_responses_local_shell_call_string_command_read_stays_verbatim(monkeypatch):
|
|
"""local_shell_call with a string (not argv) command is also covered."""
|
|
monkeypatch.setenv("HEADROOM_PROTECT_READS", "1")
|
|
handler = _handler_with_router(_lossy_router())
|
|
payload = {
|
|
"model": "gpt-5",
|
|
"input": [
|
|
{
|
|
"type": "local_shell_call",
|
|
"call_id": "call_lsc_str",
|
|
"action": {"type": "exec", "command": "cat src/app.py"},
|
|
},
|
|
{
|
|
"type": "local_shell_call_output",
|
|
"call_id": "call_lsc_str",
|
|
"output": _NL_OUTPUT,
|
|
},
|
|
],
|
|
}
|
|
|
|
new_payload, _modified, _s, _t, _u, _c, _a = _run(handler, payload)
|
|
|
|
assert new_payload["input"][1]["output"] == _NL_OUTPUT
|
|
|
|
|
|
def test_responses_debug_path_with_read_protected_output(monkeypatch):
|
|
"""Debug logging over a read-protected output records and does not raise."""
|
|
from headroom.proxy.handlers import openai as openai_handler
|
|
|
|
monkeypatch.setattr(openai_handler, "_log_codex_compression_debug", lambda *a, **k: None)
|
|
monkeypatch.setenv("HEADROOM_PROTECT_READS", "1")
|
|
handler = _handler_with_router(_lossy_router())
|
|
payload = {
|
|
"model": "gpt-5",
|
|
"input": [
|
|
{
|
|
"type": "function_call",
|
|
"call_id": "call_dbg",
|
|
"name": "bash",
|
|
"arguments": '{"command": "nl -ba ROADMAP.md"}',
|
|
},
|
|
{
|
|
"type": "function_call_output",
|
|
"call_id": "call_dbg",
|
|
"output": _NL_OUTPUT,
|
|
},
|
|
],
|
|
}
|
|
|
|
new_payload, _modified, _s, _t, _u, _c, _a = _run(handler, payload)
|
|
|
|
assert new_payload["input"][1]["output"] == _NL_OUTPUT
|
|
|
|
|
|
def test_responses_read_scan_tolerates_non_dict_and_missing_call_id(monkeypatch):
|
|
"""The producer scan must skip non-dict items and calls without a string
|
|
call_id without breaking normal compression."""
|
|
monkeypatch.setenv("HEADROOM_PROTECT_READS", "1")
|
|
handler = _handler_with_router(_lossy_router())
|
|
payload = {
|
|
"model": "gpt-5",
|
|
"input": [
|
|
"a bare string item",
|
|
{
|
|
"type": "function_call",
|
|
"name": "bash",
|
|
"arguments": '{"command": "cat src/app.py"}',
|
|
},
|
|
{
|
|
"type": "function_call",
|
|
"call_id": 42,
|
|
"name": "bash",
|
|
"arguments": '{"command": "cat src/app.py"}',
|
|
},
|
|
{
|
|
"type": "function_call_output",
|
|
"call_id": "call_x",
|
|
"output": _NL_OUTPUT,
|
|
},
|
|
],
|
|
}
|
|
|
|
new_payload, modified, _s, _t, _u, _c, _a = _run(handler, payload)
|
|
|
|
assert modified is True
|
|
assert new_payload["input"][0] == "a bare string item"
|
|
assert new_payload["input"][3]["output"] == "kept words"
|