headroom/tests/test_proxy_context_budget.py

1198 lines
44 KiB
Python

"""Context budget guard tests for headroom target #2649.
Test matrix follows the Required Proof Matrix in the execution prompt:
reproduction base-fails/head-passes contract
mode_* / variant_* every Fault Scope variant is classified
preservation unconfigured requests forward unchanged
negative_space at-threshold / observe-over / bypassed-over all forward
production_route real POST /v1/messages through create_app
contract_isolation policy module has no FastAPI/handler/provider imports
"""
from __future__ import annotations
import asyncio
import importlib
import json
import os
from types import SimpleNamespace
from typing import Any
from unittest.mock import MagicMock, patch
import anyio
import pytest
from fastapi import Request
from fastapi.testclient import TestClient
# --------------------------------------------------------------------------- #
# Shared helpers #
# --------------------------------------------------------------------------- #
class _DummyTokenizer:
"""Token counter that returns a configurable value."""
def __init__(self, count: int = 1) -> None:
self._count = count
def count_messages(self, messages) -> int:
return self._count
def count_text(self, text: str) -> int:
return self._count
def count(self, messages) -> int:
return self._count
class _FinalizedBodyTokenizer(_DummyTokenizer):
"""Expose system and tool content only when the final body is counted."""
def count_messages(self, messages) -> int:
count = self._count
if any(message.get("role") == "system" for message in messages):
count += 50_000
return count
def count_text(self, text: str) -> int:
return 50_000 if "large-tool-schema" in text else 0
class _DummyMetrics:
def __init__(self) -> None:
self.stage_timings: list = []
async def record_request(self, **kwargs) -> None:
return None
async def record_stage_timings(self, path, timings) -> None:
self.stage_timings.append((path, timings))
async def record_failed(self, **kwargs) -> None:
return None
async def record_rate_limited(self, **kwargs) -> None:
return None
def record_compression_failed(self, reason: str) -> None:
return None
def _stub_response(status: int = 200) -> Any:
body = json.dumps(
{
"id": "msg_test",
"type": "message",
"role": "assistant",
"content": [{"type": "text", "text": "ok"}],
"model": "step-router-v1",
"stop_reason": "end_turn",
"usage": {"input_tokens": 1, "output_tokens": 1},
}
)
ns = SimpleNamespace()
ns.status_code = status
ns.headers = {"content-type": "application/json"}
ns.content = body.encode()
ns.text = body
ns.json = lambda: json.loads(body)
return ns
def _make_anthropic_provider(operator_limit: int | None = None) -> Any:
limits: dict[str, int] = {}
if operator_limit is not None:
limits["step-router-v1"] = operator_limit
def _get_operator_limit(model: str) -> int | None:
if model in limits:
return limits[model]
from headroom.providers.anthropic import sanitize_anthropic_model_id
sanitized = sanitize_anthropic_model_id(model)
return limits.get(sanitized)
return SimpleNamespace(
get_context_limit=lambda model: 200_000,
get_operator_context_limit=_get_operator_limit,
has_raw_operator_context_limit=lambda model: model in limits,
_operator_context_limits=limits,
)
class _DummyHandler:
"""Minimal AnthropicHandlerMixin with configurable upstream stub."""
from headroom.proxy.handlers.anthropic import AnthropicHandlerMixin
ANTHROPIC_API_URL = "https://api.anthropic.com"
def __init__(
self,
*,
operator_limit: int | None = None,
token_count: int = 1,
bypass: bool = False,
mode_header: str | None = None,
) -> None:
from headroom.proxy.models import ProxyConfig
# Inherit the mixin by embedding via __class__ trick is messy;
# subclass it inline.
self._operator_limit = operator_limit
self._token_count = token_count
self._bypass_header = bypass
self.upstream_calls: list[dict] = []
self.upstream_bodies: list[dict] = []
self.upstream_original_body_bytes: list[bytes | None] = []
self.rate_limiter = None
self.metrics = _DummyMetrics()
self.config = ProxyConfig(
optimize=False,
image_optimize=False,
retry_max_attempts=1,
retry_base_delay_ms=1,
retry_max_delay_ms=1,
connect_timeout_seconds=10,
mode="token",
cache_enabled=False,
rate_limit_enabled=False,
fallback_enabled=False,
fallback_provider=None,
prefix_freeze_enabled=False,
memory_enabled=False,
)
self.usage_reporter = None
self.anthropic_provider = _make_anthropic_provider(operator_limit)
self.anthropic_pipeline = SimpleNamespace(apply=MagicMock())
self.anthropic_backend = None
self.cost_tracker = None
self.memory_handler = None
self.cache = None
self.security = None
self.ccr_context_tracker = None
self.ccr_injector = None
self.ccr_response_handler = None
self.ccr_feedback = None
self.ccr_batch_processor = None
self.ccr_mcp_server = None
self.traffic_learner = None
self.tool_injector = None
self.read_lifecycle_manager = None
self.logger = SimpleNamespace(log=lambda *a, **k: None)
self.request_logger = self.logger
self.usage_observer = None
self.image_compressor = None
self.session_tracker_store = SimpleNamespace(
compute_session_id=lambda *a, **k: "sess-budget-test",
get_or_create=lambda *a, **k: SimpleNamespace(
_cached_token_count=0,
get_frozen_message_count=lambda: 0,
get_last_original_messages=lambda: [],
get_last_forwarded_messages=lambda: [],
update_from_response=lambda *a, **k: None,
record_request=lambda *a, **k: None,
),
resolve_tracker=lambda *a, **k: SimpleNamespace(
_cached_token_count=0,
get_frozen_message_count=lambda: 0,
get_last_original_messages=lambda: [],
get_last_forwarded_messages=lambda: [],
update_from_response=lambda *a, **k: None,
record_request=lambda *a, **k: None,
),
)
self.anthropic_pre_upstream_sem = None
self.anthropic_pre_upstream_concurrency = 0
import concurrent.futures as _cf
import threading as _threading
self._compression_executor = _cf.ThreadPoolExecutor(max_workers=2)
self.compression_max_workers = 2
self._compression_in_flight = 0
self._compression_in_flight_max = 0
self._compression_leaked_threads = 0
self._compression_metrics_lock = _threading.Lock()
self._background_compression_enabled = False
async def _run_compression_in_executor(self, fn, *, timeout):
loop = asyncio.get_running_loop()
future = loop.run_in_executor(self._compression_executor, fn)
return await asyncio.wait_for(future, timeout=timeout)
async def _record_request_outcome(self, outcome) -> None:
from headroom.proxy.outcome import emit_request_outcome
await emit_request_outcome(self, outcome)
async def _next_request_id(self) -> str:
return "req-budget-test"
def _extract_tags(self, headers):
return {}
async def _retry_request(self, method, url, headers, body, **_kwargs):
self.upstream_calls.append({"method": method, "url": url})
self.upstream_bodies.append(json.loads(body) if isinstance(body, bytes) else body)
self.upstream_original_body_bytes.append(_kwargs.get("original_body_bytes"))
return _stub_response()
def _get_compression_cache(self, session_id):
return SimpleNamespace(
apply_cached=lambda m: m,
compute_frozen_count=lambda m: 0,
mark_stable_from_messages=lambda *a, **k: None,
should_defer_compression=lambda h: False,
mark_stable=lambda h: None,
content_hash=lambda c: "h",
update_from_result=lambda *a, **k: None,
_cache={},
_stable_hashes=set(),
)
def _extract_anthropic_cache_ttl_metrics(self, usage):
return (0, 0)
def _make_handler_subclass() -> type:
"""Return a concrete class inheriting both _DummyHandler and AnthropicHandlerMixin."""
from headroom.proxy.handlers.anthropic import AnthropicHandlerMixin
class _BudgetHandler(_DummyHandler, AnthropicHandlerMixin):
pass
return _BudgetHandler
def _build_request(
body: dict,
headers: dict[str, str] | None = None,
) -> Request:
h = {"authorization": "Bearer sk-ant-api-test"}
if headers:
h.update(headers)
payload = json.dumps(body).encode("utf-8")
async def receive():
return {"type": "http.request", "body": payload, "more_body": False}
scope = {
"type": "http",
"asgi": {"version": "3.0"},
"http_version": "1.1",
"method": "POST",
"scheme": "https",
"path": "/v1/messages",
"raw_path": b"/v1/messages",
"query_string": b"",
"headers": [(k.lower().encode(), v.encode()) for k, v in h.items()],
"client": ("127.0.0.1", 12345),
"server": ("testserver", 443),
}
return Request(scope, receive)
# --------------------------------------------------------------------------- #
# Policy unit tests (contract_isolation) #
# --------------------------------------------------------------------------- #
def test_contract_isolation_no_fastapi_import():
"""The policy module must not import FastAPI, the handler, or the provider."""
src = importlib.import_module("headroom.proxy.context_budget_policy")
# Walk module's __dict__ for imported symbols that would indicate a bad dep.
forbidden = {"fastapi", "headroom.proxy.handlers", "headroom.providers"}
for mod_name in list(src.__dict__.keys()):
obj = src.__dict__[mod_name]
if hasattr(obj, "__module__") and obj.__module__ is not None:
for f in forbidden:
assert not obj.__module__.startswith(f), (
f"Policy module imported {obj.__module__!r} (matches forbidden prefix {f!r})"
)
def test_contract_isolation_standalone_evaluate_matches_handler(monkeypatch):
"""evaluate() produces the same result whether called directly or via the handler."""
from headroom.proxy.context_budget_policy import evaluate, resolve_mode, resolve_safety_margin
monkeypatch.setenv("HEADROOM_CONTEXT_LIMIT_MODE", "reject")
monkeypatch.setenv("HEADROOM_CONTEXT_LIMIT_SAFETY_MARGIN", "0")
mode = resolve_mode()
margin = resolve_safety_margin()
# Standalone call
direct = evaluate(
counted_tokens=270_000,
declared_limit=262_144,
max_output_tokens=8_192,
mode=mode,
safety_margin=margin,
)
assert direct.should_reject is True
assert direct.reason == "over_threshold"
BudgetHandler = _make_handler_subclass()
handler = BudgetHandler(operator_limit=262_144, token_count=270_000)
req = _build_request(
{
"model": "step-router-v1",
"messages": [{"role": "user", "content": "hi"}],
"max_tokens": 8_192,
}
)
import headroom.tokenizers as _tk
monkeypatch.setattr(_tk, "get_tokenizer", lambda m: _DummyTokenizer(270_000))
response = anyio.run(handler.handle_anthropic_messages, req)
assert response.status_code == 400
assert len(handler.upstream_calls) == 0
# --------------------------------------------------------------------------- #
# Policy evaluate branches (mode_ / variant_) #
# --------------------------------------------------------------------------- #
def test_mode_no_declared_limit():
from headroom.proxy.context_budget_policy import evaluate
d = evaluate(
counted_tokens=300_000,
declared_limit=None,
max_output_tokens=8_192,
mode="reject",
safety_margin=0,
)
assert d.reason == "no_declared_limit"
assert d.should_reject is False
def test_mode_observe_under_threshold():
from headroom.proxy.context_budget_policy import evaluate
d = evaluate(
counted_tokens=100_000,
declared_limit=262_144,
max_output_tokens=8_192,
mode="observe",
safety_margin=0,
)
assert d.reason == "under_threshold"
assert d.should_reject is False
def test_mode_observe_over_threshold():
from headroom.proxy.context_budget_policy import evaluate
d = evaluate(
counted_tokens=260_000,
declared_limit=262_144,
max_output_tokens=8_192,
mode="observe",
safety_margin=0,
)
assert d.reason == "over_threshold"
assert d.should_reject is False # observe => no rejection
def test_variant_observe_over_threshold_logs_decision_fields(monkeypatch, caplog):
monkeypatch.setenv("HEADROOM_CONTEXT_LIMIT_MODE", "observe")
monkeypatch.setenv("HEADROOM_CONTEXT_LIMIT_SAFETY_MARGIN", "0")
import headroom.tokenizers as _tk
BudgetHandler = _make_handler_subclass()
handler = BudgetHandler(operator_limit=262_144, token_count=260_000)
req = _build_request(
{
"model": "step-router-v1",
"messages": [{"role": "user", "content": "hi"}],
"max_tokens": 8_192,
}
)
monkeypatch.setattr(_tk, "get_tokenizer", lambda m: _DummyTokenizer(260_000))
anyio.run(handler.handle_anthropic_messages, req)
assert len(handler.upstream_calls) == 1
warning = next(
record.getMessage()
for record in caplog.records
if "context_budget_guard" in record.getMessage()
)
assert "declared_limit=262144" in warning
assert "reserve=8192" in warning
assert "threshold=253952" in warning
assert "counted=260000" in warning
assert "overage=6048" in warning
assert "mode=observe" in warning
def test_mode_reject_over_threshold():
from headroom.proxy.context_budget_policy import evaluate
d = evaluate(
counted_tokens=260_000,
declared_limit=262_144,
max_output_tokens=8_192,
mode="reject",
safety_margin=0,
)
assert d.reason == "over_threshold"
assert d.should_reject is True
def test_mode_resolvers_default_and_invalid(monkeypatch):
from headroom.proxy.context_budget_policy import resolve_mode, resolve_safety_margin
monkeypatch.delenv("HEADROOM_CONTEXT_LIMIT_MODE", raising=False)
monkeypatch.delenv("HEADROOM_CONTEXT_LIMIT_SAFETY_MARGIN", raising=False)
assert resolve_mode() == "observe"
assert resolve_safety_margin() == 0
monkeypatch.setenv("HEADROOM_CONTEXT_LIMIT_MODE", "invalid")
with pytest.raises(ValueError, match="accepted values"):
resolve_mode()
monkeypatch.setenv("HEADROOM_CONTEXT_LIMIT_SAFETY_MARGIN", "not-an-int")
with pytest.raises(ValueError, match="not an integer"):
resolve_safety_margin()
monkeypatch.setenv("HEADROOM_CONTEXT_LIMIT_SAFETY_MARGIN", "-1")
with pytest.raises(ValueError, match="must be >= 0"):
resolve_safety_margin()
def test_variant_degenerate_threshold():
"""Equal max_output_tokens (threshold=0) is over budget in reject mode.
A request whose reserved output consumes the entire window cannot fit by
construction, so reject mode refuses it instead of forwarding (review
4850625429 regression).
"""
from headroom.proxy.context_budget_policy import evaluate
d = evaluate(
counted_tokens=100_000,
declared_limit=8_192,
max_output_tokens=8_192, # equal to declared_limit -> threshold=0
mode="reject",
safety_margin=0,
)
assert d.reason == "degenerate_threshold"
assert d.threshold == 0
assert d.should_reject is True
assert d.overage == 100_000 # counted_tokens - threshold
def test_variant_degenerate_threshold_max_output_exceeds():
"""max_output_tokens > declared_limit is over budget in reject mode."""
from headroom.proxy.context_budget_policy import evaluate
d = evaluate(
counted_tokens=100_000,
declared_limit=8_192,
max_output_tokens=16_000,
mode="reject",
safety_margin=0,
)
assert d.reason == "degenerate_threshold"
assert d.threshold == 8_192 - 16_000
assert d.should_reject is True
assert d.overage == 100_000 - d.threshold
def test_variant_degenerate_threshold_observe_forwards():
"""Observe mode keeps a non-positive threshold non-rejecting and logs."""
from headroom.proxy.context_budget_policy import evaluate
d = evaluate(
counted_tokens=100_000,
declared_limit=8_192,
max_output_tokens=8_192,
mode="observe",
safety_margin=0,
)
assert d.reason == "degenerate_threshold"
assert d.should_reject is False
assert d.overage == 100_000
def test_variant_degenerate_threshold_safety_margin_reject():
"""safety_margin >= declared_limit is over budget in reject mode."""
from headroom.proxy.context_budget_policy import evaluate
d = evaluate(
counted_tokens=100_000,
declared_limit=8_192,
max_output_tokens=4_096,
mode="reject",
safety_margin=8_192, # equal to declared_limit -> threshold <= 0
)
assert d.reason == "degenerate_threshold"
assert d.threshold <= 0
assert d.should_reject is True
assert d.overage == 100_000 - d.threshold
def test_variant_degenerate_threshold_safety_margin_observe():
"""Degenerate safety_margin in observe mode stays non-rejecting."""
from headroom.proxy.context_budget_policy import evaluate
d = evaluate(
counted_tokens=100_000,
declared_limit=8_192,
max_output_tokens=4_096,
mode="observe",
safety_margin=8_192,
)
assert d.reason == "degenerate_threshold"
assert d.should_reject is False
def test_handler_degenerate_max_tokens_rejects_with_zero_upstream(monkeypatch):
"""Handler: max_tokens >= declared_limit in reject mode returns 400 locally.
Review 4850625429 scenario: declared_limit=200_000, max_output_tokens=200_000,
counted_tokens=1, mode=reject. The threshold is 0, which must count as over
budget so the request is refused before any upstream attempt.
"""
monkeypatch.setenv("HEADROOM_CONTEXT_LIMIT_MODE", "reject")
monkeypatch.setenv("HEADROOM_CONTEXT_LIMIT_SAFETY_MARGIN", "0")
import headroom.tokenizers as _tk
BudgetHandler = _make_handler_subclass()
handler = BudgetHandler(operator_limit=200_000, token_count=1)
req = _build_request(
{
"model": "step-router-v1",
"messages": [{"role": "user", "content": "hi"}],
"max_tokens": 200_000, # equals declared_limit -> threshold=0
},
)
monkeypatch.setattr(_tk, "get_tokenizer", lambda m: _DummyTokenizer(1))
resp = anyio.run(handler.handle_anthropic_messages, req)
assert resp.status_code == 400, f"Expected local 400, got {resp.status_code}"
body = json.loads(resp.body)
assert "step-router-v1" in body["error"]["message"]
assert len(handler.upstream_calls) == 0
def test_handler_degenerate_safety_margin_rejects_with_zero_upstream(monkeypatch):
"""Handler: safety_margin >= declared_limit in reject mode returns 400 locally."""
monkeypatch.setenv("HEADROOM_CONTEXT_LIMIT_MODE", "reject")
monkeypatch.setenv("HEADROOM_CONTEXT_LIMIT_SAFETY_MARGIN", "200000")
import headroom.tokenizers as _tk
BudgetHandler = _make_handler_subclass()
handler = BudgetHandler(operator_limit=200_000, token_count=1)
req = _build_request(
{
"model": "step-router-v1",
"messages": [{"role": "user", "content": "hi"}],
"max_tokens": 8_192,
},
)
monkeypatch.setattr(_tk, "get_tokenizer", lambda m: _DummyTokenizer(1))
resp = anyio.run(handler.handle_anthropic_messages, req)
assert resp.status_code == 400, f"Expected local 400, got {resp.status_code}"
assert len(handler.upstream_calls) == 0
def test_handler_degenerate_observe_logs_and_forwards(monkeypatch, caplog):
"""Handler: observe mode logs a degenerate threshold and still forwards."""
monkeypatch.setenv("HEADROOM_CONTEXT_LIMIT_MODE", "observe")
monkeypatch.setenv("HEADROOM_CONTEXT_LIMIT_SAFETY_MARGIN", "0")
import headroom.tokenizers as _tk
BudgetHandler = _make_handler_subclass()
handler = BudgetHandler(operator_limit=200_000, token_count=1)
req = _build_request(
{
"model": "step-router-v1",
"messages": [{"role": "user", "content": "hi"}],
"max_tokens": 200_000,
},
)
monkeypatch.setattr(_tk, "get_tokenizer", lambda m: _DummyTokenizer(1))
anyio.run(handler.handle_anthropic_messages, req)
assert len(handler.upstream_calls) == 1
warning = next(
record.getMessage()
for record in caplog.records
if "context_budget_guard" in record.getMessage()
)
assert "declared_limit=200000" in warning
assert "threshold=0" in warning
assert "overage=1" in warning
def test_variant_safety_margin_adds_reserve():
from headroom.proxy.context_budget_policy import evaluate
# declared_limit=262144, max_output_tokens=8192, safety_margin=10000
# reserve = max(10000, 8192) = 10000
# threshold = 262144 - 10000 = 252144
# count = 253000 > 252144 => over
d = evaluate(
counted_tokens=253_000,
declared_limit=262_144,
max_output_tokens=8_192,
mode="reject",
safety_margin=10_000,
)
assert d.reason == "over_threshold"
assert d.reserve == 10_000
assert d.threshold == 252_144
assert d.overage == 253_000 - 252_144
def test_variant_bypass_not_evaluated(monkeypatch):
"""The bypass variant skips evaluation entirely — tested via handler."""
monkeypatch.setenv("HEADROOM_CONTEXT_LIMIT_MODE", "reject")
monkeypatch.setenv("HEADROOM_CONTEXT_LIMIT_SAFETY_MARGIN", "0")
import headroom.tokenizers as _tk
BudgetHandler = _make_handler_subclass()
handler = BudgetHandler(operator_limit=262_144, token_count=300_000)
req = _build_request(
{
"model": "step-router-v1",
"messages": [{"role": "user", "content": "hi"}],
"max_tokens": 8_192,
},
{"x-headroom-bypass": "true"},
)
monkeypatch.setattr(_tk, "get_tokenizer", lambda m: _DummyTokenizer(300_000))
anyio.run(handler.handle_anthropic_messages, req)
# Guard must not fire: upstream call must have happened
assert len(handler.upstream_calls) == 1
def test_variant_context1m_without_raw_declaration_degrades_to_observe(monkeypatch, caplog):
"""context-1m beta without raw-id declaration degrades to observe even in reject mode."""
monkeypatch.setenv("HEADROOM_CONTEXT_LIMIT_MODE", "reject")
monkeypatch.setenv("HEADROOM_CONTEXT_LIMIT_SAFETY_MARGIN", "0")
import headroom.tokenizers as _tk
# Operator declared 'claude-opus-4' (sanitized form) but NOT 'claude-opus-4[1m]'
BudgetHandler = _make_handler_subclass()
handler = BudgetHandler(operator_limit=None, token_count=300_000)
# The sanitized declaration must be present so this is a true context-1m false positive.
handler.anthropic_provider._operator_context_limits.update({"claude-opus-4": 262_144})
req = _build_request(
{
"model": "claude-opus-4[1m]",
"messages": [{"role": "user", "content": "hi"}],
"max_tokens": 8_192,
},
# Configured header casing must not change context-1m detection.
{"anthropic-beta": "Context-1M"},
)
monkeypatch.setattr(_tk, "get_tokenizer", lambda m: _DummyTokenizer(300_000))
anyio.run(handler.handle_anthropic_messages, req)
# Degrades to observe: request forwards (not rejected)
assert len(handler.upstream_calls) == 1
assert any("no raw model declaration" in record.getMessage() for record in caplog.records)
def test_variant_guard_internal_error_forwards(monkeypatch):
"""Any exception inside the guard block forwards the request unchanged."""
monkeypatch.setenv("HEADROOM_CONTEXT_LIMIT_MODE", "reject")
import headroom.proxy.context_budget_policy as _pol
import headroom.tokenizers as _tk
BudgetHandler = _make_handler_subclass()
handler = BudgetHandler(operator_limit=262_144, token_count=300_000)
def _bad_evaluate(**kwargs):
raise RuntimeError("synthetic policy error")
monkeypatch.setattr(_pol, "evaluate", _bad_evaluate)
monkeypatch.setattr(_tk, "get_tokenizer", lambda m: _DummyTokenizer(300_000))
req = _build_request(
{
"model": "step-router-v1",
"messages": [{"role": "user", "content": "hi"}],
"max_tokens": 8_192,
},
)
anyio.run(handler.handle_anthropic_messages, req)
# Forwarded despite policy error
assert len(handler.upstream_calls) == 1
def test_variant_invalid_guard_configuration_forwards_with_warning(monkeypatch, caplog):
"""Invalid operator values fail open and identify the configuration fix."""
monkeypatch.setenv("HEADROOM_CONTEXT_LIMIT_MODE", "invalid")
import headroom.tokenizers as _tk
BudgetHandler = _make_handler_subclass()
handler = BudgetHandler(operator_limit=262_144, token_count=300_000)
req = _build_request(
{
"model": "step-router-v1",
"messages": [{"role": "user", "content": "hi"}],
"max_tokens": 8_192,
}
)
monkeypatch.setattr(_tk, "get_tokenizer", lambda m: _DummyTokenizer(300_000))
anyio.run(handler.handle_anthropic_messages, req)
assert len(handler.upstream_calls) == 1
assert any(
"invalid configuration" in record.getMessage()
and "forwarding unchanged" in record.getMessage()
for record in caplog.records
)
# --------------------------------------------------------------------------- #
# Preservation (preservation) #
# --------------------------------------------------------------------------- #
def test_preservation_unconfigured_install_forwards(monkeypatch):
"""Without any operator limit, requests forward byte-identically."""
import headroom.tokenizers as _tk
BudgetHandler = _make_handler_subclass()
handler = BudgetHandler(operator_limit=None, token_count=999_999)
original_body = {
"model": "step-router-v1",
"messages": [{"role": "user", "content": "hi"}],
"max_tokens": 8_192,
}
req = _build_request(original_body)
monkeypatch.setattr(_tk, "get_tokenizer", lambda m: _DummyTokenizer(999_999))
anyio.run(handler.handle_anthropic_messages, req)
assert len(handler.upstream_calls) == 1
assert handler.upstream_bodies[-1] == original_body
assert handler.upstream_original_body_bytes[-1] == json.dumps(original_body).encode()
def test_preservation_get_context_limit_unchanged():
"""get_context_limit behavior is identical before and after the change."""
from headroom.providers.anthropic import AnthropicProvider
provider = AnthropicProvider(
context_limits={"test-model": 200_000},
warn=False,
)
assert provider.get_context_limit("test-model") == 200_000
def test_preservation_get_operator_context_limit_no_declaration():
"""get_operator_context_limit returns None for undeclared models."""
from headroom.providers.anthropic import AnthropicProvider
provider = AnthropicProvider(warn=False)
# step-router-v1 is not in the built-in table so operator has not declared it
result = provider.get_operator_context_limit("step-router-v1")
assert result is None
def test_preservation_get_operator_context_limit_declared():
"""get_operator_context_limit returns the declared value for a declared model."""
import json
from headroom.providers.anthropic import AnthropicProvider
limits = json.dumps({"context_limits": {"step-router-v1": 262_144}})
with patch.dict(os.environ, {"HEADROOM_MODEL_LIMITS": limits}):
provider = AnthropicProvider(warn=False)
assert provider.get_operator_context_limit("step-router-v1") == 262_144
# Must not affect get_context_limit behavior
assert provider.get_context_limit("step-router-v1") == 262_144
def test_preservation_get_operator_context_limit_sanitized_variant():
"""A sanitized lookup finds the declaration for a styled model id."""
from headroom.providers.anthropic import AnthropicProvider
provider = AnthropicProvider(
context_limits={"claude-opus-4": 262_144},
warn=False,
)
assert provider.get_operator_context_limit("claude-opus-4[1m]") == 262_144
assert provider.has_raw_operator_context_limit("claude-opus-4[1m]") is False
def test_preservation_has_raw_operator_context_limit():
"""A declaration keyed by the styled id is recognized as raw."""
from headroom.providers.anthropic import AnthropicProvider
provider = AnthropicProvider(
context_limits={"claude-opus-4[1m]": 1_000_000},
warn=False,
)
assert provider.get_operator_context_limit("claude-opus-4[1m]") == 1_000_000
assert provider.has_raw_operator_context_limit("claude-opus-4[1m]") is True
def test_preservation_no_message_mutation(monkeypatch):
"""The guard never mutates body['messages'], body['system'], or body['tools']."""
monkeypatch.setenv("HEADROOM_CONTEXT_LIMIT_MODE", "observe")
import headroom.tokenizers as _tk
BudgetHandler = _make_handler_subclass()
handler = BudgetHandler(operator_limit=262_144, token_count=300_000)
original_messages = [{"role": "user", "content": "hi there"}]
req = _build_request(
{"model": "step-router-v1", "messages": original_messages, "max_tokens": 8_192},
)
monkeypatch.setattr(_tk, "get_tokenizer", lambda m: _DummyTokenizer(300_000))
anyio.run(handler.handle_anthropic_messages, req)
# Body forwarded: upstream was called (observe mode, not rejected)
assert len(handler.upstream_calls) == 1
# The guard must not have mutated messages; the request reached upstream unchanged.
assert handler.upstream_bodies[-1]["messages"] == original_messages
assert "system" not in handler.upstream_bodies[-1]
assert "tools" not in handler.upstream_bodies[-1]
def test_preservation_system_and_tools_forward_unchanged(monkeypatch):
"""Observe mode forwards top-level system and tools without mutation."""
monkeypatch.setenv("HEADROOM_CONTEXT_LIMIT_MODE", "observe")
import headroom.tokenizers as _tk
original_body = {
"model": "step-router-v1",
"system": "system instructions",
"messages": [{"role": "user", "content": "hi"}],
"tools": [{"name": "tool", "input_schema": {"type": "object"}}],
"max_tokens": 8_192,
}
BudgetHandler = _make_handler_subclass()
handler = BudgetHandler(operator_limit=262_144, token_count=10)
req = _build_request(original_body)
monkeypatch.setattr(_tk, "get_tokenizer", lambda m: _DummyTokenizer(10))
anyio.run(handler.handle_anthropic_messages, req)
assert len(handler.upstream_calls) == 1
assert handler.upstream_bodies[-1] == original_body
assert handler.upstream_original_body_bytes[-1] == json.dumps(original_body).encode()
# --------------------------------------------------------------------------- #
# Negative space (negative_space) #
# --------------------------------------------------------------------------- #
def test_negative_space_at_threshold_forwards(monkeypatch):
"""A request exactly at threshold forwards unchanged in both modes."""
monkeypatch.setenv("HEADROOM_CONTEXT_LIMIT_MODE", "reject")
from headroom.proxy.context_budget_policy import evaluate
# declared=262144, max_out=8192 => threshold=253952; count=253952 (at threshold)
declared = 262_144
max_out = 8_192
threshold = declared - max_out
d = evaluate(
counted_tokens=threshold,
declared_limit=declared,
max_output_tokens=max_out,
mode="reject",
safety_margin=0,
)
assert d.reason == "under_threshold"
assert d.should_reject is False
def test_negative_space_observe_over_threshold_still_forwards(monkeypatch):
"""In observe mode an over-threshold request still reaches upstream."""
monkeypatch.setenv("HEADROOM_CONTEXT_LIMIT_MODE", "observe")
monkeypatch.setenv("HEADROOM_CONTEXT_LIMIT_SAFETY_MARGIN", "0")
import headroom.tokenizers as _tk
BudgetHandler = _make_handler_subclass()
handler = BudgetHandler(operator_limit=262_144, token_count=300_000)
req = _build_request(
{
"model": "step-router-v1",
"messages": [{"role": "user", "content": "big payload"}],
"max_tokens": 8_192,
},
)
monkeypatch.setattr(_tk, "get_tokenizer", lambda m: _DummyTokenizer(300_000))
anyio.run(handler.handle_anthropic_messages, req)
assert len(handler.upstream_calls) == 1
def test_negative_space_bypassed_over_threshold_in_reject_mode_forwards(monkeypatch):
"""A bypassed over-threshold request in reject mode still reaches upstream."""
monkeypatch.setenv("HEADROOM_CONTEXT_LIMIT_MODE", "reject")
import headroom.tokenizers as _tk
BudgetHandler = _make_handler_subclass()
handler = BudgetHandler(operator_limit=262_144, token_count=300_000)
req = _build_request(
{
"model": "step-router-v1",
"messages": [{"role": "user", "content": "hi"}],
"max_tokens": 8_192,
},
{"x-headroom-bypass": "true"},
)
monkeypatch.setattr(_tk, "get_tokenizer", lambda m: _DummyTokenizer(300_000))
anyio.run(handler.handle_anthropic_messages, req)
assert len(handler.upstream_calls) == 1
def test_variant_context1m_with_raw_declaration_still_enforces(monkeypatch):
"""A sticky context-1m beta must not disarm a model the operator declared by raw id.
`anthropic-beta` is session-sticky (`get_session_beta_tracker`), so a later
request can carry `context-1m` it never sent. The degrade-to-observe branch
keys on whether the operator declared the raw id, so a declared model keeps
enforcing even when the beta arrives from the session baseline.
"""
monkeypatch.setenv("HEADROOM_CONTEXT_LIMIT_MODE", "reject")
monkeypatch.setenv("HEADROOM_CONTEXT_LIMIT_SAFETY_MARGIN", "0")
import headroom.tokenizers as _tk
BudgetHandler = _make_handler_subclass()
handler = BudgetHandler(operator_limit=262_144, token_count=300_000)
req = _build_request(
{
"model": "step-router-v1",
"messages": [{"role": "user", "content": "hi"}],
"max_tokens": 8_192,
},
{"anthropic-beta": "context-1m"},
)
monkeypatch.setattr(_tk, "get_tokenizer", lambda m: _DummyTokenizer(300_000))
resp = anyio.run(handler.handle_anthropic_messages, req)
assert resp.status_code == 400
assert len(handler.upstream_calls) == 0
def test_variant_context1m_suffixed_declaration_still_enforces(monkeypatch):
"""A declaration keyed by the [1m] model id survives handler sanitization."""
monkeypatch.setenv("HEADROOM_CONTEXT_LIMIT_MODE", "reject")
monkeypatch.setenv("HEADROOM_CONTEXT_LIMIT_SAFETY_MARGIN", "0")
import headroom.tokenizers as _tk
BudgetHandler = _make_handler_subclass()
handler = BudgetHandler(operator_limit=None, token_count=300_000)
handler.anthropic_provider._operator_context_limits["claude-opus-4[1m]"] = 262_144
req = _build_request(
{
"model": "claude-opus-4[1m]",
"messages": [{"role": "user", "content": "hi"}],
"max_tokens": 8_192,
},
{"anthropic-beta": "context-1m"},
)
monkeypatch.setattr(_tk, "get_tokenizer", lambda m: _DummyTokenizer(300_000))
response = anyio.run(handler.handle_anthropic_messages, req)
assert response.status_code == 400
assert len(handler.upstream_calls) == 0
def test_variant_finalized_body_counts_system_and_tools(monkeypatch):
"""Reject mode counts top-level system and tool input after shaping."""
monkeypatch.setenv("HEADROOM_CONTEXT_LIMIT_MODE", "reject")
monkeypatch.setenv("HEADROOM_CONTEXT_LIMIT_SAFETY_MARGIN", "0")
import headroom.tokenizers as _tk
BudgetHandler = _make_handler_subclass()
handler = BudgetHandler(operator_limit=262_144, token_count=220_000)
req = _build_request(
{
"model": "step-router-v1",
"system": "system instructions",
"messages": [{"role": "user", "content": "hi"}],
"tools": [{"name": "large-tool-schema", "input_schema": {"type": "object"}}],
"max_tokens": 8_192,
}
)
monkeypatch.setattr(_tk, "get_tokenizer", lambda m: _FinalizedBodyTokenizer(220_000))
response = anyio.run(handler.handle_anthropic_messages, req)
assert response.status_code == 400
assert len(handler.upstream_calls) == 0
def test_variant_output_shaper_mutation_is_counted_after_recount(monkeypatch):
"""The guard sees a system mutation made after the metrics recount."""
monkeypatch.setenv("HEADROOM_CONTEXT_LIMIT_MODE", "reject")
monkeypatch.setenv("HEADROOM_CONTEXT_LIMIT_SAFETY_MARGIN", "0")
monkeypatch.setenv("HEADROOM_OUTPUT_SHAPER", "1")
import headroom.proxy.output_savings as _savings
import headroom.proxy.output_shaper as _shaper
import headroom.tokenizers as _tk
monkeypatch.setattr(_savings, "assign_arm", lambda *args: "treatment")
monkeypatch.setattr(_shaper, "resolve_verbosity_level", lambda settings: (2, "test"))
def _mutate_body(body, settings, *, level_override):
body["system"] = "new system content"
return SimpleNamespace(changed=True, labels=["test-shaper"])
monkeypatch.setattr(_shaper, "shape_request", _mutate_body)
BudgetHandler = _make_handler_subclass()
handler = BudgetHandler(operator_limit=262_144, token_count=220_000)
req = _build_request(
{
"model": "step-router-v1",
"messages": [{"role": "user", "content": "hi"}],
"max_tokens": 8_192,
}
)
monkeypatch.setattr(_tk, "get_tokenizer", lambda m: _FinalizedBodyTokenizer(220_000))
response = anyio.run(handler.handle_anthropic_messages, req)
assert response.status_code == 400
assert len(handler.upstream_calls) == 0
# --------------------------------------------------------------------------- #
# Reproduction (reproduction) #
# --------------------------------------------------------------------------- #
def test_reproduction_over_limit_rejected_locally_with_zero_upstream_calls(monkeypatch):
"""head: over-threshold request in reject mode returns 400 with zero upstream calls."""
monkeypatch.setenv("HEADROOM_CONTEXT_LIMIT_MODE", "reject")
monkeypatch.setenv("HEADROOM_CONTEXT_LIMIT_SAFETY_MARGIN", "0")
import headroom.tokenizers as _tk
BudgetHandler = _make_handler_subclass()
handler = BudgetHandler(operator_limit=262_144, token_count=270_000)
req = _build_request(
{
"model": "step-router-v1",
"messages": [{"role": "user", "content": "a" * 1000}],
"max_tokens": 8_192,
},
)
monkeypatch.setattr(_tk, "get_tokenizer", lambda m: _DummyTokenizer(270_000))
resp = anyio.run(handler.handle_anthropic_messages, req)
assert resp.status_code == 400, f"Expected 400, got {resp.status_code}"
body = json.loads(resp.body)
assert body["type"] == "error"
assert body["error"]["type"] == "invalid_request_error"
assert "step-router-v1" in body["error"]["message"]
# Zero upstream calls
assert len(handler.upstream_calls) == 0
# --------------------------------------------------------------------------- #
# Production route (production_route) #
# --------------------------------------------------------------------------- #
def test_production_route_guard_fires_on_create_app(monkeypatch):
"""Guard fires on a real POST /v1/messages through create_app.
The upstream transport is stubbed so no live API call is made.
Falsifiability: removing the guard by patching get_operator_context_limit
to return None makes the request reach the (stubbed) upstream instead.
"""
import json as _json
import headroom.tokenizers as _tk
from headroom.proxy.models import ProxyConfig
from headroom.proxy.server import create_app
monkeypatch.setenv("HEADROOM_CONTEXT_LIMIT_MODE", "reject")
monkeypatch.setenv("HEADROOM_CONTEXT_LIMIT_SAFETY_MARGIN", "0")
config = ProxyConfig(
optimize=False,
cache_enabled=False,
rate_limit_enabled=False,
cost_tracking_enabled=False,
memory_enabled=False,
)
app = create_app(config)
proxy = app.state.proxy
# Patch the tokenizer to return a token count above the declared limit.
monkeypatch.setattr(_tk, "get_tokenizer", lambda m: _DummyTokenizer(270_000))
# Declare a limit on the provider (step-router-v1 at 262144).
# The guard uses get_operator_context_limit; patch it to return 262144.
original_get_op = proxy.anthropic_provider.get_operator_context_limit
proxy.anthropic_provider._operator_context_limits = {"step-router-v1": 262_144}
proxy.anthropic_provider.get_operator_context_limit = lambda m: (
proxy.anthropic_provider._operator_context_limits.get(m)
)
# Stub upstream so it records calls instead of hitting the network.
upstream_calls: list[dict] = []
async def _stub_retry(self_inner, method, url, headers, body, **kwargs):
upstream_calls.append({"method": method, "url": url})
return _stub_response()
import headroom.proxy.server as _srv
monkeypatch.setattr(_srv.HeadroomProxy, "_retry_request", _stub_retry)
req_body = _json.dumps(
{
"model": "step-router-v1",
"messages": [{"role": "user", "content": "over limit payload"}],
"max_tokens": 8_192,
}
)
with TestClient(app) as client:
# --- HEAD: guard fires, returns 400, zero upstream calls ---
resp = client.post(
"/v1/messages",
content=req_body.encode(),
headers={
"authorization": "Bearer sk-ant-api-test",
"content-type": "application/json",
},
)
assert resp.status_code == 400, f"Expected 400, got {resp.status_code}: {resp.text}"
pre_calls = len(upstream_calls)
assert pre_calls == 0, f"Guard should have prevented upstream call; got {pre_calls}"
# --- MUTATION CHECK: removing guard (no declared limit) forwards request ---
proxy.anthropic_provider.get_operator_context_limit = lambda m: None
proxy.anthropic_provider._operator_context_limits = {}
upstream_calls.clear()
resp2 = client.post(
"/v1/messages",
content=req_body.encode(),
headers={
"authorization": "Bearer sk-ant-api-test",
"content-type": "application/json",
},
)
# Without a declared limit the guard is inert; upstream should be called.
assert len(upstream_calls) >= 1, (
"Mutation check failed: removing declared limit should allow upstream call"
)
assert resp2.status_code != 400 or len(upstream_calls) >= 1
# Restore for cleanup
proxy.anthropic_provider.get_operator_context_limit = original_get_op