fix(e2e): bind provider-cache recordings to the deployment's test, not the serving process

The cache edge keyed every recording on its own process's PYTEST_CURRENT_TEST.
Under xdist that names whatever test the serving worker is in, which is
unrelated to the caller: the proxy is a separate pod, and the Claude Code compat
matrix registered its shared aliases from every worker, each pointing at that
worker's edge, so the router spread one worker's calls across all eight edges.
Builds 234 and 235 of litellm-e2e, same commit, credited the same Bedrock
request to unrelated tests 92% of the time, and Bedrock never converged past a
~20% hit rate while OpenAI, whose deployments are per test, sat at 90%.

A deployment registered from inside a test now carries its test's slug in the
edge URL it is pointed at, `{edge}/{mount}/t/{slug}`, and the edge reads that
segment off every request before forwarding. A request without one is forwarded
live and never cached, and the edge no longer falls back to process state. The
compat aliases are registered with provider_live=True and stay on their real
provider path: no single test owns them, and the matrix exists to prove the real
CLI against real providers.
This commit is contained in:
Yuneng Jiang 2026-09-16 17:08:17 -07:00
parent 765e6e498d
commit c63d0e6922
No known key found for this signature in database
11 changed files with 228 additions and 62 deletions

View file

@ -26,6 +26,7 @@ from e2e_http import NetworkError, PreparedForward, RawResponse, StreamChunk, St
from models import LiteLLMParamsBody, ModelMode
from botocore.credentials import Credentials
from botocore.eventstream import EventStreamBuffer
from fixture_bundle import slug_for_test
from provider_cache import (
SIGNATURE_HEADERS,
CacheEdge,
@ -35,7 +36,9 @@ from provider_cache import (
ResponseStore,
cacheable_endpoint,
request_identity,
scoped_edge_base,
slotted_key,
split_test_segment,
successful_response,
)
from provider_cache_redis import PUBLISH, RedisCommands, RedisResponseStore, configured_cache, redis_store
@ -47,7 +50,13 @@ from provider_cache_routing import (
route_cache_model,
)
from fixture_mode import SESSION_TEST_KEY
from provider_edge import EDGE_MOUNTS, configured_cache_backend, resolve_mount, start_provider_edge
from provider_edge import (
EDGE_MOUNTS,
configured_cache_backend,
provider_edge_api_base,
resolve_mount,
start_provider_edge,
)
from provider_edge_bedrock import bedrock_signer
from redis.exceptions import ConnectionError as RedisConnectionError
@ -57,6 +66,7 @@ SUCCESS: Final = b'{"id":"provider-fixed-id","choices":[{"message":{"content":"h
HEADERS: Final = {"content-type": "application/json", "authorization": "Bearer synthetic-account-one"}
TEST_KEY: Final = "tests/e2e/synthetic_suite.py::TestCase::test_case"
OTHER_TEST_KEY: Final = "tests/e2e/synthetic_suite.py::TestCase::test_other_case"
TEST_SLUG: Final = slug_for_test(TEST_KEY)
def marked(marker: str) -> bytes:
@ -173,11 +183,11 @@ def store(redis_url: str) -> RedisResponseStore:
return redis_store(redis_url, "test-" + uuid.uuid4().hex)
def cache_edge(store: ResponseStore, test_key: str = TEST_KEY) -> CacheEdge:
def cache_edge(store: ResponseStore) -> CacheEdge:
"""A cache edge standing in for one pytest process. A fresh instance over the
same store is the next build running the same test: the recordings survive,
the per-test FIFO slot counters start over."""
return CacheEdge(store, SECRET, test_key=lambda: test_key)
return CacheEdge(store, SECRET)
def slot_key(
@ -186,12 +196,13 @@ def slot_key(
) -> str:
prepared: Final = prepare_forward("POST", url, headers, body)
assert isinstance(prepared, PreparedForward)
return slotted_key(SECRET, request_identity(SECRET, test_key, "POST", url, prepared.headers, body), slot)
identity: Final = request_identity(SECRET, slug_for_test(test_key), "POST", url, prepared.headers, body)
return slotted_key(SECRET, identity, slot)
def bedrock_cache_edge(store: ResponseStore, test_key: str = TEST_KEY) -> CacheEdge:
def bedrock_cache_edge(store: ResponseStore) -> CacheEdge:
return CacheEdge(
store, SECRET, test_key=lambda: test_key,
store, SECRET,
policies={BEDROCK_MOUNT: MountPolicy(
sign=bedrock_signer("us-east-1", lambda: STATIC_CREDENTIALS), unkeyed_headers=SIGNATURE_HEADERS,
)},
@ -199,11 +210,14 @@ def bedrock_cache_edge(store: ResponseStore, test_key: str = TEST_KEY) -> CacheE
@contextmanager
def edge(cache: CacheEdge, provider: Provider) -> Generator[str, None, None]:
def edge(cache: CacheEdge, provider: Provider, test_key: str | None = TEST_KEY) -> Generator[str, None, None]:
"""The URL a deployment registered by ``test_key`` would carry, or the bare
mount URL for None, which is what a registration made outside any test gets."""
upstream: Final = f"http://127.0.0.1:{provider.server_port}"
running: Final = start_provider_edge(cache, mounts={"openai": upstream})
base: Final = running.edge.api_base("openai")
try:
yield running.edge.api_base("openai") + "/v1/chat/completions"
yield f"{base if test_key is None else scoped_edge_base(base, test_key)}/v1/chat/completions"
finally:
running.shutdown()
@ -213,7 +227,7 @@ def bedrock_edge(cache: CacheEdge, provider: Provider, action: str = "converse")
upstream: Final = f"http://127.0.0.1:{provider.server_port}"
running: Final = start_provider_edge(cache, mounts={BEDROCK_MOUNT: upstream})
try:
yield f"{running.edge.api_base(BEDROCK_MOUNT)}/model/{BEDROCK_MODEL}/{action}"
yield f"{scoped_edge_base(running.edge.api_base(BEDROCK_MOUNT), TEST_KEY)}/model/{BEDROCK_MODEL}/{action}"
finally:
running.shutdown()
@ -288,7 +302,7 @@ def test_expiry_does_not_slide(store: RedisResponseStore, provider: Provider) ->
url: Final = f"http://127.0.0.1:{provider.server_port}/v1/chat/completions"
def drain() -> None:
head = cache_edge(short).forward("openai", "POST", url, dict(HEADERS), BODY, 5)
head = cache_edge(short).forward("openai", "POST", url, dict(HEADERS), BODY, 5, test_key=TEST_SLUG)
assert isinstance(head, StreamHead)
assert b"".join(step.data for step in head.steps if isinstance(step, StreamChunk)) == SUCCESS
@ -312,7 +326,7 @@ def test_concurrent_builds_publish_one_recording_atomically(
edges: Final = tuple(cache_edge(store) for _ in range(5))
def drain(cache: CacheEdge) -> bytes:
head = cache.forward("openai", "POST", url, dict(HEADERS), BODY, 5)
head = cache.forward("openai", "POST", url, dict(HEADERS), BODY, 5, test_key=TEST_SLUG)
assert isinstance(head, StreamHead)
return b"".join(step.data for step in head.steps if isinstance(step, StreamChunk))
@ -401,7 +415,7 @@ def test_corrupt_entry_is_replaced_by_same_successful_request(store: RedisRespon
assert store.publish(key, lease, payload)
caches: Final = tuple(cache_edge(store) for _ in range(2))
for cache in caches:
head = cache.forward("openai", "POST", upstream, dict(HEADERS), BODY, 5)
head = cache.forward("openai", "POST", upstream, dict(HEADERS), BODY, 5, test_key=TEST_SLUG)
assert isinstance(head, StreamHead)
assert b"".join(step.data for step in head.steps if isinstance(step, StreamChunk)) == SUCCESS
assert len(provider.hits) == 1
@ -471,28 +485,101 @@ def test_another_test_never_reuses_this_tests_recording(
with edge(cache_edge(store), provider) as url:
call(url)
assert len(provider.hits) == 1
with edge(cache_edge(store, OTHER_TEST_KEY), provider) as url:
with edge(cache_edge(store), provider, OTHER_TEST_KEY) as url:
call(url)
assert len(provider.hits) == 2
with edge(cache_edge(store, OTHER_TEST_KEY), provider) as url:
with edge(cache_edge(store), provider, OTHER_TEST_KEY) as url:
call(url)
assert len(provider.hits) == 2
def test_calls_outside_any_test_are_never_cached(
store: RedisResponseStore, provider: Provider,
def test_a_request_without_a_test_segment_is_never_cached(
store: RedisResponseStore, provider: Provider, monkeypatch: pytest.MonkeyPatch,
) -> None:
url: Final = f"http://127.0.0.1:{provider.server_port}/v1/chat/completions"
cache: Final = CacheEdge(store, SECRET, test_key=lambda: SESSION_TEST_KEY)
for _ in range(2):
head = cache.forward("openai", "POST", url, dict(HEADERS), BODY, 5)
assert isinstance(head, StreamHead)
assert b"".join(step.data for step in head.steps if isinstance(step, StreamChunk)) == SUCCESS
"""The bare mount URL is what a deployment registered outside any test would
carry. The serving process is inside a test here, and that must not count:
the edge never names the test from its own process state."""
monkeypatch.setenv("PYTEST_CURRENT_TEST", f"{TEST_KEY} (call)")
cache: Final = cache_edge(store)
with edge(cache, provider, test_key=None) as url:
assert call(url).body == SUCCESS
assert call(url).body == SUCCESS
assert len(provider.hits) == 2
assert dict(cache.counters.counts) == {
"bypass": 2, "mount:openai:bypass": 2,
"upstream_attempts": 2, "mount:openai:upstream_attempts": 2,
}
with edge(cache_edge(store), provider) as url:
assert call(url).body == SUCCESS
assert len(provider.hits) == 3
def test_attribution_comes_from_the_deployment_path_not_the_serving_process(
store: RedisResponseStore, provider: Provider, monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Under xdist the process serving a call is unrelated to the test that made
it: the proxy is a separate pod, and the compat matrix's shared aliases had
every worker's edge answering every other worker's cells. The recording must
land under the test whose deployment the request came through, whatever
``PYTEST_CURRENT_TEST`` says in the edge's own process."""
monkeypatch.setenv("PYTEST_CURRENT_TEST", f"{OTHER_TEST_KEY} (call)")
monkeypatch.setenv("E2E_PROVIDER_CACHE_METRICS_DIR", "unused-but-enables-the-probe")
first: Final = cache_edge(store)
with edge(first, provider) as url:
assert call(url).body == SUCCESS
assert len(provider.hits) == 1
assert dict(first.probe.rows[0])["test_key"] == TEST_SLUG
monkeypatch.setenv("PYTEST_CURRENT_TEST", f"{TEST_KEY} (call)")
with edge(cache_edge(store), provider, OTHER_TEST_KEY) as url:
assert call(url).body == SUCCESS
assert len(provider.hits) == 2
with edge(cache_edge(store), provider) as url:
assert call(url).body == SUCCESS
assert len(provider.hits) == 2
@pytest.mark.parametrize("upstream_path,expected", [
(f"t/{TEST_SLUG}/v1/chat/completions", (TEST_SLUG, "v1/chat/completions")),
(f"t/{TEST_SLUG}/model/{BEDROCK_MODEL}/converse-stream", (TEST_SLUG, f"model/{BEDROCK_MODEL}/converse-stream")),
("v1/chat/completions", (None, "v1/chat/completions")),
(f"model/{BEDROCK_MODEL}/invoke", (None, f"model/{BEDROCK_MODEL}/invoke")),
("t//v1/chat/completions", (None, "v1/chat/completions")),
("t", (None, "")),
])
def test_the_test_segment_is_read_off_the_path_and_never_reaches_the_provider(
upstream_path: str, expected: tuple[str | None, str],
) -> None:
assert split_test_segment(upstream_path) == expected
assert split_test_segment(scoped_edge_base("", TEST_KEY).lstrip("/") + "/v1/chat/completions") == (
TEST_SLUG, "v1/chat/completions",
)
def test_the_cache_edge_base_is_scoped_to_the_registering_test(
redis_url: str, monkeypatch: pytest.MonkeyPatch, tmp_path,
) -> None:
monkeypatch.setenv("E2E_PROVIDER_CACHE", "1")
monkeypatch.setenv("E2E_PROVIDER_CACHE_REDIS_URL", redis_url)
monkeypatch.setenv("E2E_PROVIDER_CACHE_HMAC_KEY", SECRET.decode())
monkeypatch.setenv("E2E_PROVIDER_CACHE_NAMESPACE", "environment-" + uuid.uuid4().hex)
configured_cache.cache_clear()
def base_for(test_key: str) -> str | None:
return provider_edge_api_base(
"openai", mode_raw="live", bundle_dir=tmp_path, bind_host="127.0.0.1", advertise_host="127.0.0.1",
test_key=test_key,
)
try:
scoped: Final = base_for(TEST_KEY)
assert scoped is not None and scoped.endswith(f"/openai/t/{TEST_SLUG}")
assert base_for(OTHER_TEST_KEY) != scoped
assert base_for(SESSION_TEST_KEY) is None
monkeypatch.setenv("E2E_PROVIDER_CACHE", "0")
configured_cache.cache_clear()
assert base_for(TEST_KEY) is None
finally:
configured_cache.cache_clear()
def test_counters_attribute_every_outcome_to_its_mount(
@ -505,8 +592,8 @@ def test_counters_attribute_every_outcome_to_its_mount(
cache: Final = cache_edge(store)
running: Final = start_provider_edge(cache, mounts={"openai": upstream, "anthropic": upstream})
try:
call(running.edge.api_base("openai") + "/v1/chat/completions")
call(running.edge.api_base("anthropic") + "/v1/messages")
call(scoped_edge_base(running.edge.api_base("openai"), TEST_KEY) + "/v1/chat/completions")
call(scoped_edge_base(running.edge.api_base("anthropic"), TEST_KEY) + "/v1/messages")
finally:
running.shutdown()
counts: Final = dict(cache.counters.counts)
@ -531,7 +618,7 @@ def test_a_rejection_says_whether_the_body_was_cut_short_or_simply_unfinished(
provider.response = b'data: {"choices":[{"index":0,"delta":{"content":"hi"},"finish_reason":"stop"}]}\n\ndata: [DONE]\n\n'
running: Final = start_provider_edge(cut_short, mounts={"openai": upstream})
try:
forward("POST", running.edge.api_base("openai") + "/v1/chat/completions",
forward("POST", scoped_edge_base(running.edge.api_base("openai"), TEST_KEY) + "/v1/chat/completions",
headers=HEADERS, body=MARKED, timeout=5)
finally:
running.shutdown()
@ -542,7 +629,7 @@ def test_a_rejection_says_whether_the_body_was_cut_short_or_simply_unfinished(
provider.response = b'{"choices":[{"index":0,"message":{"content":"hi"}}]}'
second: Final = start_provider_edge(unfinished, mounts={"openai": upstream})
try:
call(second.edge.api_base("openai") + "/v1/chat/completions", MARKED)
call(scoped_edge_base(second.edge.api_base("openai"), TEST_KEY) + "/v1/chat/completions", MARKED)
finally:
second.shutdown()
@ -551,7 +638,7 @@ def test_a_rejection_says_whether_the_body_was_cut_short_or_simply_unfinished(
provider.response = b'{"message":"Too many requests"}'
third: Final = start_provider_edge(refused, mounts={"openai": upstream})
try:
call(third.edge.api_base("openai") + "/v1/chat/completions", MARKED)
call(scoped_edge_base(third.edge.api_base("openai"), TEST_KEY) + "/v1/chat/completions", MARKED)
finally:
third.shutdown()
@ -586,7 +673,7 @@ def openai_edge(cache: CacheEdge, provider: Provider, path: str) -> Generator[st
upstream: Final = f"http://127.0.0.1:{provider.server_port}"
running: Final = start_provider_edge(cache, mounts={"openai": upstream})
try:
yield running.edge.api_base("openai") + path
yield scoped_edge_base(running.edge.api_base("openai"), TEST_KEY) + path
finally:
running.shutdown()
@ -787,7 +874,7 @@ class TestBedrockSigning:
def signing_edge() -> CacheEdge:
return CacheEdge(
store, SECRET, test_key=lambda: TEST_KEY,
store, SECRET,
policies={BEDROCK_MOUNT: MountPolicy(sign=varying, unkeyed_headers=SIGNATURE_HEADERS)},
)
@ -1065,7 +1152,8 @@ def test_connection_failure_releases_capture_lease(store: RedisResponseStore) ->
unavailable.bind(("127.0.0.1", 0))
url: Final = f"http://127.0.0.1:{unavailable.getsockname()[1]}/v1/chat/completions"
cache: Final = cache_edge(store)
assert isinstance(cache.forward("openai", "POST", url, dict(HEADERS), BODY, 0.2), NetworkError)
head: Final = cache.forward("openai", "POST", url, dict(HEADERS), BODY, 0.2, test_key=TEST_SLUG)
assert isinstance(head, NetworkError)
key: Final = slot_key(url)
lease: Final = store.lookup(key)
assert isinstance(lease, CaptureLease)
@ -1076,7 +1164,7 @@ def test_connection_failure_releases_capture_lease(store: RedisResponseStore) ->
def test_close_before_first_chunk_releases_lease(store: RedisResponseStore, provider: Provider) -> None:
url: Final = f"http://127.0.0.1:{provider.server_port}/v1/chat/completions"
cache: Final = cache_edge(store)
head: Final = cache.forward("openai", "POST", url, dict(HEADERS), BODY, 5)
head: Final = cache.forward("openai", "POST", url, dict(HEADERS), BODY, 5, test_key=TEST_SLUG)
assert isinstance(head, StreamHead)
head.steps.close()
key: Final = slot_key(url)
@ -1094,7 +1182,7 @@ def test_effective_account_change_cannot_reuse_cache(
netrc = tmp_path / account
netrc.write_text(f"machine 127.0.0.1 login {account} password synthetic\n")
monkeypatch.setenv("NETRC", str(netrc))
head = cache.forward("openai", "POST", url, dict(HEADERS), BODY, 5)
head = cache.forward("openai", "POST", url, dict(HEADERS), BODY, 5, test_key=TEST_SLUG)
assert isinstance(head, StreamHead)
assert b"".join(step.data for step in head.steps if isinstance(step, StreamChunk)) == SUCCESS
assert len(provider.hits) == 2

View file

@ -1,6 +1,6 @@
# Shared provider-response cache
`E2E_PROVIDER_CACHE=1` enables automatic response reuse in the live E2E mode. Standard OpenAI and Anthropic model registrations use the provider edge, as do Anthropic-on-Bedrock registrations that carry no AWS identity of their own. Existing custom API bases, named credentials, mocked models and realtime WebSocket deployments keep their existing routing. Other provider protocols remain live
`E2E_PROVIDER_CACHE=1` enables automatic response reuse in the live E2E mode. Standard OpenAI and Anthropic model registrations made from inside a test use the provider edge, as do Anthropic-on-Bedrock registrations that carry no AWS identity of their own. A registration made outside any test, or with `provider_live=True`, keeps its real provider path. Existing custom API bases, named credentials, mocked models and realtime WebSocket deployments keep their existing routing. Other provider protocols remain live
The edge caches complete successful POST responses for `/v1/chat/completions`, `/v1/messages`, `/v1/embeddings` and `/v1/responses` on the OpenAI and Anthropic mounts, SSE streams included, and for `/model/{id}/converse` and `/model/{id}/invoke` on a Bedrock mount. Unsupported endpoints pass through. Each endpoint family has its own completeness rule, so a truncated embedding or a Responses run that never reached `response.completed` is not stored
@ -10,13 +10,13 @@ Two details of that rule are worth knowing before changing it. A ConverseStream
## Request identity
A recording belongs to one test. The key is a keyed digest over the test's node id, the method, the URL, the effective outbound headers (including authentication and HTTP-library defaults), body presence and the body bytes, with one normalization: a 12-hex-digit run, the shape `unique_marker()` mints, is replaced by a placeholder in both the URL and a UTF-8 body. Nothing else is normalized away. No prompts, JSON values or credentials are rewritten, and the rule is the one `fixture_canonical.py` already applies for record/replay, so there is a single definition of what a marker is
A recording belongs to one test, and the test is named by the deployment rather than by the process. A deployment registered from inside a test gets the cache edge's mount URL with a test segment appended, `{edge}/{mount}/t/{slug}`, where the slug is `slug_for_test` of the registering test's node id, and the edge reads that segment off every request before forwarding. The key is a keyed digest over that slug, the method, the upstream URL, the effective outbound headers (including authentication and HTTP-library defaults), body presence and the body bytes, with one normalization: a 12-hex-digit run, the shape `unique_marker()` mints, is replaced by a placeholder in both the URL and a UTF-8 body. Nothing else is normalized away. No prompts, JSON values or credentials are rewritten, and the rule is the one `fixture_canonical.py` already applies for record/replay, so there is a single definition of what a marker is
Requests that differ only by their markers therefore share a canonical identity, which is what makes the cache reusable across builds: every e2e test salts its prompt afresh, so an exact-byte key would miss on every call. Within one test, calls that share a canonical identity are still recorded and replayed separately, by a FIFO slot index appended to the key. That matters because a replayed response carries the recorded provider response id, `LiteLLM_SpendLogs.request_id` is that id, and one shared recording answering two calls would collapse two spend rows into one
Two different tests never share a recording, and a provider call made outside any test (fixtures, session setup) is never cached, because the identity has no test node id to bind to
Two different tests never share a recording. A request that reaches the edge without a test segment is forwarded live and never cached, and the edge never names the test from its own process's `PYTEST_CURRENT_TEST`. It used to, and that was wrong whenever the calling test and the serving process differed: the proxy is a separate pod, and under xdist the Claude Code compat matrix registered its shared aliases from every worker, each pointing at that worker's edge, so the router spread one worker's calls across all of them and each call was keyed on whatever test the serving worker was in. Builds 234 and 235 of the e2e pipeline, same commit, credited the same Bedrock request to unrelated tests 92% of the time, which is why that mount never converged
A client that varies its own request between runs defeats that identity without breaking any rule, and the Claude Code compat cells did. The CLI sends a device id and a session id in `metadata.user_id`, and its system prompt names both its memory directory and its working directory, adding the branch and recent commits when that directory is a git repository. Driven with a fresh HOME and the checkout as its working directory, every cell sent different bytes every build. The fix belongs in the driver rather than here: `claude_code/cli_driver.py` pins the config directory, the working directory and both identifiers, which is why the cache needs no rule for any of it. Normalizing them instead would have hidden a real defect class, since a rule cannot tell a client's own churn from a value a test means to assert on
The Claude Code compat cells are not cached. Their aliases are registered once per worker session and shared by every cell, so no call to them belongs to one test, and the matrix exists to prove the real CLI against real providers; `claude_code/conftest.py` registers them with `provider_live=True`. The driver still pins the CLI's config directory, working directory, device id and session id (`_driver_unit_tests/test_request_determinism.py` holds that), so a CLI-driven deployment registered by one test would send stable bytes. Normalizing those values in the key instead would hide a real defect class, since a rule cannot tell a client's own churn from a value a test means to assert on
Provider `Set-Cookie` headers are dropped before validation and never recorded: the edge already withholds them from the proxy, and OpenAI responses always carry Cloudflare bot-management cookies
@ -62,4 +62,4 @@ Provider remaining-quota headers describe the captured response. Metrics derived
## Qualification
`tests/code_coverage_tests/test_provider_cache.py` exercises local HTTP providers and disposable real Redis, including the marker-canonical key, the FIFO slot index, per-test isolation, SigV4 re-signing against a local upstream, and each endpoint's completeness rule. CI runs these checks with the existing provider-edge and replay harness tests. These component checks do not establish Buildkite deployment, full-suite cross-build reuse or a genuine 24-hour expiry observation; those require separate runtime evidence
`tests/code_coverage_tests/test_provider_cache.py` exercises local HTTP providers and disposable real Redis, including the marker-canonical key, the FIFO slot index, per-test isolation, attribution from the deployment's test segment whatever the serving process is running, SigV4 re-signing against a local upstream, and each endpoint's completeness rule. CI runs these checks with the existing provider-edge and replay harness tests. These component checks do not establish Buildkite deployment, full-suite cross-build reuse or a genuine 24-hour expiry observation; those require separate runtime evidence

View file

@ -600,10 +600,13 @@ def _build_control_plane_client(proxy_config: ProxyConfig):
def _register_deployment(proxy, deployment: CompatDeployment) -> str:
"""Register one deployment and return its proxy-assigned model_id
once it is servable on the data plane."""
once it is servable on the data plane. The aliases are shared by every
cell and, under xdist, by every worker, so no call to them belongs to
one test and none is cached: the matrix exists to reach real providers."""
return proxy.create_model(
deployment.model_name,
deployment.litellm_params,
provider_live=True,
)

View file

@ -124,8 +124,7 @@ def pytest_configure(config: pytest.Config) -> None:
)
config.addinivalue_line(
"markers",
"cli_determinism: drives the real claude CLI for several seconds, which widens the window in which "
"another test's in-flight upstream call is attributed to it; deselected unless E2E_CLI_DETERMINISM is set",
"cli_determinism: drives the real claude CLI for several seconds; deselected unless E2E_CLI_DETERMINISM is set",
)
config.addinivalue_line(
"markers",

View file

@ -13,7 +13,7 @@ from pathlib import Path
from typing import Final
from dotenv import load_dotenv
from fixture_mode import deterministic_marker, parse_fixture_mode
from fixture_mode import current_test_key, deterministic_marker, parse_fixture_mode
from provider_edge import provider_edge_api_base
# Local runs keep provider / DataDog keys in tests/e2e/.env (see CONTRIBUTING.md).
@ -200,13 +200,15 @@ def datadog_mcp_url(*, toolsets: str = "core") -> str:
def provider_edge_base(mount: str) -> str | None:
"""The api_base an edge-wired deployment should register with, using this
process's fixture-mode and edge-host configuration: None in live mode, the
shared edge server's mount URL in record and replay."""
shared edge server's mount URL in record and replay, and with the shared
cache on, the cache edge's mount URL scoped to the running test."""
return provider_edge_api_base(
mount,
mode_raw=FIXTURE_MODE_RAW,
bundle_dir=FIXTURE_DIR,
bind_host=PROVIDER_EDGE_BIND_HOST,
advertise_host=PROVIDER_EDGE_ADVERTISE_HOST,
test_key=current_test_key(),
forward_timeout=REQUEST_TIMEOUT,
)

View file

@ -27,8 +27,8 @@ from e2e_http import (
prepare_forward,
primed_steps,
)
from fixture_bundle import slug_for_test
from fixture_canonical import MARKER_PATTERN, MARKER_PLACEHOLDER
from fixture_mode import SESSION_TEST_KEY, current_test_key
from pydantic import BaseModel, ConfigDict, JsonValue, TypeAdapter, ValidationError
LIFETIME_SECONDS: Final = 86_400
@ -58,6 +58,19 @@ EVENT_TYPE_HEADER: Final = ":event-type"
EVENTSTREAM_HEADERS: Final[TypeAdapter[dict[str, str]]] = TypeAdapter(dict[str, str])
OPENAI_JSON_PATHS: Final = frozenset({"/v1/chat/completions", "/v1/messages", "/v1/embeddings", "/v1/responses"})
JSON_VALUE: Final[TypeAdapter[JsonValue]] = TypeAdapter(JsonValue)
TEST_SEGMENT: Final = "t"
def scoped_edge_base(base: str, test_key: str) -> str:
return f"{base}/{TEST_SEGMENT}/{slug_for_test(test_key)}"
def split_test_segment(upstream_path: str) -> tuple[str | None, str]:
head, _, rest = upstream_path.partition("/")
if head != TEST_SEGMENT:
return None, upstream_path
slug, _, remainder = rest.partition("/")
return slug or None, remainder
@dataclass(frozen=True, slots=True)
@ -517,7 +530,6 @@ class CacheEdge:
wait_seconds: float = 2.0
clock: Callable[[], float] = time.monotonic
sleep: Callable[[float], None] = time.sleep
test_key: Callable[[], str] = current_test_key
def lookup(self, key: str) -> CacheLookup:
deadline: Final = self.clock() + self.wait_seconds
@ -555,9 +567,9 @@ class CacheEdge:
def forward(
self, mount: str, method: str, url: str, headers: dict[str, str], body: bytes | None, timeout: float,
*, test_key: str | None,
) -> StreamHead | NetworkError:
test_key: Final = self.test_key()
if test_key == SESSION_TEST_KEY or not cacheable_endpoint(mount, method, url, body):
if test_key is None or not cacheable_endpoint(mount, method, url, body):
self.count(mount, "bypass")
self.count(mount, "upstream_attempts")
return forward_stream(

View file

@ -88,13 +88,21 @@ from fixture_canonical import (
)
from fixture_mode import (
FIXTURE_MODES,
SESSION_TEST_KEY,
InvalidFixtureMode,
ReplayMiss,
current_test_key,
parse_fixture_mode,
)
from fixture_profile import IneligibleRequest, MatchProfile, match_profile, strict_identity
from provider_cache import SIGNATURE_HEADERS, CacheEdge, MountPolicy, is_bedrock
from provider_cache import (
SIGNATURE_HEADERS,
CacheEdge,
MountPolicy,
is_bedrock,
scoped_edge_base,
split_test_segment,
)
from provider_cache_routing import LIVE_PROVIDER_REQUIRED
from pydantic import JsonValue, TypeAdapter
@ -778,14 +786,14 @@ def _handle_record(
def _handle_live(
method: str, url: str, headers: Mapping[str, str], body: bytes | None, timeout: float,
cache: CacheEdge | None = None, mount: str = "",
cache: CacheEdge | None = None, mount: str = "", test_key: str | None = None,
) -> EdgeOutcome:
forwarded: Final = {
name: value for name, value in headers.items() if name.lower() not in _REQUEST_DROPPED_HEADERS
}
head: Final = (
forward_stream(method, url, headers=forwarded, body=body, timeout=timeout)
if cache is None else cache.forward(mount, method, url, forwarded, body, timeout)
if cache is None else cache.forward(mount, method, url, forwarded, body, timeout, test_key=test_key)
)
match head:
case NetworkError(message=message):
@ -826,7 +834,7 @@ def handle_edge_request(
return _text_reply(404, f"unknown provider mount {unknown!r}; known mounts: {', '.join(sorted(mounts))}")
mount: Final = resolved.mount
upstream_base: Final = resolved.upstream_base
upstream_path: Final = resolved.upstream_path
test_key, upstream_path = split_test_segment(resolved.upstream_path)
profile: Final = (
backend.recorder.profile
if isinstance(backend, RecordEdge)
@ -858,7 +866,7 @@ def handle_edge_request(
case CacheEdge():
return _handle_live(
method, _upstream_url(upstream_base, upstream_path, split.query), headers, body, timeout,
backend, mount,
backend, mount, test_key,
)
case LiveEdge():
return _handle_live(
@ -1093,19 +1101,25 @@ def provider_edge_api_base(
bundle_dir: Path,
bind_host: str,
advertise_host: str,
test_key: str,
forward_timeout: float = 60.0,
) -> str | None:
"""The api_base a suite gives an edge-wired deployment: None in live mode
(the deployment keeps its real provider api_base) and the process-wide edge
server's mount URL in record and replay, booting the server on first use."""
server's mount URL in record and replay, booting the server on first use.
With the shared cache configured, live mode answers with the cache edge's
mount URL scoped to ``test_key``, the test registering the deployment, and
None outside any test, since a call nobody can attribute is never cached."""
mode: Final = parse_fixture_mode(mode_raw)
match mode:
case InvalidFixtureMode(value=value):
raise ValueError(f"E2E_FIXTURE_MODE={value!r} is not one of {', '.join(FIXTURE_MODES)}")
case "live":
if configured_cache_backend() is not None:
return _shared_cache_edge(bind_host, advertise_host, forward_timeout).api_base(mount)
return None
if configured_cache_backend() is None or test_key == SESSION_TEST_KEY:
return None
return scoped_edge_base(
_shared_cache_edge(bind_host, advertise_host, forward_timeout).api_base(mount), test_key
)
case "record" | "replay":
if is_bedrock(mount):
return None

View file

@ -613,6 +613,8 @@ class ProxyClient:
model_name: str,
litellm_params: LiteLLMParamsBody,
mode: ModelMode | None = None,
*,
provider_live: bool = False,
) -> str:
"""Register a deployment under `model_name` and return its proxy-assigned
model_id, once the model is actually servable on the data plane."""
@ -621,15 +623,20 @@ class ProxyClient:
model_name=model_name,
litellm_params=litellm_params,
model_info=ModelInfoBody(mode=mode),
)
),
provider_live=provider_live,
)
def register_model(self, body: ModelNewBody, listed_for: str | None = None) -> str:
def register_model(
self, body: ModelNewBody, listed_for: str | None = None, *, provider_live: bool = False
) -> str:
"""`create_model` for deployments that carry more than a mode: access groups,
team scoping, a pinned id. `listed_for` is the virtual key whose /v1/models
view must list the deployment before it counts as servable, because a
team-scoped deployment is listed to its own team and to nobody else, master
key included; leave it unset for a proxy-wide model.
key included; leave it unset for a proxy-wide model. `provider_live` keeps
the deployment on its real provider path whatever the cache setting, for a
deployment shared across tests or workers, which no one test could own.
/model/new is a control-plane route; the data plane (which serves /chat,
/ocr, ...) only picks the new model up on its next DB reload, so a call
@ -650,7 +657,8 @@ class ProxyClient:
headers=self.management_headers(),
json=body.model_copy(update={"litellm_params": route_cache_model(
body.litellm_params, provider_edge_base,
enabled=os.environ.get("E2E_PROVIDER_CACHE", "0") == "1", mode=body.model_info.mode,
enabled=os.environ.get("E2E_PROVIDER_CACHE", "0") == "1" and not provider_live,
mode=body.model_info.mode,
)}),
response_type=ModelNewResponse,
)

View file

@ -10,5 +10,5 @@ markers =
weekly: real-provider anomaly load test that spends real money; deselected unless E2E_WEEKLY_ANOMALY is set
managed_files: needs a proxy running with require_managed_files enabled; deselected unless E2E_MANAGED_FILES_STACK is set
prompt_caching_stack: needs a proxy running with router_settings.optional_pre_call_checks including prompt_caching; deselected unless E2E_PROMPT_CACHING_STACK is set
cli_determinism: drives the real claude CLI for several seconds, which widens the window in which another test's in-flight upstream call is attributed to it; deselected unless E2E_CLI_DETERMINISM is set
cli_determinism: drives the real claude CLI for several seconds; deselected unless E2E_CLI_DETERMINISM is set
redis_chaos: load test that pauses the proxy's Redis outright mid-run; needs a proxy booted from gateway/redis_chaos_ci_config.yml on the same host, and is deselected unless E2E_REDIS_CHAOS is set

View file

@ -1264,6 +1264,7 @@ class TestApiBaseSeam:
bundle_dir=tmp_path / "bundle",
bind_host="127.0.0.1",
advertise_host="127.0.0.1",
test_key="tests/e2e/synthetic_suite.py::test_case",
)
is None
)
@ -1276,6 +1277,7 @@ class TestApiBaseSeam:
bundle_dir=tmp_path / "bundle",
bind_host="127.0.0.1",
advertise_host="127.0.0.1",
test_key="tests/e2e/synthetic_suite.py::test_case",
)
def test_unknown_mount_raises_naming_the_known_mounts(self, tmp_path: Path) -> None:
@ -1286,6 +1288,7 @@ class TestApiBaseSeam:
bundle_dir=tmp_path / "bundle",
bind_host="127.0.0.1",
advertise_host="127.0.0.1",
test_key="tests/e2e/synthetic_suite.py::test_case",
)
@pytest.mark.parametrize("mode_raw", ["record", "replay"])
@ -1301,15 +1304,18 @@ class TestApiBaseSeam:
bundle_dir=tmp_path / "bundle",
bind_host="127.0.0.1",
advertise_host="127.0.0.1",
test_key="tests/e2e/synthetic_suite.py::test_case",
) is None
def test_record_mode_boots_one_shared_edge_and_prepares_the_bundle(self, tmp_path: Path) -> None:
root = tmp_path / "bundle"
first = provider_edge_api_base(
"openai", mode_raw="record", bundle_dir=root, bind_host="127.0.0.1", advertise_host="127.0.0.1"
"openai", mode_raw="record", bundle_dir=root, bind_host="127.0.0.1", advertise_host="127.0.0.1",
test_key="tests/e2e/synthetic_suite.py::test_case",
)
second = provider_edge_api_base(
"anthropic", mode_raw="record", bundle_dir=root, bind_host="127.0.0.1", advertise_host="127.0.0.1"
"anthropic", mode_raw="record", bundle_dir=root, bind_host="127.0.0.1", advertise_host="127.0.0.1",
test_key="tests/e2e/synthetic_suite.py::test_case",
)
assert first is not None and second is not None
assert first.endswith("/openai")

View file

@ -26,6 +26,8 @@ from typing import Final, cast
import pytest
from e2e_config import parse_replica_urls
from e2e_http import NoBody, Result, Success, without_retries
from fixture_bundle import slug_for_test
from fixture_mode import current_test_key
from idp import Keycloak
from lifecycle import ResourceManager
from management.jwt_actors import ActorFactory
@ -581,3 +583,35 @@ def test_partial_updates_preserve_explicit_null_at_the_http_boundary(operation:
)
assert json.loads(bodies.get_nowait()) == expected
assert bodies.empty()
@pytest.mark.parametrize("provider_live", (False, True))
def test_registration_binds_the_deployment_to_this_test_unless_it_is_provider_live(
provider_live: bool, monkeypatch: pytest.MonkeyPatch,
) -> None:
"""With the shared cache on, a deployment registered from inside a test carries
this test's segment in its api_base, which is how the edge knows whose recording
a call belongs to. `provider_live` is the opt-out for a deployment no single test
owns: it goes to the proxy exactly as written."""
from provider_cache_redis import configured_cache
monkeypatch.setenv("E2E_PROVIDER_CACHE", "1")
monkeypatch.setenv("E2E_PROVIDER_CACHE_REDIS_URL", "redis://127.0.0.1:1/0")
monkeypatch.setenv("E2E_PROVIDER_CACHE_HMAC_KEY", "synthetic-cache-hmac-key-for-tests")
monkeypatch.setenv("E2E_PROVIDER_CACHE_NAMESPACE", "registration-seam")
configured_cache.cache_clear()
bodies: Final[SimpleQueue[bytes]] = SimpleQueue()
try:
with caller_boundary(status=401, bodies=bodies) as (bootstrap, _), without_retries():
with pytest.raises(AssertionError):
bootstrap.proxy.create_model(
"owned", LiteLLMParamsBody(model="openai/synthetic"), provider_live=provider_live
)
finally:
configured_cache.cache_clear()
params: Final = json.loads(bodies.get_nowait())["litellm_params"]
assert bodies.empty()
if provider_live:
assert params.get("api_base") is None
return
assert params["api_base"].endswith(f"/openai/t/{slug_for_test(current_test_key())}/v1")