mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-27 14:17:10 -04:00
## Description A Claude Code session that reads a large tool result through `headroom proxy` can fail on turn 2 with Anthropic's `400 prompt_too_long`. The reporter's controlled comparison completed eight turns through 0.33.0 with 187,986 input tokens, while 0.35.0 failed after five requests with 753,077 input tokens. The local regression uses an actual prior optimized request to populate tracker state, then a decision-false bypass turn with Claude-shaped tool-result content. The old unconditional replay path substitutes the compressed prefix; the eligibility gate preserves the client's outbound body without claiming a live provider reproduction. The Anthropic `/v1/messages` route computes whether a request should be compressed, but cached-prefix replay currently runs outside that decision. The replay helper also derives its prefix length from the original message list and applies that index to the optimized list without proving the two lists still align. A stale forwarded prefix can therefore be grafted onto the wrong positions and enlarge later requests. This change limits replay to requests whose existing compression decision permits it and whose pre-upstream backpressure path is inactive. It also makes `overlay_cached_prefix()` decline misaligned or inflating candidates while preserving normal append-only replay. Reported by @itsumonotakumi, whose controlled comparison isolated the failure from compression, headers, one-request serialization, memory, code graph, and CCR. Closes #3026 ## 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 - Gate Anthropic cached-prefix replay on the existing `CompressionDecision.should_compress` result and the existing pre-upstream backpressure state. - Require positional alignment between optimized and original message arrays before replay. - Reject replay candidates that would serialize larger than the current optimized messages. - Add focused handler coverage for the decision-false tool-result regression, bypass and backpressure paths, and outbound optimize-on preservation. - Add direct unit coverage for positional mismatch, no-inflation, and JSON sizing-failure bailouts. - Update the moved-cache-control and pure-block-append regression fixtures to keep the no-inflation contract explicit. - Run the unchanged OpenAI cache-stability preservation proof; no OpenAI production code was edited. ## Testing - [x] Unit tests pass (153 focused proxy, helper, cache-control, block-append, cross-turn, byte-faithful, Anthropic, OpenAI, and backpressure tests) - [x] Linting passes (Ruff check and format validation on the seven changed repository files) - [ ] Type checking passes (`uv run mypy headroom`) - [x] New tests added for new functionality when applicable - [x] Manual testing performed with the in-process proxy and local stub upstream ### Test Output ```text python -m pytest tests/test_proxy/test_anthropic_no_optimize_history_passthrough.py tests/test_cache_prefix_overlay.py tests/test_cross_turn_cache_safety.py tests/test_cache_control_move_bust.py tests/test_proxy_byte_faithful_forwarding.py tests/test_proxy_anthropic_cache_stability.py tests/test_anthropic_pre_upstream_backpressure.py -q python -m pytest tests/test_proxy_openai_cache_stability.py -q python -m pytest tests/test_issue_2671_block_growth_cache.py::test_pure_append_replays_forwarded_blocks_and_advances_breakpoint -q 153 passed across focused invocations, exit code 0 optimize_off turn2_message_count=3 marker_count=1 outbound_compact_utf8_bytes=2293 client_compact_utf8_bytes=2293 optimize_on turn2_message_count=3 client_message_count=3 marker_count=0 outbound_compact_utf8_bytes=171 client_compact_utf8_bytes=182 python -m ruff check headroom/proxy/handlers/anthropic.py headroom/cache/prefix_tracker.py tests/test_proxy/test_anthropic_no_optimize_history_passthrough.py tests/test_cache_prefix_overlay.py tests/test_cache_control_move_bust.py tests/test_issue_2671_block_growth_cache.py tests/test_proxy_openai_cache_stability.py All checks passed!, exit code 0 python -m ruff format headroom/proxy/handlers/anthropic.py headroom/cache/prefix_tracker.py tests/test_proxy/test_anthropic_no_optimize_history_passthrough.py tests/test_cache_prefix_overlay.py tests/test_cache_control_move_bust.py tests/test_issue_2671_block_growth_cache.py tests/test_proxy_openai_cache_stability.py --check 7 files already formatted, exit code 0 git diff --check clean, exit code 0 ``` ## Real Behavior Proof - Environment: Windows 11, Python 3.12 via `uv`, real Headroom proxy app with a local stub Anthropic upstream - Exact command / steps: send an actual optimize-on first request through the in-process proxy with a deterministic production-pipeline seam, then send a decision-false bypass turn containing a large Claude-shaped `tool_result` with moved `cache_control`; separately send an aligned optimize-on turn with a new suffix - Observed result: the exact base checkout fails with `AssertionError: assert 'compressed-tool-result' == 'large-tool-result-marker ...'`; the guarded path passes with the client marker present once and outbound compact JSON no larger than the client body. The optimize-on preservation run records `optimize_on turn2_message_count=3 client_message_count=3 marker_count=0 outbound_compact_utf8_bytes=171 client_compact_utf8_bytes=182`, proving the actual compressed prefix is outbound before the new suffix without turn-2 growth. - Not tested: live Claude Code session against api.anthropic.com on this host ## Runtime Rollout Safety - Rollout-managed feature(s): none. - Minimum rollout channel: N/A. - Stable/default behavior changed: cached-prefix replay now follows the existing compression and backpressure decision and rejects misaligned or inflating candidates. - Kill switch / disable path: no new switch; the existing optimize and bypass controls remain available. - Unsafe override required: none. - Qualification impact: none. - Rollback path: revert the implementation commit. ## 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 - [ ] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [ ] 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 ## Additional Notes `CHANGELOG.md` is not modified because Headroom's release automation generates it from conventional commits. This change does not add a context-limit guard or alter compression, streaming tracker provenance, outbound-body selection, OpenAI behavior, or provider limits. Local tests prove request-body ownership and replay bounds. The reporter's live Claude Code completion and Anthropic token acceptance remain external to this local proof.
511 lines
18 KiB
Python
511 lines
18 KiB
Python
"""Regression tests for OpenAI cache-mode stability in proxy mode."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import copy
|
|
from types import SimpleNamespace
|
|
|
|
import httpx
|
|
import pytest
|
|
|
|
pytest.importorskip("fastapi")
|
|
|
|
from fastapi.testclient import TestClient
|
|
|
|
from headroom.proxy.server import ProxyConfig, create_app
|
|
|
|
|
|
class _FakePrefixTracker:
|
|
def __init__(
|
|
self,
|
|
frozen_count: int,
|
|
previous_original: list[dict] | None = None,
|
|
previous_forwarded: list[dict] | None = None,
|
|
):
|
|
self._frozen_count = frozen_count
|
|
self._previous_original = previous_original or []
|
|
self._previous_forwarded = previous_forwarded or []
|
|
|
|
def get_frozen_message_count(self) -> int:
|
|
return self._frozen_count
|
|
|
|
# Empty history → overlay_cached_prefix() is a no-op here, so these tests
|
|
# keep asserting the cache-freeze behavior they always have. The cross-turn
|
|
# overlay itself is exercised in test_cross_turn_cache_safety.py against the
|
|
# real tracker; these stubs just satisfy the handler's overlay call.
|
|
def get_last_original_messages(self): # noqa: ANN201
|
|
return copy.deepcopy(self._previous_original)
|
|
|
|
def get_last_forwarded_messages(self): # noqa: ANN201
|
|
return copy.deepcopy(self._previous_forwarded)
|
|
|
|
def update_from_response(self, **kwargs): # noqa: ANN003
|
|
return None
|
|
|
|
|
|
def _make_proxy_client() -> TestClient:
|
|
config = ProxyConfig(
|
|
optimize=False,
|
|
cache_enabled=False,
|
|
rate_limit_enabled=False,
|
|
cost_tracking_enabled=False,
|
|
log_requests=False,
|
|
ccr_inject_tool=False,
|
|
ccr_handle_responses=False,
|
|
ccr_context_tracking=False,
|
|
image_optimize=False,
|
|
)
|
|
app = create_app(config)
|
|
return TestClient(app)
|
|
|
|
|
|
def test_openai_cache_mode_freezes_previous_turns() -> None:
|
|
captured = {}
|
|
with _make_proxy_client() as client:
|
|
proxy = client.app.state.proxy
|
|
proxy.config.optimize = True
|
|
proxy.config.mode = "cache"
|
|
|
|
fake_tracker = _FakePrefixTracker(frozen_count=0)
|
|
proxy.session_tracker_store.compute_session_id = lambda request, model, messages: (
|
|
"stable-session"
|
|
)
|
|
proxy.session_tracker_store.get_or_create = lambda session_id, provider: fake_tracker
|
|
|
|
def _fake_apply(**kwargs):
|
|
captured["frozen_message_count"] = kwargs.get("frozen_message_count")
|
|
return SimpleNamespace(
|
|
messages=kwargs["messages"],
|
|
transforms_applied=[],
|
|
timing={},
|
|
tokens_before=60,
|
|
tokens_after=60,
|
|
waste_signals=None,
|
|
)
|
|
|
|
proxy.openai_pipeline.apply = _fake_apply
|
|
|
|
async def _fake_retry(method, url, headers, body, stream=False, **kwargs): # noqa: ANN001
|
|
return httpx.Response(
|
|
200,
|
|
json={
|
|
"id": "chatcmpl_1",
|
|
"choices": [
|
|
{
|
|
"index": 0,
|
|
"message": {"role": "assistant", "content": "ok"},
|
|
"finish_reason": "stop",
|
|
}
|
|
],
|
|
"usage": {"prompt_tokens": 60, "completion_tokens": 3, "total_tokens": 63},
|
|
},
|
|
)
|
|
|
|
proxy._retry_request = _fake_retry
|
|
|
|
response = client.post(
|
|
"/v1/chat/completions",
|
|
headers={"authorization": "Bearer test-key"},
|
|
json={
|
|
"model": "gpt-4o-mini",
|
|
"messages": [
|
|
{"role": "user", "content": "turn1"},
|
|
{"role": "assistant", "content": "turn1-assistant"},
|
|
{"role": "user", "content": "current turn"},
|
|
],
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert captured["frozen_message_count"] == 2
|
|
|
|
|
|
def test_openai_handler_replays_nonempty_cached_prefix() -> None:
|
|
captured = {}
|
|
previous_original = [{"role": "user", "content": "original prefix"}]
|
|
previous_forwarded = [{"role": "user", "content": "comp"}]
|
|
fake_tracker = _FakePrefixTracker(0, previous_original, previous_forwarded)
|
|
with _make_proxy_client() as client:
|
|
proxy = client.app.state.proxy
|
|
proxy.session_tracker_store.compute_session_id = lambda request, model, messages: (
|
|
"stable-session"
|
|
)
|
|
proxy.session_tracker_store.resolve_tracker = lambda *args, **kwargs: fake_tracker
|
|
|
|
async def _fake_retry(method, url, headers, body, stream=False, **kwargs): # noqa: ANN001
|
|
captured["body"] = body
|
|
return httpx.Response(
|
|
200,
|
|
json={
|
|
"id": "chatcmpl_overlay",
|
|
"choices": [
|
|
{
|
|
"index": 0,
|
|
"message": {"role": "assistant", "content": "ok"},
|
|
"finish_reason": "stop",
|
|
}
|
|
],
|
|
"usage": {"prompt_tokens": 20, "completion_tokens": 3, "total_tokens": 23},
|
|
},
|
|
)
|
|
|
|
proxy._retry_request = _fake_retry
|
|
response = client.post(
|
|
"/v1/chat/completions",
|
|
headers={"authorization": "Bearer test-key"},
|
|
json={
|
|
"model": "gpt-4o-mini",
|
|
"messages": [
|
|
{"role": "user", "content": "original prefix"},
|
|
{"role": "user", "content": "new suffix"},
|
|
],
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert captured["body"]["messages"] == [
|
|
previous_forwarded[0],
|
|
{"role": "user", "content": "new suffix"},
|
|
]
|
|
|
|
|
|
@pytest.mark.parametrize("tail_role", ["tool", "function"])
|
|
def test_openai_cache_mode_keeps_final_tool_observation_mutable(tail_role: str) -> None:
|
|
captured = {}
|
|
with _make_proxy_client() as client:
|
|
proxy = client.app.state.proxy
|
|
proxy.config.optimize = True
|
|
proxy.config.mode = "cache"
|
|
|
|
fake_tracker = _FakePrefixTracker(frozen_count=0)
|
|
proxy.session_tracker_store.compute_session_id = lambda request, model, messages: (
|
|
"stable-session"
|
|
)
|
|
proxy.session_tracker_store.get_or_create = lambda session_id, provider: fake_tracker
|
|
|
|
def _fake_apply(**kwargs):
|
|
captured.setdefault("calls", []).append(
|
|
{
|
|
"frozen_message_count": kwargs.get("frozen_message_count"),
|
|
"roles": [msg.get("role") for msg in kwargs["messages"]],
|
|
"mode": proxy.config.mode,
|
|
}
|
|
)
|
|
return SimpleNamespace(
|
|
messages=kwargs["messages"],
|
|
transforms_applied=["test:compress-tail"],
|
|
timing={},
|
|
tokens_before=120,
|
|
tokens_after=80,
|
|
waste_signals=None,
|
|
)
|
|
|
|
proxy.openai_pipeline.apply = _fake_apply
|
|
|
|
async def _fake_retry(method, url, headers, body, stream=False, **kwargs): # noqa: ANN001
|
|
return httpx.Response(
|
|
200,
|
|
json={
|
|
"id": "chatcmpl_tool_tail",
|
|
"choices": [
|
|
{
|
|
"index": 0,
|
|
"message": {"role": "assistant", "content": "ok"},
|
|
"finish_reason": "stop",
|
|
}
|
|
],
|
|
"usage": {"prompt_tokens": 80, "completion_tokens": 3, "total_tokens": 83},
|
|
},
|
|
)
|
|
|
|
proxy._retry_request = _fake_retry
|
|
|
|
tail = {
|
|
"role": tail_role,
|
|
"content": "large command observation " * 200,
|
|
}
|
|
if tail_role == "tool":
|
|
tail["tool_call_id"] = "call_1"
|
|
else:
|
|
tail["name"] = "bash"
|
|
|
|
response = client.post(
|
|
"/v1/chat/completions",
|
|
headers={"authorization": "Bearer test-key"},
|
|
json={
|
|
"model": "gpt-4o-mini",
|
|
"messages": [
|
|
{"role": "user", "content": "turn1"},
|
|
{"role": "assistant", "content": "run command"},
|
|
tail,
|
|
],
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert any(call["frozen_message_count"] == 2 for call in captured["calls"]), captured[
|
|
"calls"
|
|
]
|
|
|
|
|
|
def test_openai_cache_mode_restores_mutated_frozen_prefix() -> None:
|
|
captured = {}
|
|
with _make_proxy_client() as client:
|
|
proxy = client.app.state.proxy
|
|
proxy.config.optimize = True
|
|
proxy.config.mode = "cache"
|
|
|
|
fake_tracker = _FakePrefixTracker(frozen_count=0)
|
|
proxy.session_tracker_store.compute_session_id = lambda request, model, messages: (
|
|
"stable-session"
|
|
)
|
|
proxy.session_tracker_store.get_or_create = lambda session_id, provider: fake_tracker
|
|
|
|
original_messages = [
|
|
{"role": "user", "content": "turn1"},
|
|
{"role": "assistant", "content": "turn1-assistant"},
|
|
{"role": "user", "content": "current turn"},
|
|
]
|
|
|
|
def _fake_apply(**kwargs):
|
|
mutated = list(kwargs["messages"])
|
|
mutated[0] = {**mutated[0], "content": "MUTATED_PREFIX"}
|
|
return SimpleNamespace(
|
|
messages=mutated,
|
|
transforms_applied=["fake:mutated"],
|
|
timing={},
|
|
tokens_before=70,
|
|
tokens_after=65,
|
|
waste_signals=None,
|
|
)
|
|
|
|
proxy.openai_pipeline.apply = _fake_apply
|
|
|
|
async def _fake_retry(method, url, headers, body, stream=False, **kwargs): # noqa: ANN001
|
|
captured["body"] = body
|
|
return httpx.Response(
|
|
200,
|
|
json={
|
|
"id": "chatcmpl_2",
|
|
"choices": [
|
|
{
|
|
"index": 0,
|
|
"message": {"role": "assistant", "content": "ok"},
|
|
"finish_reason": "stop",
|
|
}
|
|
],
|
|
"usage": {"prompt_tokens": 65, "completion_tokens": 3, "total_tokens": 68},
|
|
},
|
|
)
|
|
|
|
proxy._retry_request = _fake_retry
|
|
|
|
response = client.post(
|
|
"/v1/chat/completions",
|
|
headers={"authorization": "Bearer test-key"},
|
|
json={
|
|
"model": "gpt-4o-mini",
|
|
"messages": original_messages,
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
sent_messages = captured["body"]["messages"]
|
|
assert sent_messages[0] == original_messages[0]
|
|
assert sent_messages[1] == original_messages[1]
|
|
|
|
|
|
# ─── Issue #327 cross-handler regression ────────────────────────────────
|
|
#
|
|
# The OpenAI handler was never affected by issue #327's content-keyed walker
|
|
# bug — it has only ever used `compute_frozen_count` (positional). This test
|
|
# locks that property by spying on the OpenAI traffic path and asserting that
|
|
# the buggy walker functions (`should_defer_compression`, `mark_stable`) are
|
|
# never called from the production handler. If a future refactor accidentally
|
|
# adds the same walker to OpenAI, this test fails immediately.
|
|
|
|
|
|
def test_issue_327_openai_handler_does_not_call_walker_functions() -> None:
|
|
calls: list[tuple[str, tuple, dict]] = []
|
|
|
|
class _SpyCompCache:
|
|
def apply_cached(self, messages): # noqa: ANN001
|
|
calls.append(("apply_cached", (), {}))
|
|
return list(messages)
|
|
|
|
def compute_frozen_count(self, messages): # noqa: ANN001
|
|
calls.append(("compute_frozen_count", (), {}))
|
|
return 0
|
|
|
|
def update_from_result(self, originals, compressed): # noqa: ANN001
|
|
calls.append(("update_from_result", (), {}))
|
|
|
|
def mark_stable_from_messages(self, messages, up_to): # noqa: ANN001
|
|
calls.append(("mark_stable_from_messages", (up_to,), {}))
|
|
|
|
# Methods below MUST NOT be called from OpenAI handler.
|
|
def should_defer_compression(self, *args, **kwargs): # noqa: ANN001, ANN002, ANN003
|
|
calls.append(("should_defer_compression", args, kwargs))
|
|
return False
|
|
|
|
def mark_stable(self, content_hash): # noqa: ANN001
|
|
calls.append(("mark_stable", (content_hash,), {}))
|
|
|
|
@staticmethod
|
|
def content_hash(content): # noqa: ANN001
|
|
return f"H({content[:40] if isinstance(content, str) else 'list'})"
|
|
|
|
with _make_proxy_client() as client:
|
|
proxy = client.app.state.proxy
|
|
proxy.config.optimize = True
|
|
proxy.config.mode = "token" # token mode is where Anthropic had the bug
|
|
|
|
fake_tracker = _FakePrefixTracker(frozen_count=0)
|
|
proxy.session_tracker_store.compute_session_id = lambda request, model, messages: (
|
|
"openai-spy-session"
|
|
)
|
|
proxy.session_tracker_store.get_or_create = lambda s, p: fake_tracker
|
|
proxy._get_compression_cache = lambda s: _SpyCompCache()
|
|
|
|
def _fake_apply(**kwargs): # noqa: ANN003
|
|
return SimpleNamespace(
|
|
messages=list(kwargs["messages"]),
|
|
transforms_applied=[],
|
|
timing={},
|
|
tokens_before=60,
|
|
tokens_after=60,
|
|
waste_signals=None,
|
|
)
|
|
|
|
proxy.openai_pipeline.apply = _fake_apply
|
|
|
|
async def _fake_retry(method, url, headers, body, stream=False, **kwargs): # noqa: ANN001
|
|
return httpx.Response(
|
|
200,
|
|
json={
|
|
"id": "cmpl",
|
|
"choices": [
|
|
{
|
|
"index": 0,
|
|
"message": {"role": "assistant", "content": "ok"},
|
|
"finish_reason": "stop",
|
|
}
|
|
],
|
|
"usage": {"prompt_tokens": 60, "completion_tokens": 3, "total_tokens": 63},
|
|
},
|
|
)
|
|
|
|
proxy._retry_request = _fake_retry
|
|
|
|
# Drive 5 turns so any walker bug would have time to fire repeatedly.
|
|
for turn in range(5):
|
|
r = client.post(
|
|
"/v1/chat/completions",
|
|
headers={"authorization": "Bearer test-key"},
|
|
json={
|
|
"model": "gpt-4o-mini",
|
|
"messages": [
|
|
{"role": "user", "content": f"turn-{turn}-q"},
|
|
{"role": "assistant", "content": f"turn-{turn}-a"},
|
|
{"role": "tool", "tool_call_id": "t1", "content": "x" * 600},
|
|
{"role": "user", "content": f"continue-{turn}"},
|
|
],
|
|
},
|
|
)
|
|
assert r.status_code == 200
|
|
|
|
method_names = [c[0] for c in calls]
|
|
assert "should_defer_compression" not in method_names, (
|
|
f"OpenAI handler unexpectedly called should_defer_compression. "
|
|
f"Calls observed: {method_names}"
|
|
)
|
|
assert "mark_stable" not in method_names, (
|
|
f"OpenAI handler unexpectedly called mark_stable (the walker side-effect). "
|
|
f"Calls observed: {method_names}"
|
|
)
|
|
# Sanity: the safe positional methods DID fire.
|
|
assert "compute_frozen_count" in method_names
|
|
assert "apply_cached" in method_names
|
|
|
|
|
|
def test_openai_chat_completions_compacts_tools_when_profile_enabled() -> None:
|
|
captured = {}
|
|
with _make_proxy_client() as client:
|
|
proxy = client.app.state.proxy
|
|
proxy.config.optimize = True
|
|
proxy.config.mode = "token"
|
|
proxy.config.savings_profile = "agent-90"
|
|
|
|
def _fake_apply(**kwargs): # noqa: ANN003
|
|
return SimpleNamespace(
|
|
messages=kwargs["messages"],
|
|
transforms_applied=[],
|
|
timing={},
|
|
tokens_before=10,
|
|
tokens_after=10,
|
|
waste_signals=None,
|
|
)
|
|
|
|
proxy.openai_pipeline.apply = _fake_apply
|
|
|
|
async def _fake_retry(method, url, headers, body, stream=False, **kwargs): # noqa: ANN001
|
|
captured["body"] = body
|
|
return httpx.Response(
|
|
200,
|
|
json={
|
|
"id": "chatcmpl_tools",
|
|
"choices": [
|
|
{
|
|
"index": 0,
|
|
"message": {"role": "assistant", "content": "ok"},
|
|
"finish_reason": "stop",
|
|
}
|
|
],
|
|
"usage": {"prompt_tokens": 500, "completion_tokens": 3, "total_tokens": 503},
|
|
},
|
|
)
|
|
|
|
proxy._retry_request = _fake_retry
|
|
verbose_schema_note = "schema annotation repeated for opencode tool definitions " * 50
|
|
tools = [
|
|
{
|
|
"type": "function",
|
|
"function": {
|
|
"name": "read_file",
|
|
"description": "read file helper",
|
|
"parameters": {
|
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
"title": "ReadFileParameters",
|
|
"type": "object",
|
|
"properties": {
|
|
"path": {
|
|
"type": "string",
|
|
"title": "Path",
|
|
"description": verbose_schema_note,
|
|
"examples": [verbose_schema_note],
|
|
}
|
|
},
|
|
"required": ["path"],
|
|
},
|
|
},
|
|
}
|
|
]
|
|
|
|
response = client.post(
|
|
"/v1/chat/completions",
|
|
headers={"authorization": "Bearer test-key"},
|
|
json={
|
|
"model": "gpt-4o-mini",
|
|
"messages": [{"role": "user", "content": "inspect this file"}],
|
|
"tools": tools,
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert "openai:chat:tool_schema_compaction" in response.headers["x-headroom-transforms"]
|
|
assert int(response.headers["x-headroom-tokens-saved"]) > 0
|
|
sent_params = captured["body"]["tools"][0]["function"]["parameters"]
|
|
assert "$schema" not in sent_params
|
|
assert "title" not in sent_params
|
|
assert "title" not in sent_params["properties"]["path"]
|
|
assert "examples" not in sent_params["properties"]["path"]
|