diff --git a/tests/e2e/CLAUDE.md b/tests/e2e/CLAUDE.md
index f0d283629b06..0e1eafb51965 100644
--- a/tests/e2e/CLAUDE.md
+++ b/tests/e2e/CLAUDE.md
@@ -171,3 +171,16 @@ other...
e.g. other.auth.jwt.valid_token_allows
other.lifecycle.readiness.reports_db
```
+
+## Hard Rules
+- no monkeypatching, mock tests or unit tests of any kind. if a contributor asks you to write an end to end test, do NOT stage a unit test with it. if you find a product gap, call it out in the PR description
+
+- use model management endpoints to create new models for a test. this could be in a conftest / inline for each test. ask the user what they want.
+
+- do not overengineer a test, i need you to write readable, clean code of what would look like a natural user scenario
+
+- when it comes to typing an input schema for an api endpoint, have it type X = A | B | C ... where X = exhaustive union of all supported input schemas and A, B, C typically are composed by a base type. types are only pretty for a api request / response body. make sure to compose types instead of repeating the same base attributes over and over again.
+
+- use the docker-compose to your advantage and spin up a local proxy, make sure all tests pass. if a test fails due to an internally found issue, let users know to create a linear ticket for it.
+
+- do not use xfail markers, tests should be written in a form that the end user expects it to pass
diff --git a/tests/e2e/claude_code/_basic_messaging.py b/tests/e2e/claude_code/_basic_messaging.py
index f6b82a38f6ae..7c581cc5e38b 100644
--- a/tests/e2e/claude_code/_basic_messaging.py
+++ b/tests/e2e/claude_code/_basic_messaging.py
@@ -27,19 +27,20 @@
from __future__ import annotations
-import os
-from typing import Any, Mapping, Sequence
+from typing import Any, Callable, Mapping, Sequence
import pytest
+from claude_code._env import require_proxy
from claude_code.cli_driver import (
ClaudeCLIError,
+ DriverResult,
failure_diagnostic,
run_claude_models_parallel,
)
-PROXY_BASE_URL_ENV = "LITELLM_PROXY_BASE_URL"
-PROXY_API_KEY_ENV = "LITELLM_PROXY_API_KEY"
+
+ClaudeRunner = Callable[..., Mapping[str, DriverResult | ClaudeCLIError]]
# Floor on the number of `stream_event` records (with delta payloads)
# we expect to see when the proxy actually streams. With
@@ -79,6 +80,8 @@ def run_basic_messaging_cell(
models: Sequence[str],
prompt: str,
verify_streaming: bool = False,
+ env: Mapping[str, str] | None = None,
+ runner: ClaudeRunner = run_claude_models_parallel,
) -> None:
"""Run the shared `basic_messaging_*` × cell body.
@@ -99,28 +102,13 @@ def run_basic_messaging_cell(
streamed reply to a single ``assistant`` event in
``--print --output-format stream-json`` mode).
"""
- base_url = os.environ.get(PROXY_BASE_URL_ENV)
- api_key = os.environ.get(PROXY_API_KEY_ENV)
- if not base_url or not api_key:
- compat_result.set(
- {
- "status": "fail",
- "error": (
- f"missing required env: set {PROXY_BASE_URL_ENV} and "
- f"{PROXY_API_KEY_ENV} to point at a running LiteLLM proxy"
- ),
- }
- )
- pytest.fail(
- f"{PROXY_BASE_URL_ENV} / {PROXY_API_KEY_ENV} not configured",
- pytrace=False,
- )
+ base_url, api_key = require_proxy(compat_result, env=env)
extra_args: Sequence[str] = (
("--include-partial-messages",) if verify_streaming else ()
)
- outcomes = run_claude_models_parallel(
+ outcomes = runner(
models=models,
prompt=prompt,
base_url=base_url,
diff --git a/tests/e2e/claude_code/_builder_unit_tests/fixtures/expected_matrix.json b/tests/e2e/claude_code/_builder_unit_tests/fixtures/expected_matrix.json
index d3aca0142dc4..405a3772a907 100644
--- a/tests/e2e/claude_code/_builder_unit_tests/fixtures/expected_matrix.json
+++ b/tests/e2e/claude_code/_builder_unit_tests/fixtures/expected_matrix.json
@@ -26,7 +26,7 @@
"providers": {
"anthropic": {
"status": "fail",
- "error": "[claude-sonnet-4-6] tool call dropped"
+ "error": "[claude-sonnet-4-5] tool call dropped"
},
"bedrock_invoke": {
"status": "not_applicable",
diff --git a/tests/e2e/claude_code/_builder_unit_tests/fixtures/results.json b/tests/e2e/claude_code/_builder_unit_tests/fixtures/results.json
index a01540c394f3..f1b00385f176 100644
--- a/tests/e2e/claude_code/_builder_unit_tests/fixtures/results.json
+++ b/tests/e2e/claude_code/_builder_unit_tests/fixtures/results.json
@@ -10,7 +10,7 @@
{
"feature_id": "basic_messaging_non_streaming",
"provider": "anthropic",
- "nodeid": "tests/e2e/claude_code/basic_messaging_non_streaming/test_anthropic.py::test_basic_messaging_non_streaming_anthropic[claude-sonnet-4-6]",
+ "nodeid": "tests/e2e/claude_code/basic_messaging_non_streaming/test_anthropic.py::test_basic_messaging_non_streaming_anthropic[claude-sonnet-4-5]",
"result": {"status": "pass"}
},
{
@@ -28,8 +28,8 @@
{
"feature_id": "tool_use",
"provider": "anthropic",
- "nodeid": "tests/e2e/claude_code/tool_use/test_anthropic.py::test_x[claude-sonnet-4-6]",
- "result": {"status": "fail", "error": "[claude-sonnet-4-6] tool call dropped"}
+ "nodeid": "tests/e2e/claude_code/tool_use/test_anthropic.py::test_x[claude-sonnet-4-5]",
+ "result": {"status": "fail", "error": "[claude-sonnet-4-5] tool call dropped"}
},
{
"feature_id": "tool_use",
diff --git a/tests/e2e/claude_code/_builder_unit_tests/test_matrix_builder.py b/tests/e2e/claude_code/_builder_unit_tests/test_matrix_builder.py
index 9ddbdd298465..95db8acec70e 100644
--- a/tests/e2e/claude_code/_builder_unit_tests/test_matrix_builder.py
+++ b/tests/e2e/claude_code/_builder_unit_tests/test_matrix_builder.py
@@ -393,7 +393,7 @@ def test_build_matrix_6x5_grid_matches_published_sample():
feature_ids = [feature["id"] for feature in manifest["features"]]
providers = manifest["providers"]
- models = ["claude-haiku-4-5", "claude-sonnet-4-6", "claude-opus-4-7"]
+ models = ["claude-haiku-4-5", "claude-sonnet-4-5", "claude-opus-4-7"]
results = []
for feature_id in feature_ids:
diff --git a/tests/e2e/claude_code/_builder_unit_tests/test_v0_layout.py b/tests/e2e/claude_code/_builder_unit_tests/test_v0_layout.py
index b1745008facd..f1772dd15efd 100644
--- a/tests/e2e/claude_code/_builder_unit_tests/test_v0_layout.py
+++ b/tests/e2e/claude_code/_builder_unit_tests/test_v0_layout.py
@@ -152,7 +152,7 @@ def test_per_provider_test_file_imports_and_parametrizes_three_models(
`claude-opus-4-7-bedrock-invoke`), so we check for the tier
substrings rather than exact alias names."""
text = (REPO_ROOT / feature_id / f"test_{provider}.py").read_text()
- for tier in ("haiku-4-5", "sonnet-4-6", "opus-4-7"):
+ for tier in ("haiku-4-5", "sonnet-4-5", "opus-4-7"):
assert (
tier in text
), f"{feature_id}/test_{provider}.py does not reference {tier}"
diff --git a/tests/e2e/claude_code/_compat_models.py b/tests/e2e/claude_code/_compat_models.py
new file mode 100644
index 000000000000..23c9c82e5960
--- /dev/null
+++ b/tests/e2e/claude_code/_compat_models.py
@@ -0,0 +1,86 @@
+"""Load the claude_code compat matrix's deployment list from
+``test_config.yaml``.
+
+``test_config.yaml`` is the ground-truth config the stage deployment
+uses; parsing it at fixture time means a change there (new tier, tier
+retirement, provider swap, endpoint rename) reaches the fixture with
+no extra edit. A drift-check test asserts every ``*_MODELS`` list
+referenced by the compat cells is covered by the yaml, so a cell that
+adds a probe for a name the yaml doesn't know about fails loudly at
+collection instead of at 400-time.
+"""
+
+from __future__ import annotations
+
+from dataclasses import dataclass
+from pathlib import Path
+from typing import Callable, Mapping
+
+import yaml
+
+from models import LiteLLMParamsBody
+
+CONFIG_PATH = Path(__file__).resolve().parent / "test_config.yaml"
+
+
+@dataclass(frozen=True, slots=True)
+class CompatDeployment:
+ model_name: str
+ litellm_params: LiteLLMParamsBody
+
+
+# The yaml uses ``vertex_ai_*`` for the vertex project/location fields
+# (that is the spelling the proxy config file historically standardized
+# on), while ``LiteLLMParamsBody`` names them without the ``_ai`` infix
+# (matching the proxy's DB column). Both spellings resolve at call time
+# on the proxy side, but pydantic silently drops unknown fields, so a
+# raw ``LiteLLMParamsBody(**entry)`` would produce a body with the
+# vertex project stripped - the resulting deployment 400s at
+# ``/v1/messages`` with "Invalid model name". Normalize the yaml keys
+# to the pydantic names in one place.
+_YAML_TO_PYDANTIC_ALIASES = {
+ "vertex_ai_project": "vertex_project",
+ "vertex_ai_location": "vertex_location",
+ "vertex_ai_credentials": "vertex_credentials",
+}
+
+
+def _normalize_params(raw: Mapping[str, object]) -> dict[str, object]:
+ return {_YAML_TO_PYDANTIC_ALIASES.get(k, k): v for k, v in raw.items()}
+
+
+ConfigReader = Callable[[Path], str]
+
+
+def _default_reader(path: Path) -> str:
+ return path.read_text()
+
+
+def load_all_deployments(
+ config_path: Path = CONFIG_PATH,
+ reader: ConfigReader = _default_reader,
+) -> tuple[CompatDeployment, ...]:
+ """Every deployment declared in the yaml, in file order."""
+ doc = yaml.safe_load(reader(config_path))
+ model_list = doc.get("model_list") or []
+ return tuple(
+ CompatDeployment(
+ model_name=entry["model_name"],
+ litellm_params=LiteLLMParamsBody(
+ **_normalize_params(entry["litellm_params"])
+ ),
+ )
+ for entry in model_list
+ )
+
+
+def all_expected_model_names(
+ *,
+ config_path: Path = CONFIG_PATH,
+ reader: ConfigReader = _default_reader,
+) -> frozenset[str]:
+ """Every virtual name the compat matrix declares - the ground truth
+ the cells are supposed to probe. Used by the drift-check test."""
+ return frozenset(
+ d.model_name for d in load_all_deployments(config_path, reader)
+ )
diff --git a/tests/e2e/claude_code/_driver_unit_tests/test_basic_messaging.py b/tests/e2e/claude_code/_driver_unit_tests/test_basic_messaging.py
index 018121a8e5c9..bb195ac50fe1 100644
--- a/tests/e2e/claude_code/_driver_unit_tests/test_basic_messaging.py
+++ b/tests/e2e/claude_code/_driver_unit_tests/test_basic_messaging.py
@@ -1,20 +1,19 @@
"""Unit tests for the shared `run_basic_messaging_cell` helper.
-These tests mock `run_claude_models_parallel` so they exercise the
-helper's branching (env-missing guard, per-model pass/fail/empty-text,
-streaming wire check) without spawning the real CLI. The streaming
-check is the regression we care about: a proxy that buffers the
-upstream stream must turn the cell red, not green.
+These tests inject a fake ``ClaudeRunner`` and a fake env mapping so
+they exercise the helper's branching (env-missing guard, per-model
+pass/fail/empty-text, streaming wire check) without spawning the real
+CLI or touching ``os.environ``. The streaming check is the regression
+we care about: a proxy that buffers the upstream stream must turn the
+cell red, not green.
"""
from __future__ import annotations
-import os
from typing import Any, Dict, List, Mapping, Optional, Sequence
import pytest
-from claude_code import _basic_messaging
from claude_code._basic_messaging import (
MIN_STREAM_DELTA_EVENTS,
_count_stream_event_deltas,
@@ -23,6 +22,12 @@
from claude_code.cli_driver import DriverResult
+_PROXY_ENV: Mapping[str, str] = {
+ "LITELLM_PROXY_URL": "http://localhost:4000",
+ "LITELLM_MASTER_KEY": "sk-test",
+}
+
+
class _FakeResult:
"""Stand-in for the test's `compat_result` fixture.
@@ -82,16 +87,17 @@ def _buffered_events() -> List[Dict[str, Any]]:
]
-def _install_fake_runner(monkeypatch, *, outcomes_by_model):
- """Patch `run_claude_models_parallel` to return canned outcomes.
+def _make_fake_runner(*, outcomes_by_model):
+ """Build an injectable runner that returns canned outcomes and
+ records the kwargs the helper passed in.
- Captures the kwargs the cell passed in so tests can assert on
- `extra_args` (which is how the streaming variant opts into
- `--include-partial-messages`).
- """
+ Returns a ``(runner, captured)`` pair; ``captured`` is a dict the
+ test can assert against without any global mutation, which is why
+ we prefer DI over ``monkeypatch.setattr``: the helper takes a
+ ``runner=`` kwarg, so tests bind their fake directly."""
captured: Dict[str, Any] = {}
- def fake(*, models, prompt, base_url, api_key, extra_args=None, **_kwargs):
+ def runner(*, models, prompt, base_url, api_key, extra_args=None, **_kwargs):
captured["models"] = list(models)
captured["prompt"] = prompt
captured["base_url"] = base_url
@@ -99,14 +105,7 @@ def fake(*, models, prompt, base_url, api_key, extra_args=None, **_kwargs):
captured["extra_args"] = list(extra_args) if extra_args else []
return {model: outcomes_by_model[model] for model in models}
- monkeypatch.setattr(_basic_messaging, "run_claude_models_parallel", fake)
- return captured
-
-
-@pytest.fixture(autouse=True)
-def _proxy_env(monkeypatch):
- monkeypatch.setenv("LITELLM_PROXY_BASE_URL", "http://localhost:4000")
- monkeypatch.setenv("LITELLM_PROXY_API_KEY", "sk-test")
+ return runner, captured
def test_count_stream_event_deltas_only_counts_records_with_event_payload():
@@ -123,28 +122,30 @@ def test_count_stream_event_deltas_only_counts_records_with_event_payload():
assert _count_stream_event_deltas(events) == 2
-def test_verify_streaming_passes_when_proxy_streams(monkeypatch):
+def test_verify_streaming_passes_when_proxy_streams():
fake_result = _FakeResult()
model = "claude-haiku-4-5"
outcome = DriverResult(text="1\n2\n3", events=_streamed_events(n_deltas=5))
- captured = _install_fake_runner(monkeypatch, outcomes_by_model={model: outcome})
+ runner, captured = _make_fake_runner(outcomes_by_model={model: outcome})
run_basic_messaging_cell(
compat_result=fake_result,
models=[model],
prompt="Count from 1 to 5, one number per line.",
verify_streaming=True,
+ env=_PROXY_ENV,
+ runner=runner,
)
assert captured["extra_args"] == ["--include-partial-messages"]
assert fake_result.rows == [{"status": "pass"}]
-def test_verify_streaming_fails_when_proxy_buffers(monkeypatch):
+def test_verify_streaming_fails_when_proxy_buffers():
fake_result = _FakeResult()
model = "claude-haiku-4-5"
outcome = DriverResult(text="1\n2\n3", events=_buffered_events())
- _install_fake_runner(monkeypatch, outcomes_by_model={model: outcome})
+ runner, _captured = _make_fake_runner(outcomes_by_model={model: outcome})
with pytest.raises(pytest.fail.Exception):
run_basic_messaging_cell(
@@ -152,7 +153,9 @@ def test_verify_streaming_fails_when_proxy_buffers(monkeypatch):
models=[model],
prompt="Count from 1 to 5, one number per line.",
verify_streaming=True,
- )
+ env=_PROXY_ENV,
+ runner=runner,
+ )
assert len(fake_result.rows) == 1
row = fake_result.rows[0]
@@ -161,33 +164,35 @@ def test_verify_streaming_fails_when_proxy_buffers(monkeypatch):
assert f"< {MIN_STREAM_DELTA_EVENTS}" in row["error"]
-def test_non_streaming_variant_omits_partial_messages_flag(monkeypatch):
+def test_non_streaming_variant_omits_partial_messages_flag():
"""Default `verify_streaming=False` keeps the non-streaming wire identical."""
fake_result = _FakeResult()
model = "claude-haiku-4-5"
outcome = DriverResult(text="pong", events=_buffered_events())
- captured = _install_fake_runner(monkeypatch, outcomes_by_model={model: outcome})
+ runner, captured = _make_fake_runner(outcomes_by_model={model: outcome})
run_basic_messaging_cell(
compat_result=fake_result,
models=[model],
prompt="Reply with the single word 'pong' and nothing else.",
+ env=_PROXY_ENV,
+ runner=runner,
)
assert captured["extra_args"] == []
assert fake_result.rows == [{"status": "pass"}]
-def test_verify_streaming_requires_all_models_to_stream(monkeypatch):
+def test_verify_streaming_requires_all_models_to_stream():
"""If any one tier buffers, the cell fails — same all-must-pass shape as
the non-streaming check."""
fake_result = _FakeResult()
outcomes = {
"claude-haiku-4-5": DriverResult(text="ok", events=_streamed_events(5)),
- "claude-sonnet-4-6": DriverResult(text="ok", events=_buffered_events()),
+ "claude-sonnet-4-5": DriverResult(text="ok", events=_buffered_events()),
"claude-opus-4-7": DriverResult(text="ok", events=_streamed_events(5)),
}
- _install_fake_runner(monkeypatch, outcomes_by_model=outcomes)
+ runner, _captured = _make_fake_runner(outcomes_by_model=outcomes)
with pytest.raises(pytest.fail.Exception):
run_basic_messaging_cell(
@@ -195,7 +200,30 @@ def test_verify_streaming_requires_all_models_to_stream(monkeypatch):
models=list(outcomes.keys()),
prompt="Count from 1 to 5, one number per line.",
verify_streaming=True,
- )
+ env=_PROXY_ENV,
+ runner=runner,
+ )
statuses = [row["status"] for row in fake_result.rows]
assert statuses == ["pass", "fail", "pass"]
+
+
+def test_missing_proxy_env_hard_fails_regardless_of_runner():
+ """The env guard fires before the runner is called, and takes the
+ env from the injected mapping (not os.environ). Passing an empty
+ env dict must hard-fail even if a happy runner is bound."""
+ fake_result = _FakeResult()
+ runner, captured = _make_fake_runner(outcomes_by_model={})
+
+ with pytest.raises(pytest.fail.Exception):
+ run_basic_messaging_cell(
+ compat_result=fake_result,
+ models=["claude-haiku-4-5"],
+ prompt="whatever",
+ env={},
+ runner=runner,
+ )
+
+ assert captured == {}, "runner must not be called when env resolution fails"
+
+
diff --git a/tests/e2e/claude_code/_driver_unit_tests/test_cli_driver.py b/tests/e2e/claude_code/_driver_unit_tests/test_cli_driver.py
index 786f60299931..6052bfd981f7 100644
--- a/tests/e2e/claude_code/_driver_unit_tests/test_cli_driver.py
+++ b/tests/e2e/claude_code/_driver_unit_tests/test_cli_driver.py
@@ -139,7 +139,7 @@ def test_run_claude_inherits_only_allowlisted_os_environ(monkeypatch):
monkeypatch.setenv("HOME", "/home/runner")
monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "totally-secret")
monkeypatch.setenv("ANTHROPIC_API_KEY", "sk-proxy-only")
- monkeypatch.setenv("AZURE_FOUNDRY_API_KEY", "azure-secret")
+ monkeypatch.setenv("AZURE_AI_API_KEY", "azure-secret")
monkeypatch.setenv("VERTEXAI_CREDENTIALS", '{"private_key": "leak"}')
monkeypatch.setenv("GITHUB_TOKEN", "ghs_xxx")
@@ -155,7 +155,7 @@ def test_run_claude_inherits_only_allowlisted_os_environ(monkeypatch):
assert env["PATH"] == "/usr/bin:/usr/local/bin"
assert "AWS_SECRET_ACCESS_KEY" not in env
assert "ANTHROPIC_API_KEY" not in env
- assert "AZURE_FOUNDRY_API_KEY" not in env
+ assert "AZURE_AI_API_KEY" not in env
assert "VERTEXAI_CREDENTIALS" not in env
assert "GITHUB_TOKEN" not in env
diff --git a/tests/e2e/claude_code/_driver_unit_tests/test_rate_limiter.py b/tests/e2e/claude_code/_driver_unit_tests/test_rate_limiter.py
index 92907eda3c47..32a1c30af98b 100644
--- a/tests/e2e/claude_code/_driver_unit_tests/test_rate_limiter.py
+++ b/tests/e2e/claude_code/_driver_unit_tests/test_rate_limiter.py
@@ -60,10 +60,10 @@
"model, expected",
[
("claude-haiku-4-5", PROVIDER_ANTHROPIC),
- ("claude-sonnet-4-6", PROVIDER_ANTHROPIC),
+ ("claude-sonnet-4-5", PROVIDER_ANTHROPIC),
("claude-opus-4-7", PROVIDER_ANTHROPIC),
("claude-haiku-4-5-azure", PROVIDER_AZURE),
- ("claude-sonnet-4-6-azure", PROVIDER_AZURE),
+ ("claude-sonnet-4-5-azure", PROVIDER_AZURE),
("claude-opus-4-7-vertex", PROVIDER_VERTEX_AI),
("claude-haiku-4-5-bedrock-converse", PROVIDER_BEDROCK_CONVERSE),
("claude-haiku-4-5-bedrock-invoke", PROVIDER_BEDROCK_INVOKE),
diff --git a/tests/e2e/claude_code/_env.py b/tests/e2e/claude_code/_env.py
new file mode 100644
index 000000000000..e6ed93e6d8e4
--- /dev/null
+++ b/tests/e2e/claude_code/_env.py
@@ -0,0 +1,88 @@
+"""Proxy-env resolution for the claude_code compat cells.
+
+Historically each cell hardcoded ``LITELLM_PROXY_BASE_URL`` and
+``LITELLM_PROXY_API_KEY``, which do not match the names the rest of
+``tests/e2e/`` reads (``LITELLM_PROXY_URL`` / ``LITELLM_MASTER_KEY`` from
+``e2e_config.py``). That drift means anyone standing up a live proxy for
+one suite has to set two extra env vars for another, and CI wiring has to
+export both spellings. Everything under ``claude_code/`` now goes through
+``resolve_proxy`` / ``require_proxy`` here, so the naming lives in one
+place and either spelling works (primary wins on tie).
+"""
+
+from __future__ import annotations
+
+import os
+from typing import Mapping, NamedTuple
+
+import pytest
+
+
+class ProxyConfig(NamedTuple):
+ base_url: str
+ api_key: str
+
+
+PRIMARY_BASE_URL_ENV = "LITELLM_PROXY_URL"
+PRIMARY_API_KEY_ENV = "LITELLM_MASTER_KEY"
+
+LEGACY_BASE_URL_ENV = "LITELLM_PROXY_BASE_URL"
+LEGACY_API_KEY_ENV = "LITELLM_PROXY_API_KEY"
+
+
+def _pick(mapping: Mapping[str, str], primary: str, legacy: str) -> str | None:
+ return mapping.get(primary) or mapping.get(legacy) or None
+
+
+def resolve_proxy_from(mapping: Mapping[str, str]) -> ProxyConfig | None:
+ """Pure resolver: takes an env mapping, returns a ProxyConfig if
+ both a base URL and an API key are present under either the primary
+ or legacy names, else None. Extracted so tests can exercise it
+ without mutating ``os.environ``."""
+ base_url = _pick(mapping, PRIMARY_BASE_URL_ENV, LEGACY_BASE_URL_ENV)
+ api_key = _pick(mapping, PRIMARY_API_KEY_ENV, LEGACY_API_KEY_ENV)
+ if not base_url or not api_key:
+ return None
+ return ProxyConfig(base_url=base_url, api_key=api_key)
+
+
+def resolve_proxy(env: Mapping[str, str] | None = None) -> ProxyConfig | None:
+ """Convenience wrapper that defaults to ``os.environ``. Prefer
+ calling ``resolve_proxy_from(env)`` from tests so nothing has to
+ reach into the process environment."""
+ return resolve_proxy_from(os.environ if env is None else env)
+
+
+def _fail_missing_proxy_env(compat_result) -> None:
+ compat_result.set(
+ {
+ "status": "fail",
+ "error": (
+ f"missing required env: set {PRIMARY_BASE_URL_ENV} and "
+ f"{PRIMARY_API_KEY_ENV} (or the legacy "
+ f"{LEGACY_BASE_URL_ENV} / {LEGACY_API_KEY_ENV}) to "
+ "point at a running LiteLLM proxy"
+ ),
+ }
+ )
+ pytest.fail(
+ f"{PRIMARY_BASE_URL_ENV} / {PRIMARY_API_KEY_ENV} not configured",
+ pytrace=False,
+ )
+
+
+def require_proxy(
+ compat_result,
+ *,
+ env: Mapping[str, str] | None = None,
+) -> ProxyConfig:
+ """Return the proxy config (base URL + master key), or hard-fail
+ the test.
+
+ ``env`` is injected for tests; production callers pass nothing and
+ the process env is used. This keeps tests off ``monkeypatch.setenv``
+ for a check that is a pure function of its inputs."""
+ cfg = resolve_proxy(env)
+ if cfg is None:
+ _fail_missing_proxy_env(compat_result)
+ return cfg
diff --git a/tests/e2e/claude_code/_pr_gate_unit_tests/test_bash_tool_restrictions.py b/tests/e2e/claude_code/_pr_gate_unit_tests/test_bash_tool_restrictions.py
index d698131670a8..ebd6d436d715 100644
--- a/tests/e2e/claude_code/_pr_gate_unit_tests/test_bash_tool_restrictions.py
+++ b/tests/e2e/claude_code/_pr_gate_unit_tests/test_bash_tool_restrictions.py
@@ -35,8 +35,7 @@
import pytest
-REPO_ROOT = Path(__file__).resolve().parents[4]
-CLAUDE_CODE_DIR = REPO_ROOT / "tests" / "e2e" / "claude_code"
+CLAUDE_CODE_DIR = Path(__file__).resolve().parents[1]
# Feature directories whose cells drive the `Bash` built-in tool. Add
# new entries here when a new Bash-using feature is added; the test
@@ -74,14 +73,14 @@ def _has_bare_bash_token(text: str) -> bool:
@pytest.mark.parametrize(
- "cell", list(_bash_cells()), ids=lambda p: str(p.relative_to(REPO_ROOT))
+ "cell", list(_bash_cells()), ids=lambda p: str(p.relative_to(CLAUDE_CODE_DIR))
)
def test_bash_allow_rule_is_pinned_to_exact_echo_pong(cell: Path) -> None:
"""The cell must pass `Bash(echo pong)` as the allow rule, not the
unrestricted `Bash` value that was originally flagged."""
text = cell.read_text()
assert '"Bash(echo pong)"' in text, (
- f"{cell.relative_to(REPO_ROOT)} must restrict `--allowed-tools` to "
+ f"{cell.relative_to(CLAUDE_CODE_DIR)} must restrict `--allowed-tools` to "
f'`Bash(echo pong)` (exact-match pattern). Unrestricted `"Bash"` '
f"grants arbitrary host command execution to model-controlled "
f"tool_use blocks, which can read `docker inspect compat-proxy` "
@@ -95,7 +94,7 @@ def test_bash_allow_rule_is_pinned_to_exact_echo_pong(cell: Path) -> None:
# in text` short-circuits to True and lets a stray bare `"Bash"`
# slip through silently.
assert not _has_bare_bash_token(text), (
- f"{cell.relative_to(REPO_ROOT)} still references the unrestricted "
+ f"{cell.relative_to(CLAUDE_CODE_DIR)} still references the unrestricted "
f'`"Bash"` value outside the `"Bash(echo pong)"` allow rule — '
f"sweep it out before merging."
)
@@ -130,7 +129,7 @@ def test_has_bare_bash_token_ignores_unrelated_substrings():
@pytest.mark.parametrize(
- "cell", list(_bash_cells()), ids=lambda p: str(p.relative_to(REPO_ROOT))
+ "cell", list(_bash_cells()), ids=lambda p: str(p.relative_to(CLAUDE_CODE_DIR))
)
def test_bash_cell_uses_dontask_permission_mode(cell: Path) -> None:
"""The cell must pair the allow rule with `--permission-mode dontAsk`
@@ -139,9 +138,25 @@ def test_bash_cell_uses_dontask_permission_mode(cell: Path) -> None:
succeed without ever surfacing the security issue)."""
text = cell.read_text()
assert '"--permission-mode"' in text and '"dontAsk"' in text, (
- f"{cell.relative_to(REPO_ROOT)} must pass `--permission-mode dontAsk` "
+ f"{cell.relative_to(CLAUDE_CODE_DIR)} must pass `--permission-mode dontAsk` "
f"alongside the `Bash(echo pong)` allow rule. Without dontAsk, "
f"commands outside the allow rule fall back to the default ask-"
f"mode behavior, which in `--print` (headless) mode is non-"
f"interactive — defeating the explicit-allow contract."
)
+
+
+def test_claude_code_dir_anchor_is_layout_independent() -> None:
+ """CLAUDE_CODE_DIR must resolve to the `claude_code/` directory that
+ contains this test file, regardless of how deep the repository is
+ mounted. The previous anchor `Path(__file__).resolve().parents[4]`
+ baked in the host layout (repo root sits four levels up) and broke
+ when the suite runs inside the stage container, where tests/e2e/ is
+ mounted at /app/e2e/ so `parents[4]` resolves to filesystem root and
+ the BASH_FEATURE_DIRS assertion looks for `/tests/e2e/claude_code/
+ tool_use`. Anchoring at `parents[1]` (the sibling of this file's
+ parent) is the same directory in both layouts.
+ """
+ assert CLAUDE_CODE_DIR.name == "claude_code"
+ assert CLAUDE_CODE_DIR.is_dir()
+ assert (CLAUDE_CODE_DIR / "_pr_gate_unit_tests" / Path(__file__).name).is_file()
diff --git a/tests/e2e/claude_code/_pr_gate_unit_tests/test_compat_models.py b/tests/e2e/claude_code/_pr_gate_unit_tests/test_compat_models.py
new file mode 100644
index 000000000000..5ab7d42f7b97
--- /dev/null
+++ b/tests/e2e/claude_code/_pr_gate_unit_tests/test_compat_models.py
@@ -0,0 +1,117 @@
+"""Regression tests for the compat-model registration loader.
+
+The compat cells hardcode virtual model names like ``claude-sonnet-4-5``
+and expect them to be registered on the proxy before the cell runs. The
+session fixture in ``conftest.py`` reads ``test_config.yaml`` and POSTs
+those deployments via ``/model/new``. These tests pin the invariants
+that make that safe:
+
+- The yaml declares an entry for every virtual name a cell references -
+ otherwise a cell probes a name the fixture never registered, and the
+ cell hits an ``Invalid model name`` 400 that is much harder to trace.
+
+- The ``vertex_ai_*`` yaml keys get normalized to the ``vertex_*``
+ pydantic-body names before ``LiteLLMParamsBody(**)`` sees them, so
+ the vertex project/location aren't silently dropped by pydantic's
+ ``extra="ignore"`` default.
+"""
+
+from __future__ import annotations
+
+import re
+from pathlib import Path
+
+import pytest
+
+from claude_code._compat_models import (
+ all_expected_model_names,
+ load_all_deployments,
+)
+
+
+CLAUDE_CODE_DIR = Path(__file__).resolve().parents[1]
+
+
+def _cell_declared_model_names() -> frozenset[str]:
+ """Every ``"claude-*"`` model name a compat cell hardcodes in a
+ ``*_MODELS`` list. Uses a simple regex rather than importing every
+ cell because the cells depend on the harness which depends on env
+ the unit-test run does not have."""
+ pattern = re.compile(r'"(claude-[a-zA-Z0-9._-]+)"')
+ found: set[str] = set()
+ for path in CLAUDE_CODE_DIR.glob("*/test_*.py"):
+ if path.parent.name.startswith("_"):
+ continue
+ for match in pattern.finditer(path.read_text()):
+ name = match.group(1)
+ # Skip upstream model references (they carry a version
+ # suffix or the ``anthropic/`` provider prefix - we only
+ # want proxy-side virtual names here).
+ if "/" in name or "@" in name:
+ continue
+ found.add(name)
+ return frozenset(found)
+
+
+def test_yaml_covers_every_cell_declared_model_name() -> None:
+ """Every ``"claude-..."`` string a cell probes must have a
+ corresponding ``model_list`` entry in ``test_config.yaml``. A new
+ cell that adds a probe for a name the yaml doesn't know fails this
+ test - the alternative is a 400 at runtime that is much harder to
+ diagnose."""
+ yaml_names = all_expected_model_names()
+ cell_names = _cell_declared_model_names()
+ missing = cell_names - yaml_names
+ assert not missing, (
+ f"compat cells reference model names not declared in "
+ f"test_config.yaml: {sorted(missing)}. Add a matching "
+ f"model_list entry so the session fixture can register them."
+ )
+
+
+def test_yaml_has_no_unused_declarations() -> None:
+ """Every declaration in ``test_config.yaml`` is referenced by at
+ least one cell. A yaml entry no test exercises is dead
+ configuration and drift-prone; delete it or add the cell."""
+ yaml_names = all_expected_model_names()
+ cell_names = _cell_declared_model_names()
+ unused = yaml_names - cell_names
+ assert not unused, (
+ f"test_config.yaml declares model names no cell references: "
+ f"{sorted(unused)}. Delete them or add the cell."
+ )
+
+
+def test_load_returns_fifteen_deployments() -> None:
+ """The compat matrix is 3 tiers x 5 provider surfaces = 15. Pin the
+ count so a future edit to the yaml can't silently drop a tier."""
+ assert len(load_all_deployments()) == 15
+
+
+def test_deployments_are_hashable_and_frozen() -> None:
+ """``CompatDeployment`` is frozen so tests cannot accidentally
+ mutate the shared list mid-session."""
+ d = load_all_deployments()[0]
+ with pytest.raises((AttributeError, TypeError)):
+ d.model_name = "mutated" # type: ignore[misc]
+
+
+def test_vertex_yaml_keys_populate_pydantic_body() -> None:
+ """The yaml spells vertex fields ``vertex_ai_project`` /
+ ``vertex_ai_location`` but ``LiteLLMParamsBody`` names them
+ ``vertex_project`` / ``vertex_location``. Without the alias
+ normalization the pydantic body silently drops the yaml keys, and
+ the deployment gets registered with no vertex project - a real
+ incident the drift regressed twice historically."""
+ all_deployments = load_all_deployments()
+ vertex = [
+ d for d in all_deployments if d.model_name.endswith("-vertex")
+ ]
+ assert vertex, "no vertex deployments found in yaml"
+ for d in vertex:
+ assert d.litellm_params.vertex_project, (
+ f"{d.model_name} lost its vertex_project after normalization"
+ )
+ assert d.litellm_params.vertex_location, (
+ f"{d.model_name} lost its vertex_location after normalization"
+ )
diff --git a/tests/e2e/claude_code/_pr_gate_unit_tests/test_env_resolution.py b/tests/e2e/claude_code/_pr_gate_unit_tests/test_env_resolution.py
new file mode 100644
index 000000000000..73aaab89cbbb
--- /dev/null
+++ b/tests/e2e/claude_code/_pr_gate_unit_tests/test_env_resolution.py
@@ -0,0 +1,198 @@
+"""Regression tests for ``claude_code/_env.py``.
+
+Pin the resolution rules so a future edit cannot silently reintroduce
+the ``LITELLM_PROXY_BASE_URL`` / ``LITELLM_PROXY_API_KEY`` naming drift
+that made every ``claude_code`` cell fail with "not configured" even
+when the surrounding e2e suite had a live proxy configured under the
+suite-wide ``LITELLM_PROXY_URL`` / ``LITELLM_MASTER_KEY`` names.
+"""
+
+from __future__ import annotations
+
+import pytest
+
+from claude_code._env import (
+ LEGACY_API_KEY_ENV,
+ LEGACY_BASE_URL_ENV,
+ PRIMARY_API_KEY_ENV,
+ PRIMARY_BASE_URL_ENV,
+ ProxyConfig,
+ require_proxy,
+ resolve_proxy_from,
+)
+
+
+class _CompatResultStub:
+ """Minimal stand-in for the compat_result fixture used by cells."""
+
+ def __init__(self) -> None:
+ self.calls: list[dict[str, str]] = []
+
+ def set(self, payload: dict[str, str]) -> None:
+ self.calls.append(payload)
+
+
+def test_primary_env_names_match_suite_wide_config() -> None:
+ """The primary names claude_code reads must exactly match the ones
+ ``e2e_config.py`` reads for the rest of the suite. Anything else
+ silently reintroduces the drift this refactor cleaned up."""
+ assert PRIMARY_BASE_URL_ENV == "LITELLM_PROXY_URL"
+ assert PRIMARY_API_KEY_ENV == "LITELLM_MASTER_KEY"
+
+
+def test_returns_none_when_no_env_is_set() -> None:
+ assert resolve_proxy_from({}) is None
+
+
+def test_returns_none_when_only_url_is_set() -> None:
+ assert (
+ resolve_proxy_from({PRIMARY_BASE_URL_ENV: "http://localhost:4000"}) is None
+ )
+
+
+def test_returns_none_when_only_key_is_set() -> None:
+ assert resolve_proxy_from({PRIMARY_API_KEY_ENV: "sk-1234"}) is None
+
+
+def test_primary_pair_resolves() -> None:
+ cfg = resolve_proxy_from(
+ {
+ PRIMARY_BASE_URL_ENV: "http://localhost:4000",
+ PRIMARY_API_KEY_ENV: "sk-1234",
+ }
+ )
+ assert cfg == ProxyConfig("http://localhost:4000", "sk-1234")
+
+
+def test_legacy_pair_resolves_when_primary_missing() -> None:
+ """Existing CI wiring that only exports the legacy names must keep
+ working — otherwise this refactor breaks stage on the way in."""
+ cfg = resolve_proxy_from(
+ {
+ LEGACY_BASE_URL_ENV: "http://legacy:4000",
+ LEGACY_API_KEY_ENV: "sk-legacy",
+ }
+ )
+ assert cfg == ProxyConfig("http://legacy:4000", "sk-legacy")
+
+
+def test_primary_wins_over_legacy_when_both_set() -> None:
+ """When both spellings are present, the suite-wide names take
+ precedence. Otherwise a stale legacy export in the environment
+ would silently override a caller who set the primary names."""
+ cfg = resolve_proxy_from(
+ {
+ PRIMARY_BASE_URL_ENV: "http://primary:4000",
+ LEGACY_BASE_URL_ENV: "http://legacy:4000",
+ PRIMARY_API_KEY_ENV: "sk-primary",
+ LEGACY_API_KEY_ENV: "sk-legacy",
+ }
+ )
+ assert cfg == ProxyConfig("http://primary:4000", "sk-primary")
+
+
+def test_mixed_url_primary_key_legacy_resolves() -> None:
+ """One spelling per var is fine — mixing across pairs must still
+ resolve, so a partial migration doesn't strand a caller."""
+ cfg = resolve_proxy_from(
+ {
+ PRIMARY_BASE_URL_ENV: "http://primary:4000",
+ LEGACY_API_KEY_ENV: "sk-legacy",
+ }
+ )
+ assert cfg == ProxyConfig("http://primary:4000", "sk-legacy")
+
+
+def test_empty_string_env_is_treated_as_unset() -> None:
+ """``os.environ.get`` on an exported-but-empty var returns "" which
+ is falsy. The resolver must treat that as unset so a shell that
+ accidentally exports ``LITELLM_PROXY_URL=`` doesn't turn into a
+ "" base_url that hits the wrong endpoint."""
+ assert (
+ resolve_proxy_from(
+ {PRIMARY_BASE_URL_ENV: "", PRIMARY_API_KEY_ENV: "sk-1234"}
+ )
+ is None
+ )
+
+
+def test_require_proxy_fails_with_helpful_message_when_env_empty() -> None:
+ """The error the user sees must name BOTH the primary and legacy
+ env vars — otherwise they can't tell why the test is failing when
+ they only set the legacy pair, or vice versa."""
+ compat = _CompatResultStub()
+ with pytest.raises(pytest.fail.Exception) as excinfo:
+ require_proxy(compat, env={})
+ assert PRIMARY_BASE_URL_ENV in str(excinfo.value)
+ assert PRIMARY_API_KEY_ENV in str(excinfo.value)
+ assert compat.calls and compat.calls[0]["status"] == "fail"
+ assert LEGACY_BASE_URL_ENV in compat.calls[0]["error"]
+
+
+def test_require_proxy_returns_config_when_primary_env_supplied() -> None:
+ cfg = require_proxy(
+ _CompatResultStub(),
+ env={
+ PRIMARY_BASE_URL_ENV: "http://localhost:4000",
+ PRIMARY_API_KEY_ENV: "sk-1234",
+ },
+ )
+ assert cfg == ProxyConfig("http://localhost:4000", "sk-1234")
+
+
+def test_require_proxy_returns_config_when_only_legacy_env_supplied() -> None:
+ cfg = require_proxy(
+ _CompatResultStub(),
+ env={
+ LEGACY_BASE_URL_ENV: "http://legacy:4000",
+ LEGACY_API_KEY_ENV: "sk-legacy",
+ },
+ )
+ assert cfg == ProxyConfig("http://legacy:4000", "sk-legacy")
+
+
+class TestControlGatewayFollowsResolvedProxy:
+ """The session fixture that registers the compat deployments must talk
+ to the *same* proxy the cells do.
+
+ It turns on whenever ``resolve_proxy()`` succeeds, which includes the
+ legacy-only spelling. Building its Gateway off ``e2e_config``'s own env
+ read instead would send ``/model/new`` to http://localhost:4000 with
+ sk-1234 (that module only knows the primary names), while the cells drive
+ the legacy host — so registration silently lands somewhere else and every
+ cell 400s with "Invalid model name" against a proxy that looks configured.
+ """
+
+ LEGACY = ProxyConfig("http://legacy-alb.internal:4000", "sk-legacy")
+
+ def _gateway(self):
+ from claude_code.conftest import _build_control_gateway
+
+ return _build_control_gateway(self.LEGACY)
+
+ def test_management_calls_go_to_the_resolved_host_and_key(self) -> None:
+ control = self._gateway().transport.control
+ assert control.base_url == self.LEGACY.base_url
+ assert control.master_key == self.LEGACY.api_key
+
+ def test_both_planes_share_the_one_address_the_cells_use(self) -> None:
+ """The deployment is fronted by a single address that routes
+ management and LLM paths itself, so a resolved proxy pins both."""
+ transport = self._gateway().transport
+ assert transport.data.base_url == self.LEGACY.base_url
+ assert transport.data.master_key == self.LEGACY.api_key
+ assert transport.control.base_url == transport.data.base_url
+
+
+def test_require_proxy_leaves_compat_result_untouched_on_success() -> None:
+ """A successful resolution must NOT append a spurious fail entry.
+ Would have silently poisoned every compat cell's result rows."""
+ compat = _CompatResultStub()
+ require_proxy(
+ compat,
+ env={
+ PRIMARY_BASE_URL_ENV: "http://localhost:4000",
+ PRIMARY_API_KEY_ENV: "sk-1234",
+ },
+ )
+ assert compat.calls == []
diff --git a/tests/e2e/claude_code/_publisher_unit_tests/__init__.py b/tests/e2e/claude_code/_publisher_unit_tests/__init__.py
deleted file mode 100644
index e69de29bb2d1..000000000000
diff --git a/tests/e2e/claude_code/_publisher_unit_tests/test_run_daily_pytest_scrubs_env.py b/tests/e2e/claude_code/_publisher_unit_tests/test_run_daily_pytest_scrubs_env.py
deleted file mode 100644
index 418d308674b1..000000000000
--- a/tests/e2e/claude_code/_publisher_unit_tests/test_run_daily_pytest_scrubs_env.py
+++ /dev/null
@@ -1,95 +0,0 @@
-"""Pin: the cron `pytest` invocation must run under `env -i`.
-
-The systemd service `litellm-compat-matrix.service` loads provider
-credentials (`ANTHROPIC_API_KEY`, `AWS_BEARER_TOKEN_BEDROCK`,
-`AZURE_FOUNDRY_API_KEY`, `VERTEXAI_*`) and the agent-shin GitHub token
-(`AGENT_SHIN_GITHUB_TOKEN`) into `run_daily.sh`'s environment from
-`/etc/litellm-compat-matrix.env`. Pytest only needs to talk to the
-loopback proxy at `127.0.0.1:${PROXY_PORT}` and has no legitimate reason
-to see provider creds in its own `os.environ`. Leaving them in would
-let a test under `tests/e2e/claude_code/` read them via `os.environ` and
-exfiltrate them, and would also let a model-directed `Read` tool call
-during a PDF/vision cell reach `/proc//environ`. The
-PR-gate's pytest step in `.circleci/config.yml` already runs under
-`env -i`; this pin enforces the same scrub on the cron path.
-"""
-
-from __future__ import annotations
-
-from pathlib import Path
-
-REPO_ROOT = Path(__file__).resolve().parents[4]
-RUN_DAILY = REPO_ROOT / "tests" / "e2e" / "claude_code" / "cron_vm" / "run_daily.sh"
-
-
-def _pytest_invocation_block() -> str:
- """Return only the executable lines around the pytest invocation.
-
- Comment text in run_daily.sh explains *why* certain credential
- names must not appear, so a naïve substring scan over the whole
- region would false-positive on the rationale itself. Strip lines
- whose first non-space character is `#`.
- """
- body = RUN_DAILY.read_text()
- start = body.index('log "running pytest"')
- end = body.index("PYTEST_EXIT=$?", start)
- return "\n".join(
- line for line in body[start:end].splitlines()
- if line.lstrip()[:1] != "#"
- )
-
-
-def test_pytest_invocation_wraps_in_env_i() -> None:
- block = _pytest_invocation_block()
- assert "env -i" in block, (
- "run_daily.sh: the pytest invocation must run under `env -i` so "
- "PR-controlled test code under tests/e2e/claude_code/ cannot read "
- "provider/agent-shin credentials out of the systemd service "
- "environment, and so a model-directed `Read` tool call cannot "
- "reach /proc//environ to pull them out."
- )
- assert block.index("env -i") < block.index('"${WORKTREE_UV}" run pytest'), (
- "run_daily.sh: `env -i` must precede the pytest invocation; "
- "otherwise pytest inherits the full credential-bearing env."
- )
-
-
-def test_pytest_invocation_env_i_excludes_provider_secrets() -> None:
- block = _pytest_invocation_block()
- for forbidden in (
- "ANTHROPIC_API_KEY",
- "AWS_BEARER_TOKEN_BEDROCK",
- "AWS_ACCESS_KEY_ID",
- "AWS_SECRET_ACCESS_KEY",
- "VERTEXAI_CREDENTIALS",
- "VERTEXAI_PROJECT",
- "VERTEXAI_LOCATION",
- "AZURE_FOUNDRY_API_KEY",
- "AZURE_FOUNDRY_API_BASE",
- "GITHUB_TOKEN",
- "AGENT_SHIN_GITHUB_TOKEN",
- ):
- assert forbidden not in block, (
- f"run_daily.sh: the pytest-step `env -i` allowlist must not "
- f"pass {forbidden} through. Found it inside the pytest "
- f"invocation block."
- )
-
-
-def test_pytest_invocation_passes_proxy_url_and_key_explicitly() -> None:
- block = _pytest_invocation_block()
- assert "LITELLM_PROXY_BASE_URL=" in block, (
- "run_daily.sh: the pytest `env -i` block must still pass "
- "LITELLM_PROXY_BASE_URL so the test suite knows where to find "
- "the loopback proxy."
- )
- assert "LITELLM_PROXY_API_KEY=" in block, (
- "run_daily.sh: the pytest `env -i` block must still pass "
- "LITELLM_PROXY_API_KEY so the test suite can authenticate to "
- "the loopback proxy."
- )
- assert "COMPAT_RESULTS_PATH=" in block, (
- "run_daily.sh: the pytest `env -i` block must still pass "
- "COMPAT_RESULTS_PATH so the conftest writes the per-cell "
- "tagged-union artifact to the script-managed path."
- )
diff --git a/tests/e2e/claude_code/_publisher_unit_tests/test_run_daily_release_pagination.py b/tests/e2e/claude_code/_publisher_unit_tests/test_run_daily_release_pagination.py
deleted file mode 100644
index fc733845ba3b..000000000000
--- a/tests/e2e/claude_code/_publisher_unit_tests/test_run_daily_release_pagination.py
+++ /dev/null
@@ -1,289 +0,0 @@
-"""Regression tests for the GitHub release pagination in `run_daily.sh`.
-
-The cron job resolves "newest LiteLLM v*-stable" via the GitHub Releases
-API. A previous version of the loop broke as soon as the current page
-contained ANY v*-stable tag. The Releases endpoint orders by
-`created_at`, NOT by semver, so a backport on an older series cut today
-(e.g. v1.80.1-stable) can land on an earlier page than a higher-version
-release cut two weeks ago (e.g. v1.83.0-stable). The early-break would
-silently pin the cron to a stale tag because the higher-version release
-on a later page never made it into the merged set the final `sort_by`
-consumed.
-
-These tests pin two things:
-
- 1. The buggy early-break-on-first-stable pattern must not return.
- 2. The loop still terminates early on the standard "empty page" guard
- so a quiet release feed doesn't burn API quota.
-
-The shell loop itself is exercised end-to-end with a fake `curl` that
-serves canned page JSON, demonstrating that the resolved tag is the
-highest-semver stable across all pages even when the highest tag lives
-on page 2+.
-"""
-
-from __future__ import annotations
-
-import os
-import shutil
-import subprocess
-import textwrap
-from pathlib import Path
-
-import pytest
-
-REPO_ROOT = Path(__file__).resolve().parents[4]
-RUN_DAILY = REPO_ROOT / "tests" / "e2e" / "claude_code" / "cron_vm" / "run_daily.sh"
-
-# The extracted snippet starts AFTER `log`/`die` are defined in run_daily.sh,
-# so the test harness has to provide its own stubs. Without them, a failure
-# inside the snippet (e.g. jq returning an empty LITELLM_VERSION) would crash
-# with `bash: die: command not found` (exit 127) instead of the intended
-# diagnostic, making test failures unnecessarily hard to debug.
-_PREAMBLE = (
- "set -Eeuo pipefail\n"
- "log() { printf '==> %s\\n' \"$*\" >&2; }\n"
- "die() { printf 'ERROR: %s\\n' \"$*\" >&2; exit 1; }\n"
-)
-
-
-def test_run_daily_does_not_early_break_on_first_stable_page() -> None:
- """The regex pattern `select(test("...stable$"))] | length > 0` followed
- by `break` is exactly the buggy early-stop. If it ever returns the
- cron will silently start testing against a stale stable tag.
- """
- body = RUN_DAILY.read_text()
- assert (
- "length > 0" not in body
- or "break" not in body
- or (
- # If both substrings exist, make sure they aren't both inside the
- # same release-pagination loop. The current loop only contains
- # a `break` for the empty-page guard, not for any "length > 0"
- # condition.
- not _shares_loop_body(body, "length > 0", "break")
- )
- ), (
- "run_daily.sh contains the old early-break-on-stable pattern. The "
- "Releases endpoint orders by created_at, not semver, so breaking "
- "on first-stable-seen can miss higher-versioned releases sitting "
- "on later pages."
- )
-
-
-def _shares_loop_body(body: str, needle_a: str, needle_b: str) -> bool:
- """Heuristic: do both needles live inside a `for page in ...; do ... done`
- block? Used as a defensive guard for the static check above."""
- in_loop = False
- saw_a = False
- saw_b = False
- for line in body.splitlines():
- stripped = line.strip()
- if stripped.startswith("for page in"):
- in_loop = True
- saw_a = False
- saw_b = False
- continue
- if in_loop and stripped == "done":
- if saw_a and saw_b:
- return True
- in_loop = False
- continue
- if in_loop:
- if needle_a in line:
- saw_a = True
- if needle_b in line:
- saw_b = True
- return False
-
-
-def test_run_daily_keeps_empty_page_break_guard() -> None:
- """The empty-page break is the only break that should remain in the
- pagination loop — without it a quiet release feed wastes API quota
- walking past the last real page."""
- body = RUN_DAILY.read_text()
- assert "jq 'length' \"${PAGE_JSON}\"" in body, (
- "run_daily.sh must still detect empty pages via `jq 'length' "
- "${PAGE_JSON}`; without this the loop walks the full 5-page cap "
- "even when there are no more releases."
- )
- assert (
- '== "0"' in body
- ), 'The empty-page guard must compare jq\'s length output to "0".'
-
-
-def _make_fake_curl(scratch: Path, pages: dict[int, str]) -> Path:
- """Build a fake `curl` shim that serves the canned page JSON for
- each `page=N` request and an empty array for any page past the
- last canned one.
-
- The shim mimics just enough of curl's CLI surface for the cron
- script: it accepts the headers + URL we pass, ignores everything
- we don't need, and writes the canned body to either stdout or the
- --output target if one is given.
- """
- pages_dir = scratch / "pages"
- pages_dir.mkdir()
- for page_num, body in pages.items():
- (pages_dir / f"page{page_num}.json").write_text(body)
-
- curl_path = scratch / "curl"
- curl_path.write_text(
- textwrap.dedent(
- f"""\
- #!/usr/bin/env bash
- # Fake curl for run_daily.sh release pagination tests. Serves
- # page JSON from {pages_dir} keyed by the `page=` query value,
- # and returns "[]" for pages past the last canned one (which
- # is exactly how the real GitHub API behaves past the end).
- url=""
- output=""
- while [[ $# -gt 0 ]]; do
- case "$1" in
- -fsS|-fsSL|-H|-o|--output)
- if [[ "$1" == "-o" || "$1" == "--output" ]]; then
- output="$2"; shift 2
- elif [[ "$1" == "-H" ]]; then
- shift 2
- else
- shift
- fi
- ;;
- http*)
- url="$1"; shift
- ;;
- *)
- shift
- ;;
- esac
- done
- page="$(printf '%s' "$url" | sed -n 's/.*[?&]page=\\([0-9]*\\).*/\\1/p')"
- [[ -z "$page" ]] && page=1
- file="{pages_dir}/page${{page}}.json"
- if [[ -f "$file" ]]; then
- if [[ -n "$output" ]]; then cp "$file" "$output"; else cat "$file"; fi
- else
- if [[ -n "$output" ]]; then printf '[]' > "$output"; else printf '[]'; fi
- fi
- """
- )
- )
- curl_path.chmod(0o755)
- return curl_path
-
-
-def _extract_resolution_snippet() -> str:
- """Pull the pagination + sort_by + assignment block out of run_daily.sh
- so the test exercises the actual production code path (not a copy).
-
- The block is everything from the GH_AUTH_HEADER setup down through
- the LITELLM_VERSION emission.
- """
- body = RUN_DAILY.read_text()
- start = body.index("GH_AUTH_HEADER=()")
- end = body.index('log "resolved litellm:')
- return body[start:end]
-
-
-@pytest.mark.skipif(shutil.which("jq") is None, reason="jq not available")
-def test_run_daily_resolves_highest_semver_across_pages(tmp_path: Path) -> None:
- """End-to-end: drive the actual run_daily.sh pagination loop with a
- fake curl whose page 1 contains a freshly-cut LOW-version backport
- (v1.80.1-stable) and page 2 contains a two-weeks-old HIGH-version
- release (v1.83.0-stable). The correct behavior is to resolve
- v1.83.0-stable. The pre-fix behavior would resolve v1.80.1-stable
- because the early-break consumed only page 1.
- """
- pages = {
- # Page 1: most-recently-created releases. The order here matches
- # what /releases?page=1 returns: created-at descending. The
- # freshly-cut v1.80.1-stable backport sits at the top, plus a
- # bunch of non-stable releases.
- 1: """[
- {"tag_name": "v1.84.0-nightly.1"},
- {"tag_name": "v1.80.1-stable"},
- {"tag_name": "v1.84.0-nightly.0"}
- ]""",
- # Page 2: older releases. The HIGHER-version stable lives here
- # because it was cut two weeks ago, before the v1.80.1 backport.
- 2: """[
- {"tag_name": "v1.83.0-rc.5"},
- {"tag_name": "v1.83.0-stable"},
- {"tag_name": "v1.82.4-stable"}
- ]""",
- # Page 3+: empty -> the loop's empty-page guard fires here.
- }
- fake_curl_dir = tmp_path / "shim"
- fake_curl_dir.mkdir()
- _make_fake_curl(fake_curl_dir, pages)
-
- workdir = tmp_path / "work"
- workdir.mkdir()
-
- snippet = _extract_resolution_snippet()
- script = (
- _PREAMBLE
- + f"WORKDIR={workdir!s}\n"
- + snippet
- + 'printf "%s" "${LITELLM_VERSION}"\n'
- )
-
- env = {
- **os.environ,
- "PATH": f"{fake_curl_dir}:{os.environ.get('PATH', '')}",
- }
- # Make sure the loop hits the fake curl, not the system one.
- env.pop("GITHUB_TOKEN", None)
- result = subprocess.run(
- ["bash", "-c", script],
- capture_output=True,
- text=True,
- env=env,
- check=True,
- )
- assert result.stdout == "v1.83.0-stable", (
- f"Expected the highest-semver stable across pages 1-2, got "
- f"{result.stdout!r}. stderr={result.stderr!r}"
- )
-
-
-@pytest.mark.skipif(shutil.which("jq") is None, reason="jq not available")
-def test_run_daily_terminates_on_empty_page(tmp_path: Path) -> None:
- """The empty-page guard must fire so we don't always walk all 5
- pages. With a single populated page and an empty page 2 we should
- stop after fetching page 2 (the first empty response)."""
- pages = {1: '[{"tag_name": "v1.50.0-stable"}]'}
- fake_curl_dir = tmp_path / "shim"
- fake_curl_dir.mkdir()
- _make_fake_curl(fake_curl_dir, pages)
-
- workdir = tmp_path / "work"
- workdir.mkdir()
-
- snippet = _extract_resolution_snippet()
- script = (
- _PREAMBLE
- + f"WORKDIR={workdir!s}\n"
- + snippet
- + 'printf "%s" "${LITELLM_VERSION}"\n'
- )
-
- env = {
- **os.environ,
- "PATH": f"{tmp_path}/shim:{os.environ.get('PATH', '')}",
- }
- env.pop("GITHUB_TOKEN", None)
- result = subprocess.run(
- ["bash", "-c", script],
- capture_output=True,
- text=True,
- env=env,
- check=True,
- )
- assert result.stdout == "v1.50.0-stable"
- # Only pages 1 and 2 should have been fetched (2 is empty -> break).
- assert (workdir / "releases.page2.json").exists()
- assert not (workdir / "releases.page3.json").exists(), (
- "Empty-page guard didn't fire — the loop kept walking past the "
- "first empty response."
- )
diff --git a/tests/e2e/claude_code/_publisher_unit_tests/test_run_daily_version_probe_scrubs_env.py b/tests/e2e/claude_code/_publisher_unit_tests/test_run_daily_version_probe_scrubs_env.py
deleted file mode 100644
index 1c3959764f34..000000000000
--- a/tests/e2e/claude_code/_publisher_unit_tests/test_run_daily_version_probe_scrubs_env.py
+++ /dev/null
@@ -1,92 +0,0 @@
-"""Pin: the cron `claude --version` probe must run under `env -i`.
-
-The systemd service `litellm-compat-matrix.service` loads provider
-credentials (`ANTHROPIC_API_KEY`, `AWS_BEARER_TOKEN_BEDROCK`,
-`AZURE_FOUNDRY_API_KEY`) and the agent-shin GitHub token
-(`AGENT_SHIN_GITHUB_TOKEN`) into `run_daily.sh`'s environment from
-`/etc/litellm-compat-matrix.env`. Running the npm-installed `claude`
-binary directly there would hand that full env to package code, so a
-compromised `@anthropic-ai/claude-code` release could read those
-secrets out of `os.environ` before the proxy or test harness ever
-starts. The version probe must be wrapped in `env -i` with a minimal
-PATH/HOME/USER/TERM/LANG/LC_ALL/TMPDIR allowlist — matching the
-PR-gate's resolver/npm-install/pytest scrubs.
-"""
-
-from __future__ import annotations
-
-from pathlib import Path
-
-REPO_ROOT = Path(__file__).resolve().parents[4]
-RUN_DAILY = REPO_ROOT / "tests" / "e2e" / "claude_code" / "cron_vm" / "run_daily.sh"
-
-
-def _version_probe_block() -> str:
- body = RUN_DAILY.read_text()
- start = body.index("CLAUDE_CODE_VERSION=")
- end = body.index('[[ -n "${CLAUDE_CODE_VERSION}" ]]', start)
- return body[start:end]
-
-
-def test_version_probe_wraps_claude_in_env_i() -> None:
- block = _version_probe_block()
- assert "env -i" in block, (
- "run_daily.sh: the `claude --version` probe must run under "
- "`env -i` so a compromised @anthropic-ai/claude-code package "
- "cannot read provider/GitHub credentials out of the systemd "
- "service environment."
- )
- assert block.index("env -i") < block.index("claude --version"), (
- "run_daily.sh: `env -i` must precede `claude --version`; "
- "otherwise the binary inherits the full credential-bearing env."
- )
-
-
-def test_version_probe_env_i_excludes_provider_secrets() -> None:
- block = _version_probe_block()
- for forbidden in (
- "ANTHROPIC_API_KEY",
- "AWS_BEARER_TOKEN_BEDROCK",
- "AWS_ACCESS_KEY_ID",
- "AWS_SECRET_ACCESS_KEY",
- "VERTEXAI_CREDENTIALS",
- "AZURE_FOUNDRY_API_KEY",
- "GITHUB_TOKEN",
- "AGENT_SHIN_GITHUB_TOKEN",
- ):
- assert forbidden not in block, (
- f"run_daily.sh: the version-probe `env -i` allowlist must "
- f"not pass {forbidden} through. Found it inside the probe "
- f"block."
- )
-
-
-def test_version_probe_uses_isolated_home_not_runtime_user_home() -> None:
- """Pin: the `claude --version` probe runs under a fresh empty HOME.
-
- `ProtectHome=read-only` in the systemd unit allows reads of the
- runtime user's real home directory. If the probe's `env -i`
- block forwards `HOME=${HOME}`, a compromised `claude` package
- can `os.path.expanduser("~/.config/gh/hosts.yml")` or
- `os.path.expanduser("~/.ssh/...")` and exfiltrate the contents
- before the proxy or test harness ever starts. The probe must
- point HOME at a per-run tmpdir under `${WORKDIR}` so the CLI
- sees an empty HOME instead.
- """
- block = _version_probe_block()
- body = RUN_DAILY.read_text()
-
- assert "CLAUDE_PROBE_HOME=" in body, (
- "run_daily.sh: must define a `CLAUDE_PROBE_HOME` per-run tmpdir "
- "for the `claude --version` probe so the CLI never sees the "
- "runtime user's real $HOME."
- )
- assert 'HOME="${CLAUDE_PROBE_HOME}"' in block, (
- "run_daily.sh: the probe's `env -i` block must set HOME to "
- "the per-run isolated tmpdir, not to the runtime user's $HOME."
- )
- assert 'HOME="${HOME}"' not in block, (
- "run_daily.sh: the probe's `env -i` block must not forward the "
- "runtime user's $HOME to `claude --version`. Use the isolated "
- "$CLAUDE_PROBE_HOME tmpdir instead."
- )
diff --git a/tests/e2e/claude_code/_publisher_unit_tests/test_systemd_unit_credential_isolation.py b/tests/e2e/claude_code/_publisher_unit_tests/test_systemd_unit_credential_isolation.py
deleted file mode 100644
index 12edce3cb505..000000000000
--- a/tests/e2e/claude_code/_publisher_unit_tests/test_systemd_unit_credential_isolation.py
+++ /dev/null
@@ -1,104 +0,0 @@
-"""Pin: the cron systemd unit hides credential-bearing dotdirs.
-
-`ProtectHome=read-only` blocks writes to /home/mateo but still allows
-reads. A model-directed `Read` tool call (the PDF cells pass
-`--allowed-tools Read` to the `claude` CLI) or a compromised
-`@anthropic-ai/claude-code` package can read absolute paths under
-the runtime user's home and exfiltrate the contents — even with the
-per-`claude`-invocation HOME isolation in place, because absolute
-paths bypass `~`-expansion.
-
-This file pins the second line of defense: the systemd unit lists
-the credential-bearing dotdirs (`~/.config/gh`, `~/.ssh`, `~/.aws`,
-`~/.docker`, `~/.kube`, `~/.gnupg`) under `InaccessiblePaths=` so
-the kernel hides them from every process in the unit's mount
-namespace, including any child of `claude --version` or the pytest
-run. It also pins that `~/.config/gh` is *not* in `ReadWritePaths=`
-— we pass `GH_TOKEN` inline to every `gh` invocation in
-`run_daily.sh`, so the host gh-cli config is unused.
-"""
-
-from __future__ import annotations
-
-import re
-from pathlib import Path
-
-REPO_ROOT = Path(__file__).resolve().parents[4]
-SERVICE = (
- REPO_ROOT / "tests" / "e2e" / "claude_code" / "cron_vm" / "litellm-compat-matrix.service"
-)
-
-
-def _service_text() -> str:
- return SERVICE.read_text()
-
-
-def _directive(name: str) -> str:
- """Return the value of a single-line systemd directive (or empty)."""
- text = _service_text()
- match = re.search(rf"^\s*{re.escape(name)}\s*=\s*(.*)$", text, re.MULTILINE)
- return match.group(1).strip() if match else ""
-
-
-def test_inaccessible_paths_hides_credential_dotdirs() -> None:
- """Every credential-bearing dotdir must be under `InaccessiblePaths=`."""
- inaccessible = _directive("InaccessiblePaths")
- assert inaccessible, (
- "litellm-compat-matrix.service: must declare `InaccessiblePaths=` "
- "to hide credential dotdirs from the `claude` subprocess and the "
- "model-directed Read tool. Without this, an absolute-path read "
- "like `Read('/home/mateo/.config/gh/hosts.yml')` exfiltrates "
- "the gh-cli token despite the per-invocation HOME isolation."
- )
- for path in (
- "/home/mateo/.config/gh",
- "/home/mateo/.ssh",
- "/home/mateo/.aws",
- "/home/mateo/.docker",
- "/home/mateo/.kube",
- "/home/mateo/.gnupg",
- ):
- # Tolerated `-` prefix means "ignore if missing on host".
- assert path in inaccessible, (
- f"litellm-compat-matrix.service: `{path}` must appear in "
- f"`InaccessiblePaths=` so the cron `claude` subprocess can "
- f"never read it (even via an absolute path that bypasses "
- f"the per-invocation HOME override)."
- )
-
-
-def test_gh_config_is_not_writeable() -> None:
- """`~/.config/gh` is not whitelisted under `ReadWritePaths=`.
-
- We pass `GH_TOKEN` inline to every `gh` invocation in
- `run_daily.sh` (`gh repo clone`, `gh pr create`, `gh pr edit`).
- The host `~/.config/gh/hosts.yml` is therefore never consulted
- or written to. Keeping it out of `ReadWritePaths=` is the second
- line of defense: a future regression that drops the inline-token
- convention will fail loudly (gh writes a new login config and
- hits a read-only filesystem) rather than silently re-introduce
- the credential exfiltration surface that
- `InaccessiblePaths=/home/mateo/.config/gh` is closing.
- """
- rw = _directive("ReadWritePaths")
- assert ".config/gh" not in rw, (
- "litellm-compat-matrix.service: `/home/mateo/.config/gh` must "
- "*not* appear in `ReadWritePaths=`. We pass `GH_TOKEN` inline "
- "to every `gh` invocation in run_daily.sh, so the host gh-cli "
- "config is never consulted or written to. Keeping the path out "
- "of ReadWritePaths means a future regression that drops the "
- "inline-token convention will fail loudly instead of silently "
- "re-opening the credential exfiltration surface that "
- "`InaccessiblePaths=` is closing."
- )
-
-
-def test_protect_home_is_read_only_or_stricter() -> None:
- """`ProtectHome=` must be at least `read-only`."""
- value = _directive("ProtectHome")
- assert value in ("read-only", "tmpfs", "yes", "true"), (
- f"litellm-compat-matrix.service: `ProtectHome=` must be `read-only`, "
- f"`tmpfs`, or `yes`. Got: {value!r}. Without this, the unit can "
- f"write anywhere under /home/mateo, including overwriting "
- f"~/.config/gh/hosts.yml."
- )
diff --git a/tests/e2e/claude_code/basic_messaging_non_streaming/test_anthropic.py b/tests/e2e/claude_code/basic_messaging_non_streaming/test_anthropic.py
index c06fff28d2d5..21383b85da58 100644
--- a/tests/e2e/claude_code/basic_messaging_non_streaming/test_anthropic.py
+++ b/tests/e2e/claude_code/basic_messaging_non_streaming/test_anthropic.py
@@ -20,6 +20,7 @@
from __future__ import annotations
+import pytest
from claude_code._basic_messaging import run_basic_messaging_cell
# Per the PRD: each cell is exercised against three Claude tiers via the
@@ -27,11 +28,12 @@
# routing config; the driver only sends the alias.
ANTHROPIC_MODELS = [
"claude-haiku-4-5",
- "claude-sonnet-4-6",
+ "claude-sonnet-4-5",
"claude-opus-4-7",
]
+@pytest.mark.covers("llm.messages.anthropic.basic.nonstream.works")
def test_basic_messaging_non_streaming_anthropic(compat_result):
"""Drive the `claude` CLI against the LiteLLM proxy and assert a reply.
diff --git a/tests/e2e/claude_code/basic_messaging_non_streaming/test_azure.py b/tests/e2e/claude_code/basic_messaging_non_streaming/test_azure.py
index 2a962b244a86..19e88dbe3cba 100644
--- a/tests/e2e/claude_code/basic_messaging_non_streaming/test_azure.py
+++ b/tests/e2e/claude_code/basic_messaging_non_streaming/test_azure.py
@@ -25,6 +25,7 @@
from __future__ import annotations
+import pytest
from claude_code._basic_messaging import run_basic_messaging_cell
# Per-model aliases registered in the LiteLLM proxy's routing config to
@@ -33,11 +34,12 @@
# resource URL and API key.
AZURE_MODELS = [
"claude-haiku-4-5-azure",
- "claude-sonnet-4-6-azure",
+ "claude-sonnet-4-5-azure",
"claude-opus-4-7-azure",
]
+@pytest.mark.covers("llm.messages.azure_foundry.basic.nonstream.works")
def test_basic_messaging_non_streaming_azure(compat_result):
"""Drive the `claude` CLI against the LiteLLM proxy and assert a reply.
diff --git a/tests/e2e/claude_code/basic_messaging_non_streaming/test_bedrock_converse.py b/tests/e2e/claude_code/basic_messaging_non_streaming/test_bedrock_converse.py
index 2245ed7417a6..2b0f49bc2050 100644
--- a/tests/e2e/claude_code/basic_messaging_non_streaming/test_bedrock_converse.py
+++ b/tests/e2e/claude_code/basic_messaging_non_streaming/test_bedrock_converse.py
@@ -20,6 +20,7 @@
from __future__ import annotations
+import pytest
from claude_code._basic_messaging import run_basic_messaging_cell
# Per-model aliases registered in the LiteLLM proxy's routing config to
@@ -28,11 +29,12 @@
# strategy.
BEDROCK_CONVERSE_MODELS = [
"claude-haiku-4-5-bedrock-converse",
- "claude-sonnet-4-6-bedrock-converse",
+ "claude-sonnet-4-5-bedrock-converse",
"claude-opus-4-7-bedrock-converse",
]
+@pytest.mark.covers("llm.messages.bedrock_converse.basic.nonstream.works")
def test_basic_messaging_non_streaming_bedrock_converse(compat_result):
"""Drive the `claude` CLI against the LiteLLM proxy and assert a reply."""
run_basic_messaging_cell(
diff --git a/tests/e2e/claude_code/basic_messaging_non_streaming/test_bedrock_invoke.py b/tests/e2e/claude_code/basic_messaging_non_streaming/test_bedrock_invoke.py
index e0a6e77f3c12..937ea5ee27e5 100644
--- a/tests/e2e/claude_code/basic_messaging_non_streaming/test_bedrock_invoke.py
+++ b/tests/e2e/claude_code/basic_messaging_non_streaming/test_bedrock_invoke.py
@@ -20,6 +20,7 @@
from __future__ import annotations
+import pytest
from claude_code._basic_messaging import run_basic_messaging_cell
# Per-model aliases registered in the LiteLLM proxy's routing config to
@@ -28,11 +29,12 @@
# routing strategy.
BEDROCK_INVOKE_MODELS = [
"claude-haiku-4-5-bedrock-invoke",
- "claude-sonnet-4-6-bedrock-invoke",
+ "claude-sonnet-4-5-bedrock-invoke",
"claude-opus-4-7-bedrock-invoke",
]
+@pytest.mark.covers("llm.messages.bedrock_invoke.basic.nonstream.works")
def test_basic_messaging_non_streaming_bedrock_invoke(compat_result):
"""Drive the `claude` CLI against the LiteLLM proxy and assert a reply."""
run_basic_messaging_cell(
diff --git a/tests/e2e/claude_code/basic_messaging_non_streaming/test_vertex_ai.py b/tests/e2e/claude_code/basic_messaging_non_streaming/test_vertex_ai.py
index e4e2a39e6cd5..c46e5a8f762e 100644
--- a/tests/e2e/claude_code/basic_messaging_non_streaming/test_vertex_ai.py
+++ b/tests/e2e/claude_code/basic_messaging_non_streaming/test_vertex_ai.py
@@ -20,6 +20,7 @@
from __future__ import annotations
+import pytest
from claude_code._basic_messaging import run_basic_messaging_cell
# Per-model aliases registered in the LiteLLM proxy's routing config to
@@ -28,11 +29,12 @@
# model id and the GCP region.
VERTEX_AI_MODELS = [
"claude-haiku-4-5-vertex",
- "claude-sonnet-4-6-vertex",
+ "claude-sonnet-4-5-vertex",
"claude-opus-4-7-vertex",
]
+@pytest.mark.covers("llm.messages.vertex.basic.nonstream.works")
def test_basic_messaging_non_streaming_vertex_ai(compat_result):
"""Drive the `claude` CLI against the LiteLLM proxy and assert a reply."""
run_basic_messaging_cell(
diff --git a/tests/e2e/claude_code/basic_messaging_streaming/test_anthropic.py b/tests/e2e/claude_code/basic_messaging_streaming/test_anthropic.py
index 56e3fb6c1818..ce453f3e523c 100644
--- a/tests/e2e/claude_code/basic_messaging_streaming/test_anthropic.py
+++ b/tests/e2e/claude_code/basic_messaging_streaming/test_anthropic.py
@@ -25,15 +25,17 @@
from __future__ import annotations
+import pytest
from claude_code._basic_messaging import run_basic_messaging_cell
ANTHROPIC_MODELS = [
"claude-haiku-4-5",
- "claude-sonnet-4-6",
+ "claude-sonnet-4-5",
"claude-opus-4-7",
]
+@pytest.mark.covers("llm.messages.anthropic.basic.stream.works")
def test_basic_messaging_streaming_anthropic(compat_result):
"""Drive the `claude` CLI against the LiteLLM proxy and assert a
non-empty streamed reply (one row per Claude tier).
diff --git a/tests/e2e/claude_code/basic_messaging_streaming/test_azure.py b/tests/e2e/claude_code/basic_messaging_streaming/test_azure.py
index b6c002d0b279..3307194e8624 100644
--- a/tests/e2e/claude_code/basic_messaging_streaming/test_azure.py
+++ b/tests/e2e/claude_code/basic_messaging_streaming/test_azure.py
@@ -19,15 +19,17 @@
from __future__ import annotations
+import pytest
from claude_code._basic_messaging import run_basic_messaging_cell
AZURE_MODELS = [
"claude-haiku-4-5-azure",
- "claude-sonnet-4-6-azure",
+ "claude-sonnet-4-5-azure",
"claude-opus-4-7-azure",
]
+@pytest.mark.covers("llm.messages.azure_foundry.basic.stream.works")
def test_basic_messaging_streaming_azure(compat_result):
"""Drive the `claude` CLI against the LiteLLM proxy and assert a
non-empty streamed reply (one row per Claude tier).
diff --git a/tests/e2e/claude_code/basic_messaging_streaming/test_bedrock_converse.py b/tests/e2e/claude_code/basic_messaging_streaming/test_bedrock_converse.py
index 44ac54515f01..a8bc0b77a5d0 100644
--- a/tests/e2e/claude_code/basic_messaging_streaming/test_bedrock_converse.py
+++ b/tests/e2e/claude_code/basic_messaging_streaming/test_bedrock_converse.py
@@ -15,15 +15,17 @@
from __future__ import annotations
+import pytest
from claude_code._basic_messaging import run_basic_messaging_cell
BEDROCK_CONVERSE_MODELS = [
"claude-haiku-4-5-bedrock-converse",
- "claude-sonnet-4-6-bedrock-converse",
+ "claude-sonnet-4-5-bedrock-converse",
"claude-opus-4-7-bedrock-converse",
]
+@pytest.mark.covers("llm.messages.bedrock_converse.basic.stream.works")
def test_basic_messaging_streaming_bedrock_converse(compat_result):
"""Drive the `claude` CLI against the LiteLLM proxy and assert a
non-empty streamed reply (one row per Claude tier).
diff --git a/tests/e2e/claude_code/basic_messaging_streaming/test_bedrock_invoke.py b/tests/e2e/claude_code/basic_messaging_streaming/test_bedrock_invoke.py
index 1d59d16cdc1c..c0ece0e07217 100644
--- a/tests/e2e/claude_code/basic_messaging_streaming/test_bedrock_invoke.py
+++ b/tests/e2e/claude_code/basic_messaging_streaming/test_bedrock_invoke.py
@@ -15,15 +15,17 @@
from __future__ import annotations
+import pytest
from claude_code._basic_messaging import run_basic_messaging_cell
BEDROCK_INVOKE_MODELS = [
"claude-haiku-4-5-bedrock-invoke",
- "claude-sonnet-4-6-bedrock-invoke",
+ "claude-sonnet-4-5-bedrock-invoke",
"claude-opus-4-7-bedrock-invoke",
]
+@pytest.mark.covers("llm.messages.bedrock_invoke.basic.stream.works")
def test_basic_messaging_streaming_bedrock_invoke(compat_result):
"""Drive the `claude` CLI against the LiteLLM proxy and assert a
non-empty streamed reply (one row per Claude tier).
diff --git a/tests/e2e/claude_code/basic_messaging_streaming/test_vertex_ai.py b/tests/e2e/claude_code/basic_messaging_streaming/test_vertex_ai.py
index 014a31160a85..13f1a0abf40e 100644
--- a/tests/e2e/claude_code/basic_messaging_streaming/test_vertex_ai.py
+++ b/tests/e2e/claude_code/basic_messaging_streaming/test_vertex_ai.py
@@ -15,15 +15,17 @@
from __future__ import annotations
+import pytest
from claude_code._basic_messaging import run_basic_messaging_cell
VERTEX_AI_MODELS = [
"claude-haiku-4-5-vertex",
- "claude-sonnet-4-6-vertex",
+ "claude-sonnet-4-5-vertex",
"claude-opus-4-7-vertex",
]
+@pytest.mark.covers("llm.messages.vertex.basic.stream.works")
def test_basic_messaging_streaming_vertex_ai(compat_result):
"""Drive the `claude` CLI against the LiteLLM proxy and assert a
non-empty streamed reply (one row per Claude tier).
diff --git a/tests/e2e/claude_code/conftest.py b/tests/e2e/claude_code/conftest.py
index d2bfa1a54bff..ee5da0404152 100644
--- a/tests/e2e/claude_code/conftest.py
+++ b/tests/e2e/claude_code/conftest.py
@@ -169,8 +169,9 @@ def _manifest_feature_ids() -> FrozenSet[str]:
Used as a positive filter so only directories that correspond to a
real matrix row contribute results — utility/support directories
- (e.g. `cron_vm`, `_driver_unit_tests`) are dropped regardless of
- naming convention, and the rate-limit summary stays clean.
+ (e.g. `_driver_unit_tests`, `_builder_unit_tests`) are dropped
+ regardless of naming convention, and the rate-limit summary stays
+ clean.
Returns an empty set if the manifest is missing or malformed; the
caller treats that as "no path is a feature path", which is the
@@ -199,11 +200,10 @@ def _infer_feature_and_provider(node_path: Path) -> Optional[tuple]:
Path shape: tests/e2e/claude_code//test_.py
Returns None if the file is not a per-feature test (e.g. unit tests
- under `_driver_unit_tests/` or support code under `cron_vm/`), so
- those don't pollute the matrix artifact. We positively filter the
- parent directory against `manifest.yaml` rather than relying on
- naming conventions, because non-feature siblings don't all share
- an underscore prefix.
+ under `_driver_unit_tests/`), so those don't pollute the matrix
+ artifact. We positively filter the parent directory against
+ `manifest.yaml` rather than relying on naming conventions, because
+ non-feature siblings don't all share an underscore prefix.
"""
name = node_path.name
if not name.startswith("test_") or not name.endswith(".py"):
@@ -548,3 +548,119 @@ def pytest_sessionfinish(session, exitstatus):
)
summary_path.write_text(json.dumps(summary, indent=2, sort_keys=True))
_print_rate_limit_summary(summary)
+
+
+# ---------------------------------------------------------------------------
+# Session-scoped compat model registration.
+#
+# The compat cells probe hardcoded virtual names like ``claude-sonnet-4-5``
+# and ``claude-sonnet-4-5-bedrock-invoke``. On stage those live in the
+# gateway's model_list at deploy time; locally the docker-config.yaml
+# under tests/e2e/ only declares one of them, so every non-haiku cell
+# 400s with ``Invalid model name``. The fixture here reconciles the two:
+# it reads ``test_config.yaml`` (the ground-truth compat matrix config)
+# and POSTs ``/model/new`` for the subset whose provider credentials are
+# actually set in the current environment, then tears them all down at
+# session end.
+#
+# Kept below the rest of the conftest so the compat-artifact hooks stay
+# grouped up top. The fixture is opt-in via autouse=True on the session
+# scope, so a cell that hits the proxy sees the deployment ready without
+# any per-cell wiring, and pure unit tests that never reach the proxy
+# pay only one skipped-liveness check.
+# ---------------------------------------------------------------------------
+
+from claude_code._env import ProxyConfig, resolve_proxy # noqa: E402
+from claude_code._compat_models import ( # noqa: E402
+ CompatDeployment,
+ load_all_deployments,
+)
+
+
+def _build_control_gateway(proxy: ProxyConfig):
+ """Local import of the shared harness so the pure-unit-test tree
+ under ``_driver_unit_tests/`` etc. never has to pull it in. The
+ control plane transport is what /model/new lives on; SplitTransport
+ routes it correctly for both monolithic and split deployments.
+
+ The endpoints come from the *resolved* proxy, not from ``e2e_config``'s
+ own env read: this suite also accepts the legacy ``LITELLM_PROXY_BASE_URL``
+ / ``LITELLM_PROXY_API_KEY`` spelling, and under that spelling
+ ``e2e_config`` sees nothing and falls back to http://localhost:4000 with
+ sk-1234 — registering models on a different host and key than the cells
+ then call. Both planes get the one URL the cells use; the deployment is
+ fronted by a single address that routes management and LLM paths itself."""
+ from e2e_gateway import build_gateway
+
+ return build_gateway(
+ base_url=proxy.base_url,
+ master_key=proxy.api_key,
+ control_plane_base_url=proxy.base_url,
+ )
+
+
+def _register_deployment(gateway, deployment: CompatDeployment) -> str:
+ """Register one deployment and return its proxy-assigned model_id
+ once it is servable on the data plane."""
+ return gateway.create_model(
+ deployment.model_name,
+ deployment.litellm_params,
+ )
+
+
+@pytest.fixture(scope="session", autouse=True)
+def _compat_models_registered() -> Any:
+ """Register every compat deployment against the running proxy, then
+ tear them all down on session exit.
+
+ Skips silently if the proxy env is not configured (no
+ ``LITELLM_PROXY_URL``/``LITELLM_MASTER_KEY``) so unit-test runs
+ stay hermetic.
+
+ Design note: we always attempt to register all 15 deployments,
+ regardless of what credentials are exported in the test-runner's
+ shell. The credentials live in the proxy container's environment
+ (via docker-compose ``env_file``), not the shell running pytest -
+ so gating on shell env would filter out deployments the proxy can
+ actually serve. Per-deployment ``/model/new`` failures are printed
+ but do not abort the session: the cells that need that specific
+ deployment will 400 with "Invalid model name" and fail loudly,
+ which is the right signal (missing cred on the proxy side)."""
+ proxy = resolve_proxy()
+ if proxy is None:
+ yield
+ return
+
+ from requests import RequestException
+
+ gateway = _build_control_gateway(proxy)
+ registered_ids: list[str] = []
+ failures: list[tuple[str, str]] = []
+ try:
+ for deployment in load_all_deployments():
+ try:
+ model_id = _register_deployment(gateway, deployment)
+ registered_ids.append(model_id)
+ except (AssertionError, RequestException) as exc:
+ failures.append((deployment.model_name, str(exc)))
+ if failures:
+ summary = "\n".join(
+ f" - {name}: {reason}" for name, reason in failures
+ )
+ print(
+ f"[compat fixture] {len(failures)} of "
+ f"{len(failures) + len(registered_ids)} deployments "
+ f"failed to register (proxy likely missing that provider's "
+ f"credentials); cells that target them will fail loudly:\n"
+ f"{summary}"
+ )
+ yield
+ finally:
+ for model_id in registered_ids:
+ try:
+ gateway.delete_model(model_id)
+ except (AssertionError, RequestException):
+ # Best-effort — teardown surfaces via warnings inside
+ # ``delete_model`` already; swallowing here so one flaky
+ # delete does not mask real test failures.
+ pass
diff --git a/tests/e2e/claude_code/count_tokens/test_anthropic.py b/tests/e2e/claude_code/count_tokens/test_anthropic.py
index 3508063459cb..2fbdd4212c43 100644
--- a/tests/e2e/claude_code/count_tokens/test_anthropic.py
+++ b/tests/e2e/claude_code/count_tokens/test_anthropic.py
@@ -37,44 +37,27 @@
from __future__ import annotations
-import os
-
import pytest
+from claude_code._env import require_proxy
from claude_code.http_probe import (
assert_count_tokens_shape,
probe_count_tokens,
)
-PROXY_BASE_URL_ENV = "LITELLM_PROXY_BASE_URL"
-PROXY_API_KEY_ENV = "LITELLM_PROXY_API_KEY"
ANTHROPIC_MODELS = [
"claude-haiku-4-5",
- "claude-sonnet-4-6",
+ "claude-sonnet-4-5",
"claude-opus-4-7",
]
+@pytest.mark.covers("llm.messages.anthropic.count_tokens.nonstream.works")
def test_count_tokens_anthropic(compat_result):
"""Probe `/v1/messages/count_tokens` for each Anthropic tier and
assert the response shape."""
- base_url = os.environ.get(PROXY_BASE_URL_ENV)
- api_key = os.environ.get(PROXY_API_KEY_ENV)
- if not base_url or not api_key:
- compat_result.set(
- {
- "status": "fail",
- "error": (
- f"missing required env: set {PROXY_BASE_URL_ENV} and "
- f"{PROXY_API_KEY_ENV} to point at a running LiteLLM proxy"
- ),
- }
- )
- pytest.fail(
- f"{PROXY_BASE_URL_ENV} / {PROXY_API_KEY_ENV} not configured",
- pytrace=False,
- )
+ base_url, api_key = require_proxy(compat_result)
failures = []
for model in ANTHROPIC_MODELS:
diff --git a/tests/e2e/claude_code/count_tokens/test_azure.py b/tests/e2e/claude_code/count_tokens/test_azure.py
index 2b8707b50b09..a9aa168ccea3 100644
--- a/tests/e2e/claude_code/count_tokens/test_azure.py
+++ b/tests/e2e/claude_code/count_tokens/test_azure.py
@@ -37,44 +37,27 @@
from __future__ import annotations
-import os
-
import pytest
+from claude_code._env import require_proxy
from claude_code.http_probe import (
assert_count_tokens_shape,
probe_count_tokens,
)
-PROXY_BASE_URL_ENV = "LITELLM_PROXY_BASE_URL"
-PROXY_API_KEY_ENV = "LITELLM_PROXY_API_KEY"
AZURE_MODELS = [
"claude-haiku-4-5-azure",
- "claude-sonnet-4-6-azure",
+ "claude-sonnet-4-5-azure",
"claude-opus-4-7-azure",
]
+@pytest.mark.covers("llm.messages.azure_foundry.count_tokens.nonstream.works")
def test_count_tokens_azure(compat_result):
"""Probe `/v1/messages/count_tokens` for each Azure (Microsoft Foundry) tier and
assert the response shape."""
- base_url = os.environ.get(PROXY_BASE_URL_ENV)
- api_key = os.environ.get(PROXY_API_KEY_ENV)
- if not base_url or not api_key:
- compat_result.set(
- {
- "status": "fail",
- "error": (
- f"missing required env: set {PROXY_BASE_URL_ENV} and "
- f"{PROXY_API_KEY_ENV} to point at a running LiteLLM proxy"
- ),
- }
- )
- pytest.fail(
- f"{PROXY_BASE_URL_ENV} / {PROXY_API_KEY_ENV} not configured",
- pytrace=False,
- )
+ base_url, api_key = require_proxy(compat_result)
failures = []
for model in AZURE_MODELS:
diff --git a/tests/e2e/claude_code/count_tokens/test_bedrock_converse.py b/tests/e2e/claude_code/count_tokens/test_bedrock_converse.py
index 4221773ead2c..6dcff3ecae32 100644
--- a/tests/e2e/claude_code/count_tokens/test_bedrock_converse.py
+++ b/tests/e2e/claude_code/count_tokens/test_bedrock_converse.py
@@ -37,44 +37,27 @@
from __future__ import annotations
-import os
-
import pytest
+from claude_code._env import require_proxy
from claude_code.http_probe import (
assert_count_tokens_shape,
probe_count_tokens,
)
-PROXY_BASE_URL_ENV = "LITELLM_PROXY_BASE_URL"
-PROXY_API_KEY_ENV = "LITELLM_PROXY_API_KEY"
BEDROCK_CONVERSE_MODELS = [
"claude-haiku-4-5-bedrock-converse",
- "claude-sonnet-4-6-bedrock-converse",
+ "claude-sonnet-4-5-bedrock-converse",
"claude-opus-4-7-bedrock-converse",
]
+@pytest.mark.covers("llm.messages.bedrock_converse.count_tokens.nonstream.works")
def test_count_tokens_bedrock_converse(compat_result):
"""Probe `/v1/messages/count_tokens` for each Bedrock (Converse) tier and
assert the response shape."""
- base_url = os.environ.get(PROXY_BASE_URL_ENV)
- api_key = os.environ.get(PROXY_API_KEY_ENV)
- if not base_url or not api_key:
- compat_result.set(
- {
- "status": "fail",
- "error": (
- f"missing required env: set {PROXY_BASE_URL_ENV} and "
- f"{PROXY_API_KEY_ENV} to point at a running LiteLLM proxy"
- ),
- }
- )
- pytest.fail(
- f"{PROXY_BASE_URL_ENV} / {PROXY_API_KEY_ENV} not configured",
- pytrace=False,
- )
+ base_url, api_key = require_proxy(compat_result)
failures = []
for model in BEDROCK_CONVERSE_MODELS:
diff --git a/tests/e2e/claude_code/count_tokens/test_bedrock_invoke.py b/tests/e2e/claude_code/count_tokens/test_bedrock_invoke.py
index cc70bf123928..ae89067dc005 100644
--- a/tests/e2e/claude_code/count_tokens/test_bedrock_invoke.py
+++ b/tests/e2e/claude_code/count_tokens/test_bedrock_invoke.py
@@ -37,44 +37,27 @@
from __future__ import annotations
-import os
-
import pytest
+from claude_code._env import require_proxy
from claude_code.http_probe import (
assert_count_tokens_shape,
probe_count_tokens,
)
-PROXY_BASE_URL_ENV = "LITELLM_PROXY_BASE_URL"
-PROXY_API_KEY_ENV = "LITELLM_PROXY_API_KEY"
BEDROCK_INVOKE_MODELS = [
"claude-haiku-4-5-bedrock-invoke",
- "claude-sonnet-4-6-bedrock-invoke",
+ "claude-sonnet-4-5-bedrock-invoke",
"claude-opus-4-7-bedrock-invoke",
]
+@pytest.mark.covers("llm.messages.bedrock_invoke.count_tokens.nonstream.works")
def test_count_tokens_bedrock_invoke(compat_result):
"""Probe `/v1/messages/count_tokens` for each Bedrock (Invoke) tier and
assert the response shape."""
- base_url = os.environ.get(PROXY_BASE_URL_ENV)
- api_key = os.environ.get(PROXY_API_KEY_ENV)
- if not base_url or not api_key:
- compat_result.set(
- {
- "status": "fail",
- "error": (
- f"missing required env: set {PROXY_BASE_URL_ENV} and "
- f"{PROXY_API_KEY_ENV} to point at a running LiteLLM proxy"
- ),
- }
- )
- pytest.fail(
- f"{PROXY_BASE_URL_ENV} / {PROXY_API_KEY_ENV} not configured",
- pytrace=False,
- )
+ base_url, api_key = require_proxy(compat_result)
failures = []
for model in BEDROCK_INVOKE_MODELS:
diff --git a/tests/e2e/claude_code/count_tokens/test_vertex_ai.py b/tests/e2e/claude_code/count_tokens/test_vertex_ai.py
index 8c2678f70108..0f952496566f 100644
--- a/tests/e2e/claude_code/count_tokens/test_vertex_ai.py
+++ b/tests/e2e/claude_code/count_tokens/test_vertex_ai.py
@@ -37,44 +37,27 @@
from __future__ import annotations
-import os
-
import pytest
+from claude_code._env import require_proxy
from claude_code.http_probe import (
assert_count_tokens_shape,
probe_count_tokens,
)
-PROXY_BASE_URL_ENV = "LITELLM_PROXY_BASE_URL"
-PROXY_API_KEY_ENV = "LITELLM_PROXY_API_KEY"
VERTEX_AI_MODELS = [
"claude-haiku-4-5-vertex",
- "claude-sonnet-4-6-vertex",
+ "claude-sonnet-4-5-vertex",
"claude-opus-4-7-vertex",
]
+@pytest.mark.covers("llm.messages.vertex.count_tokens.nonstream.works")
def test_count_tokens_vertex_ai(compat_result):
"""Probe `/v1/messages/count_tokens` for each Vertex AI tier and
assert the response shape."""
- base_url = os.environ.get(PROXY_BASE_URL_ENV)
- api_key = os.environ.get(PROXY_API_KEY_ENV)
- if not base_url or not api_key:
- compat_result.set(
- {
- "status": "fail",
- "error": (
- f"missing required env: set {PROXY_BASE_URL_ENV} and "
- f"{PROXY_API_KEY_ENV} to point at a running LiteLLM proxy"
- ),
- }
- )
- pytest.fail(
- f"{PROXY_BASE_URL_ENV} / {PROXY_API_KEY_ENV} not configured",
- pytrace=False,
- )
+ base_url, api_key = require_proxy(compat_result)
failures = []
for model in VERTEX_AI_MODELS:
diff --git a/tests/e2e/claude_code/cron_vm/build_matrix.py b/tests/e2e/claude_code/cron_vm/build_matrix.py
deleted file mode 100644
index 128f041cced4..000000000000
--- a/tests/e2e/claude_code/cron_vm/build_matrix.py
+++ /dev/null
@@ -1,50 +0,0 @@
-"""Tiny CLI wrapper around `claude_code.matrix_builder.build_from_paths`.
-
-Exists only so `run_daily.sh` can hand the version metadata + paths into
-the matrix builder without re-implementing it in bash. All real logic
-lives in `matrix_builder.py`, which has its own unit tests under
-`_builder_unit_tests/`.
-
-Invoked from the cron worktree (where `uv sync` has installed pyyaml),
-not the dev checkout — the bash script `cd`s into the worktree before
-`uv run python`-ing this file.
-"""
-
-from __future__ import annotations
-
-import argparse
-import datetime
-import sys
-from pathlib import Path
-
-sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
-
-from claude_code.matrix_builder import build_from_paths # noqa: E402 # import needs the sys.path bootstrap above
-
-
-def main() -> int:
- parser = argparse.ArgumentParser(description=__doc__)
- parser.add_argument("--manifest", type=Path, required=True)
- parser.add_argument("--results", type=Path, required=True)
- parser.add_argument("--output", type=Path, required=True)
- parser.add_argument("--litellm-version", required=True)
- parser.add_argument("--claude-code-version", required=True)
- args = parser.parse_args()
-
- generated_at = datetime.datetime.now(datetime.timezone.utc).strftime(
- "%Y-%m-%dT%H:%M:%SZ"
- )
- build_from_paths(
- manifest_path=args.manifest,
- results_path=args.results,
- litellm_version=args.litellm_version,
- claude_code_version=args.claude_code_version,
- generated_at=generated_at,
- output_path=args.output,
- )
- print(f"wrote {args.output}")
- return 0
-
-
-if __name__ == "__main__":
- sys.exit(main())
diff --git a/tests/e2e/claude_code/cron_vm/litellm-compat-matrix.env.example b/tests/e2e/claude_code/cron_vm/litellm-compat-matrix.env.example
deleted file mode 100644
index 11633810533d..000000000000
--- a/tests/e2e/claude_code/cron_vm/litellm-compat-matrix.env.example
+++ /dev/null
@@ -1,50 +0,0 @@
-# Environment file consumed by `litellm-compat-matrix.service`.
-#
-# Install at `/etc/litellm-compat-matrix.env` and chmod 0600.
-# `EnvironmentFile=-` in the unit means the service is allowed to start
-# even if this file is missing, but the populator will fail at the
-# first provider request without these credentials.
-
-# Anthropic
-ANTHROPIC_API_KEY=
-
-# Bedrock (invoke + converse columns).
-# Use Anthropic's Bedrock API-key passthrough (long-lived bearer token).
-# No AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY required for the matrix --
-# both the LiteLLM invoke and converse routes pick up
-# AWS_BEARER_TOKEN_BEDROCK when present.
-AWS_BEARER_TOKEN_BEDROCK=
-AWS_REGION_NAME=us-east-1
-
-# Vertex AI.
-# On the GCP VM, the default service-account ADC from the metadata server
-# is used -- no JSON key file is needed. If you ever need to run outside
-# GCP, also export GOOGLE_APPLICATION_CREDENTIALS=/path/to/sa.json.
-VERTEXAI_PROJECT=
-VERTEXAI_LOCATION=global
-
-# Microsoft Foundry (Azure column)
-AZURE_FOUNDRY_API_KEY=
-AZURE_FOUNDRY_API_BASE=
-
-# REQUIRED for publishing: PAT for the `agent-shin` user, used to push
-# the daily compat-matrix branch to its fork (agent-shin/litellm-docs)
-# and open the cross-repo PR against BerriAI/litellm-docs. Scopes:
-# classic `repo` + `workflow`, or fine-grained on agent-shin/litellm-docs
-# with Contents:RW + Pull requests:RW + Workflows:RW.
-# Skip by setting SKIP_PUBLISH=1 (publishes nothing; only writes the
-# matrix JSON locally).
-AGENT_SHIN_GITHUB_TOKEN=
-
-# Optional: lifts the unauthenticated rate limit on the GitHub Releases
-# API used by `resolver.py`. Any token works (read-only). Not required.
-# GITHUB_TOKEN=
-
-# Optional overrides; defaults are sensible for the cron VM.
-# PROXY_PORT=4100
-# LITELLM_WORKTREE=/home/mateo/litellm-cron-worktree
-# DOCS_REPO=BerriAI/litellm-docs
-# DOCS_BRANCH=main
-# DOCS_TARGET_PATH=src/data/compatibility-matrix.json
-# FORK_OWNER=agent-shin
-# FORK_REPO=agent-shin/litellm-docs
diff --git a/tests/e2e/claude_code/cron_vm/litellm-compat-matrix.service b/tests/e2e/claude_code/cron_vm/litellm-compat-matrix.service
deleted file mode 100644
index c05ece90f506..000000000000
--- a/tests/e2e/claude_code/cron_vm/litellm-compat-matrix.service
+++ /dev/null
@@ -1,141 +0,0 @@
-# systemd service for the Claude Code compatibility-matrix populator.
-#
-# Triggered by `litellm-compat-matrix.timer`; not started directly. The
-# unit is a `Type=oneshot` so the timer's `OnCalendar=` semantics
-# describe "run once per day" cleanly — there's no long-lived daemon to
-# supervise; each invocation runs the populator end-to-end and exits.
-#
-# Install
-# -------
-#
-# sudo cp tests/e2e/claude_code/cron_vm/litellm-compat-matrix.service /etc/systemd/system/
-# sudo cp tests/e2e/claude_code/cron_vm/litellm-compat-matrix.timer /etc/systemd/system/
-# sudo systemctl daemon-reload
-# sudo systemctl enable --now litellm-compat-matrix.timer
-#
-# Paths are hard-coded to /home/mateo rather than using systemd's %h
-# specifier. Why: in *system* units (this one), %h is expanded at
-# parse time against the *manager's* home -- which is /root for PID 1
-# -- and *not* against the User= directive. That mismatch makes
-# ReadWritePaths point at /root/.cache (which doesn't exist), causing
-# the namespace setup to fail with status=226/NAMESPACE before the
-# script ever runs. The runtime user (`User=mateo`) must:
-#
-# * have a checkout of `BerriAI/litellm` at `~/litellm/litellm` so the
-# publisher module is importable;
-# * have a uv venv at `~/litellm/litellm/.venv` (created by
-# `uv sync --frozen` inside that checkout once);
-# * have `gh` already authenticated against an account with
-# `pull-requests: write` on `BerriAI/litellm-docs`;
-# * have provider credentials exported in `/etc/litellm-compat-matrix.env`
-# (see `litellm-compat-matrix.env.example` in this directory).
-
-[Unit]
-Description=Claude Code compatibility-matrix populator (oneshot)
-Wants=network-online.target
-After=network-online.target
-
-[Service]
-Type=oneshot
-User=mateo
-Group=mateo
-
-# Provider credentials + any gh/PROXY_PORT overrides live here. Format
-# is the standard `KEY=value` one line per env var.
-EnvironmentFile=-/etc/litellm-compat-matrix.env
-
-# systemd starts with a minimal PATH (~/usr/local/bin:/usr/bin:/bin).
-# `uv` and `claude` are installed under the runtime user's `~/.local/bin`
-# so we have to prepend it explicitly; otherwise run_daily.sh fails at
-# the up-front command-presence check.
-Environment=PATH=/home/mateo/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
-
-# `HOME` is auto-set to /home/mateo when User=mateo is honored, but be
-# explicit so anything that reads $HOME (e.g. uv's cache lookup, the
-# claude CLI's per-session dir) sees the right value even if a future
-# refactor flips DynamicUser= or PrivateUsers= on.
-Environment=HOME=/home/mateo
-
-WorkingDirectory=/home/mateo/litellm/litellm
-
-ExecStart=/home/mateo/litellm/litellm/tests/e2e/claude_code/cron_vm/run_daily.sh
-
-# 90 minutes is generous: cold runs do `git clone` + `uv sync` of a new
-# tag's lockfile, which can take a couple of minutes on a 2-vCPU VM,
-# plus 30 cells of pytest hitting four cloud providers.
-TimeoutStartSec=90min
-
-# A failed run shouldn't restart automatically — the next timer fire is
-# the right retry. Reruns of the same day's matrix are idempotent.
-Restart=no
-
-# Security hardening: the populator only reads the litellm checkout and
-# the env-file; everything else it writes lives in either the worktree
-# (managed) or `/tmp` (cleaned up by tempfile).
-#
-# `ProtectHome=read-only` blocks writes to /home/mateo but still
-# allows reads. That's safe for the trusted run_daily.sh script
-# itself, but unsafe for any subprocess we don't control: a
-# compromised npm-installed `claude` package, or a model-directed
-# `Read` tool call during a PDF/vision cell, could read sensitive
-# host files like `~/.config/gh/hosts.yml` (gh-host token),
-# `~/.ssh/`, or `~/.bash_history`. We mitigate that at the call
-# boundary: every `claude` subprocess (the up-front `claude --version`
-# probe in run_daily.sh, plus every CLI invocation routed through
-# tests/e2e/claude_code/cli_driver.py) runs with `HOME` pointed at a
-# fresh empty per-invocation tmpdir, not at /home/mateo. The CLI
-# never sees the runtime user's real dotfiles. `gh` invocations in
-# run_daily.sh pass `GH_TOKEN` inline, so they never need to read
-# ~/.config/gh either; that path is intentionally NOT in the
-# whitelist below — keeping it out is the second line of defense if
-# the inline-token convention is ever accidentally regressed.
-#
-# ReadWritePaths whitelist:
-# * litellm-cron-worktree - the long-lived stable-tag checkout +
-# its `.venv` (`uv sync` rewrites every
-# run) + `.uv-bin` (pinned `uv` binary
-# cache).
-# * .cache - uv's wheel cache (~/.cache/uv) so we
-# don't redownload pinned deps each
-# run. Used only by the trusted `uv`
-# process; not exposed to `claude`.
-# * /tmp - mktemp -d workdir, proxy logs, and
-# the per-`claude`-invocation isolated
-# HOME tmpdirs. PrivateTmp=true below
-# gives the service its own tmpfs view
-# so these don't escape to the host.
-NoNewPrivileges=true
-ProtectSystem=strict
-ProtectHome=read-only
-ReadWritePaths=/home/mateo/litellm-cron-worktree /home/mateo/.cache /tmp
-PrivateTmp=true
-
-# Filesystem-level hiding for credential-bearing dotdirs/files. Even
-# though `ProtectHome=read-only` prevents writes, a model-directed
-# `Read` tool call (the PDF cells pass `--allowed-tools Read`) or a
-# compromised `claude` package can read absolute paths under
-# /home/mateo and exfiltrate the contents. `InaccessiblePaths=` makes
-# the listed paths look like empty/missing to every process in the
-# unit's mount namespace -- including the trusted populator script,
-# which is fine because it doesn't need any of these:
-#
-# * .config/gh - gh CLI host token; we pass GH_TOKEN inline to
-# every `gh` invocation (clone/PR/reviewer) so the
-# host config is never consulted.
-# * .ssh - never used by the populator.
-# * .aws - upstream AWS credentials are passed to the proxy
-# via the EnvironmentFile (provider env vars), not
-# via shared SDK config files.
-# * .docker - the populator never talks to a docker socket.
-# * .kube - the populator never talks to a k8s API.
-# * .gnupg - no GPG signing on the bot's commits.
-#
-# Leading `-` makes systemd tolerant if a path doesn't exist on the
-# host (the unit is portable across VMs that may not have all of
-# them set up). Anything else under /home/mateo (the litellm
-# checkout, the cron worktree, the uv cache, .local/bin for the
-# claude/uv/gh binaries on PATH) stays read-accessible.
-InaccessiblePaths=-/home/mateo/.config/gh -/home/mateo/.ssh -/home/mateo/.aws -/home/mateo/.docker -/home/mateo/.kube -/home/mateo/.gnupg
-
-[Install]
-WantedBy=multi-user.target
diff --git a/tests/e2e/claude_code/cron_vm/litellm-compat-matrix.timer b/tests/e2e/claude_code/cron_vm/litellm-compat-matrix.timer
deleted file mode 100644
index ee22538c6ed8..000000000000
--- a/tests/e2e/claude_code/cron_vm/litellm-compat-matrix.timer
+++ /dev/null
@@ -1,25 +0,0 @@
-# Daily timer for the compatibility-matrix populator.
-#
-# 06:00 UTC matches the original GitHub Actions cron schedule; chosen so
-# operators in US/EU timezones see fresh PRs at the start of their work
-# day.
-#
-# `Persistent=true` causes a missed run (VM was off / suspended) to
-# fire the next time the timer is started, which is the property we
-# want for a once-a-day job: the matrix should refresh as soon as the
-# VM is reachable again, not wait another 24h.
-#
-# `RandomizedDelaySec=10min` smears load if multiple matrix-style
-# pipelines are ever colocated on the same VM in the future.
-
-[Unit]
-Description=Run the Claude Code compatibility-matrix populator daily
-
-[Timer]
-OnCalendar=*-*-* 06:00:00 UTC
-Persistent=true
-RandomizedDelaySec=10min
-Unit=litellm-compat-matrix.service
-
-[Install]
-WantedBy=timers.target
diff --git a/tests/e2e/claude_code/cron_vm/run_daily.sh b/tests/e2e/claude_code/cron_vm/run_daily.sh
deleted file mode 100755
index ae2d67c070cf..000000000000
--- a/tests/e2e/claude_code/cron_vm/run_daily.sh
+++ /dev/null
@@ -1,590 +0,0 @@
-#!/usr/bin/env bash
-# Daily Claude Code compatibility-matrix populator.
-#
-# Runs from the GCP VM `litellm-compatibility-matrix-populator` via the
-# systemd timer in this directory. The flow is:
-#
-# 1. Resolve the latest LiteLLM v*-stable tag from the GitHub Releases API.
-# 2. Update a long-lived worktree at $WORKTREE to that tag and `uv sync` it.
-# 3. Boot the proxy as a background subprocess on $PROXY_PORT (default
-# 4100; a separate port from the human-tended :4000 proxy).
-# 4. Run `pytest tests/e2e/claude_code/` against the proxy. Test failures
-# become `fail` cells in the JSON, not script errors.
-# 5. Hand the per-test results artifact + manifest to a small Python
-# CLI (`build_matrix.py`) that wraps the existing
-# `matrix_builder.build_from_paths` to produce the published
-# compatibility-matrix.json.
-# 6. `gh repo clone` litellm-docs, write the JSON to a deterministic
-# branch (`compat-matrix/--`), commit,
-# `git push --force`, and `gh pr create`.
-#
-# Same-day reruns land on the same branch so they update the existing PR
-# rather than spawning a new one. If the JSON is byte-identical to the
-# docs branch, we skip the push entirely.
-#
-# Required commands on $PATH: git, uv, gh, jq, curl, claude.
-# Required state: ~/litellm/litellm checked out (this file lives in it),
-# $WORKTREE is created on first run, gh is already authenticated.
-#
-# Override any default by setting the matching env var; see the systemd
-# unit for the production wiring.
-
-set -Eeuo pipefail
-
-LITELLM_REPO="${LITELLM_REPO:-${HOME}/litellm/litellm}"
-WORKTREE="${LITELLM_WORKTREE:-${HOME}/litellm-cron-worktree}"
-PROXY_PORT="${PROXY_PORT:-4100}"
-PROXY_API_KEY="${PROXY_API_KEY:-sk-cron-matrix}"
-DOCS_REPO="${DOCS_REPO:-BerriAI/litellm-docs}"
-DOCS_BRANCH="${DOCS_BRANCH:-main}"
-DOCS_TARGET_PATH="${DOCS_TARGET_PATH:-src/data/compatibility-matrix.json}"
-SKIP_PUBLISH="${SKIP_PUBLISH:-0}"
-PYTEST_K="${PYTEST_K:-}"
-# Comma-separated GitHub usernames to request a review from on every PR.
-# Reviewers must have at least read access to ${DOCS_REPO}. PR-author
-# (agent-shin) has implicit rights to request reviews from anyone with
-# read access, so no extra token scope is needed. Set to empty to skip.
-PR_REVIEWERS="${PR_REVIEWERS:-mateo-berri}"
-
-POPULATOR_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
-WORKDIR="$(mktemp -d -t litellm-compat-matrix.XXXXXX)"
-PROXY_PID_FILE="${WORKDIR}/proxy.pid"
-
-# Cleanup is intentionally aggressive: it can run on normal exit, on a
-# signal received by the script, or after a partial failure where the
-# proxy is up but ${PROXY_PID_FILE} is stale. We try four things in
-# order and stop as soon as the proxy port is free:
-#
-# 1. SIGTERM the pid recorded in proxy.pid.
-# 2. SIGKILL anything from `pgrep -f "litellm.*--port ${PROXY_PORT}"`
-# that survived. This catches the common case where the recorded
-# pid was the sh wrapper, not the long-lived python child.
-# 3. ss -K on the port (kernel kills sockets but not processes;
-# mostly useful for catching lingering CLOSE_WAITs).
-# 4. wipe ${WORKDIR}.
-cleanup() {
- local rc=$?
- set +e
- local proxy_pid
- if [[ -f "${PROXY_PID_FILE}" ]]; then
- proxy_pid="$(cat "${PROXY_PID_FILE}")"
- if [[ -n "${proxy_pid}" ]]; then
- kill -TERM "-${proxy_pid}" 2>/dev/null || kill -TERM "${proxy_pid}" 2>/dev/null || true
- for _ in 1 2 3 4 5; do
- kill -0 "${proxy_pid}" 2>/dev/null || break
- sleep 1
- done
- fi
- fi
- # Belt-and-braces: any python or uv talking to ${PROXY_PORT} that
- # survived the SIGTERM gets SIGKILL'd by name.
- pgrep -f "litellm.*--port[ =]?${PROXY_PORT}([^0-9]|$)" 2>/dev/null \
- | xargs -r kill -KILL 2>/dev/null || true
- pgrep -f "${WORKTREE}/.uv-bin/uv.*run litellm" 2>/dev/null \
- | xargs -r kill -KILL 2>/dev/null || true
- rm -rf "${WORKDIR}"
- exit "${rc}"
-}
-trap cleanup EXIT INT TERM
-
-log() { printf '==> %s\n' "$*" >&2; }
-die() { printf 'ERROR: %s\n' "$*" >&2; exit 1; }
-
-for cmd in git uv gh jq curl claude; do
- command -v "${cmd}" >/dev/null 2>&1 || die "missing required command: ${cmd}"
-done
-
-# Publishing is from a fork (agent-shin/litellm-docs) so neither the cron
-# host nor the bot identity needs write access to BerriAI/litellm-docs. We
-# require the fork token up front -- failing 30 minutes into a run because
-# the env file is missing one line is a waste of CI quota.
-if [[ "${SKIP_PUBLISH}" != "1" ]]; then
- [[ -n "${AGENT_SHIN_GITHUB_TOKEN:-}" ]] \
- || die "AGENT_SHIN_GITHUB_TOKEN required to open PRs from agent-shin/litellm-docs (or set SKIP_PUBLISH=1)"
-fi
-
-# ---------------------------------------------------------------------------
-# 1. Resolve versions
-# ---------------------------------------------------------------------------
-
-# Newest v*-stable release on BerriAI/litellm. The `select(...)` filter
-# drops drafts/non-stable, the version_key sort handles 1.10 > 1.9.
-#
-# Paginate through the releases endpoint instead of grabbing only page 1
-# (default page_size=30). LiteLLM ships multiple non-stable releases per
-# day, so it's common to need to walk past 30+ entries before hitting
-# the most recent v*-stable. We cap at 5 pages (500 releases) which is
-# conservatively beyond the worst observed gap.
-#
-# We deliberately do NOT short-circuit on the first page that contains a
-# v*-stable tag. The /releases endpoint orders by `created_at`, not by
-# semver, so a backport on an older series (e.g. v1.80.1-stable cut
-# today) can show up on an earlier page than a higher-versioned release
-# (v1.83.0-stable cut two weeks ago). Breaking early on first-stable-seen
-# would silently pin the cron to the stale tag because the
-# higher-versioned release still on a later page would never make it
-# into the merged set the `sort_by` below consumes. The only break we
-# keep is the empty-page guard, which means a quiet period in the
-# release feed doesn't waste API quota — we just always walk far enough
-# to be confident we've seen the highest stable tag.
-GH_AUTH_HEADER=()
-if [[ -n "${GITHUB_TOKEN:-}" ]]; then
- GH_AUTH_HEADER=(-H "Authorization: Bearer ${GITHUB_TOKEN}")
-fi
-RELEASES_JSON="${WORKDIR}/releases.json"
-echo "[]" >"${RELEASES_JSON}"
-for page in 1 2 3 4 5; do
- PAGE_JSON="${WORKDIR}/releases.page${page}.json"
- curl -fsS \
- -H 'Accept: application/vnd.github+json' \
- -H 'User-Agent: litellm-compat-matrix' \
- "${GH_AUTH_HEADER[@]}" \
- "https://api.github.com/repos/BerriAI/litellm/releases?per_page=100&page=${page}" \
- >"${PAGE_JSON}"
- jq -s '.[0] + .[1]' "${RELEASES_JSON}" "${PAGE_JSON}" >"${RELEASES_JSON}.merged"
- mv "${RELEASES_JSON}.merged" "${RELEASES_JSON}"
- # No more pages? GitHub returns an empty array past the last page.
- if [[ "$(jq 'length' "${PAGE_JSON}")" == "0" ]]; then
- break
- fi
-done
-LITELLM_VERSION="$(
- jq -r '
- [ .[] | .tag_name // empty
- | select(test("^v[0-9]+\\.[0-9]+\\.[0-9]+-stable$"))
- ]
- | sort_by(
- capture("^v(?[0-9]+)\\.(?[0-9]+)\\.(?[0-9]+)-stable$")
- | [(.a|tonumber), (.b|tonumber), (.c|tonumber)]
- )
- | last // empty
- ' "${RELEASES_JSON}"
-)"
-[[ -n "${LITELLM_VERSION}" ]] || die "could not resolve latest v*-stable tag in 5 pages of releases"
-log "resolved litellm: ${LITELLM_VERSION}"
-
-# The systemd unit loads provider credentials and the agent-shin GitHub
-# token from /etc/litellm-compat-matrix.env into this script's
-# environment. Running the npm-installed `claude` binary directly here
-# would hand that full env to package code -- a compromised
-# @anthropic-ai/claude-code release could read ANTHROPIC_API_KEY /
-# AWS_BEARER_TOKEN_BEDROCK / AZURE_FOUNDRY_API_KEY /
-# AGENT_SHIN_GITHUB_TOKEN from os.environ and exfiltrate them before
-# the proxy or test harness ever starts. Probe under `env -i` with the
-# same minimal allowlist the PR-gate uses (the matrix run itself goes
-# through cli_driver.py, which already scrubs the CLI env).
-#
-# The probe also runs under a fresh empty HOME instead of the runtime
-# user's real $HOME. `ProtectHome=read-only` in the systemd unit
-# blocks *writes* to /home/mateo but still allows reads, so a
-# compromised claude package invoked here with HOME=/home/mateo could
-# read ~/.config/gh/hosts.yml (the gh-host token), ~/.bash_history,
-# or ~/.ssh/. Pointing HOME at a per-run dir under ${WORKDIR} hides
-# those entirely from the subprocess; ${WORKDIR} is rm -rf'd by the
-# script-wide cleanup() trap regardless of probe outcome.
-CLAUDE_PROBE_HOME="${WORKDIR}/claude-probe-home"
-mkdir -p "${CLAUDE_PROBE_HOME}"
-CLAUDE_CODE_VERSION="$(env -i \
- PATH="${PATH}" \
- HOME="${CLAUDE_PROBE_HOME}" \
- USER="${USER:-mateo}" \
- TERM="${TERM:-dumb}" \
- LANG="${LANG:-C.UTF-8}" \
- LC_ALL="${LC_ALL:-}" \
- TMPDIR="${TMPDIR:-/tmp}" \
- claude --version 2>/dev/null \
- | grep -oE '[0-9]+\.[0-9]+\.[0-9]+([.-][A-Za-z0-9.-]+)?' \
- | head -n1 || true)"
-# `|| true` above keeps `set -Eeuo pipefail` from aborting silently when
-# `grep` finds no match (exit 1) — without it the assignment inherits the
-# pipeline's non-zero exit, `set -e` kills the script, and the operator
-# never sees the helpful diagnostic below.
-[[ -n "${CLAUDE_CODE_VERSION}" ]] || die "could not parse semver from 'claude --version'"
-log "local claude code: ${CLAUDE_CODE_VERSION}"
-
-# ---------------------------------------------------------------------------
-# 2. Update the worktree to that tag
-# ---------------------------------------------------------------------------
-
-if [[ ! -d "${WORKTREE}/.git" ]]; then
- log "first run: cloning litellm into ${WORKTREE}"
- mkdir -p "$(dirname "${WORKTREE}")"
- git clone https://github.com/BerriAI/litellm.git "${WORKTREE}"
-fi
-
-log "updating worktree to ${LITELLM_VERSION}"
-git -C "${WORKTREE}" fetch --tags --force
-git -C "${WORKTREE}" reset --hard
-# Keep the venv and the .uv-bin cache around — uv sync will reconcile
-# the venv on every run, and we don't want to re-download the pinned
-# uv binary each time. Drop everything else (including any prior
-# tests/e2e/claude_code/ shim) so each run starts clean before the shim
-# below rewrites it from the dev checkout.
-git -C "${WORKTREE}" clean -fdx -e .venv -e .uv-bin
-git -C "${WORKTREE}" checkout --force "${LITELLM_VERSION}"
-
-# Always overwrite tests/e2e/claude_code/ in the worktree with the copy
-# from the dev checkout, regardless of whether the resolved
-# ${LITELLM_VERSION} tag already ships a tests/e2e/claude_code/ tree of
-# its own. Rationale: the matrix populator's job is to exercise
-# today's tests against the latest stable proxy. The dev checkout
-# carries the most recent test fixes (e.g. the stream-json vision
-# rewrite, the --effort thinking knob, the WebSearch tool_use
-# assertion) that haven't yet rolled into a v*-stable, and we want
-# every cron run to pick those up the moment they land on
-# ${LITELLM_REPO}, not whenever the next stable release happens.
-#
-# Concretely this means a fresh `rm -rf` + `cp -r` every run so the
-# tree is byte-identical to ${LITELLM_REPO}/tests/e2e/claude_code (no
-# stale files left over from the tag's own checkout, no drift across
-# runs).
-if [[ ! -d "${LITELLM_REPO}/tests/e2e/claude_code" ]]; then
- die "no shim source at ${LITELLM_REPO}/tests/e2e/claude_code"
-fi
-log "shimming tests/e2e/claude_code/ from ${LITELLM_REPO} (always-overwrite)"
-rm -rf "${WORKTREE}/tests/e2e/claude_code"
-mkdir -p "${WORKTREE}/tests/e2e"
-cp -r "${LITELLM_REPO}/tests/e2e/claude_code" "${WORKTREE}/tests/e2e/"
-
-# litellm pins an exact uv version in pyproject.toml's [tool.uv]
-# `required-version` field, so a system uv that's newer or older
-# refuses to sync. We pin our own local copy at the version the
-# checked-out tag asks for, cached under .uv-bin/ inside the worktree
-# so subsequent runs skip the download.
-PINNED_UV_VERSION="$(
- awk -F'"' '
- /^required-version[[:space:]]*=/ {
- # Field 2 is the value between the quotes, e.g. ">=0.10.9" or
- # "0.10.9". Strip any leading specifier prefix so we end up with
- # the bare version string, which is what /releases/download//
- # expects.
- v = $2
- sub(/^[[:space:]=<>!~]+/, "", v)
- if (v != "") { print v; exit }
- }
- ' "${WORKTREE}/pyproject.toml"
-)"
-if [[ -z "${PINNED_UV_VERSION}" ]]; then
- log "no uv version pin in pyproject.toml; using system uv"
- WORKTREE_UV="$(command -v uv)"
-else
- WORKTREE_UV="${WORKTREE}/.uv-bin/uv-${PINNED_UV_VERSION}"
- if [[ ! -x "${WORKTREE_UV}" ]]; then
- log "downloading uv ${PINNED_UV_VERSION} for the worktree"
- mkdir -p "${WORKTREE}/.uv-bin"
- # Detect host arch so the same script works on x86_64 GCP VMs and on
- # aarch64 hosts (Astral publishes both `uv-x86_64-unknown-linux-gnu`
- # and `uv-aarch64-unknown-linux-gnu` tarballs under the same release
- # tag, and `uname -m` already returns the exact token uv uses).
- UV_ARCH="$(uname -m)"
- UV_TRIPLE="uv-${UV_ARCH}-unknown-linux-gnu"
- UV_TARBALL_NAME="${UV_TRIPLE}.tar.gz"
- UV_DOWNLOAD_URL="https://github.com/astral-sh/uv/releases/download/${PINNED_UV_VERSION}/${UV_TARBALL_NAME}"
- UV_TMPDIR="$(mktemp -d -t uv-download.XXXXXX)"
- # Download the tarball and Astral's official .sha256 sidecar to disk
- # and verify the digest before extracting/executing anything. This
- # closes the supply-chain trust gap of piping a remote binary
- # straight into `tar -xzO ... > file ; chmod +x` (see CLAUDE.md
- # "CI Supply-Chain Safety").
- curl -fsSL --output "${UV_TMPDIR}/${UV_TARBALL_NAME}" "${UV_DOWNLOAD_URL}"
- curl -fsSL --output "${UV_TMPDIR}/${UV_TARBALL_NAME}.sha256" "${UV_DOWNLOAD_URL}.sha256"
- (cd "${UV_TMPDIR}" && sha256sum -c "${UV_TARBALL_NAME}.sha256") \
- || { rm -rf "${UV_TMPDIR}"; die "uv ${PINNED_UV_VERSION} sha256 mismatch — refusing to install"; }
- tar -xzf "${UV_TMPDIR}/${UV_TARBALL_NAME}" -C "${UV_TMPDIR}" "${UV_TRIPLE}/uv"
- mv "${UV_TMPDIR}/${UV_TRIPLE}/uv" "${WORKTREE_UV}.tmp"
- chmod +x "${WORKTREE_UV}.tmp"
- mv "${WORKTREE_UV}.tmp" "${WORKTREE_UV}"
- rm -rf "${UV_TMPDIR}"
- fi
-fi
-# `--extra proxy` pulls fastapi/uvicorn/etc. so `uv run litellm` can
-# actually serve. `--group proxy-dev` brings in pytest and the rest of
-# what tests/e2e/claude_code/ needs.
-log "uv sync --frozen --group proxy-dev --extra proxy (uv ${PINNED_UV_VERSION:-system})"
-(cd "${WORKTREE}" && "${WORKTREE_UV}" sync --frozen --group proxy-dev --extra proxy)
-
-PROXY_CONFIG="${WORKTREE}/tests/e2e/claude_code/test_config.yaml"
-[[ -f "${PROXY_CONFIG}" ]] || die "proxy config not found at ${PROXY_CONFIG} (does ${LITELLM_VERSION} predate the compat matrix work?)"
-
-# ---------------------------------------------------------------------------
-# 3. Boot the proxy
-# ---------------------------------------------------------------------------
-
-log "starting proxy on 127.0.0.1:${PROXY_PORT}"
-# Bind the proxy to loopback only. The populator proxy is talked to
-# exclusively by the pytest run on the same host (the health check and
-# the test env set `LITELLM_PROXY_BASE_URL=http://127.0.0.1:...`),
-# so there's no reason to expose it on the VM's external interfaces.
-# Without `--host`, `litellm` defaults to 0.0.0.0, which combined with
-# the predictable default `LITELLM_MASTER_KEY=sk-cron-matrix` would
-# allow anything that can reach :${PROXY_PORT} on the VM to authenticate
-# and burn upstream provider credentials.
-#
-# `setsid` puts the proxy in its own session+pgroup so cleanup() can
-# SIGTERM the whole tree by passing the pgid as a negative pid. We
-# write that pid to a file so cleanup() doesn't need to remember a
-# variable that might be stale by the time the trap fires.
-#
-# Pass the master key as a shell-prefix assignment on `setsid` (inherited
-# via the environment) rather than as `env KEY=VAL ...` argv. The argv
-# form would land the literal key in /proc//cmdline, where
-# any local reader (a model-directed `Read` tool call, another user on
-# the VM, a crash dump) could pick it up before the process execs into
-# the litellm child. The shell-prefix form keeps the key out of argv at
-# every layer (setsid → bash → uv → litellm).
-LITELLM_MASTER_KEY="${PROXY_API_KEY}" setsid bash -c '
- echo "$$" > "$0"
- cd "$1"
- exec "$2" run litellm --config "$3" --host 127.0.0.1 --port "$4"
-' "${PROXY_PID_FILE}" "${WORKTREE}" "${WORKTREE_UV}" "${PROXY_CONFIG}" "${PROXY_PORT}" \
- >"${WORKDIR}/proxy.log" 2>&1 &
-disown
-
-HEALTH_URL="http://127.0.0.1:${PROXY_PORT}/health/liveliness"
-for _ in $(seq 1 45); do
- if curl -fsS "${HEALTH_URL}" >/dev/null 2>&1; then
- break
- fi
- sleep 2
-done
-curl -fsS "${HEALTH_URL}" >/dev/null \
- || { tail -50 "${WORKDIR}/proxy.log" >&2; die "proxy did not become healthy"; }
-
-# ---------------------------------------------------------------------------
-# 4. Run pytest
-# ---------------------------------------------------------------------------
-
-RESULTS_JSON="${WORKDIR}/compat-results.json"
-PYTEST_ARGS=(
- tests/e2e/claude_code/
- --ignore=tests/e2e/claude_code/_driver_unit_tests
- --ignore=tests/e2e/claude_code/_builder_unit_tests
- --ignore=tests/e2e/claude_code/_publisher_unit_tests
- --ignore=tests/e2e/claude_code/_pr_gate_unit_tests
-)
-if [[ -n "${PYTEST_K}" ]]; then
- log "PYTEST_K set; narrowing to: ${PYTEST_K}"
- PYTEST_ARGS+=(-k "${PYTEST_K}")
-fi
-
-log "running pytest"
-set +e
-# Pytest only needs to talk to the loopback proxy at 127.0.0.1:${PROXY_PORT}
-# — it has no legitimate reason to see ANTHROPIC_API_KEY /
-# AWS_BEARER_TOKEN_BEDROCK / VERTEXAI_* / AZURE_FOUNDRY_* /
-# AGENT_SHIN_GITHUB_TOKEN / GITHUB_TOKEN in its own env. The systemd
-# unit's EnvironmentFile injects all of those into this script for the
-# proxy to consume, and pytest inherits them by default. Wrap the
-# invocation in `env -i` so:
-#
-# 1. test code under tests/e2e/claude_code/ (or anything it imports)
-# cannot read provider/agent-shin creds out of `os.environ` and
-# exfiltrate them via an outbound call from inside a conftest hook
-# or a fixture (a sibling vector to the model-controlled Bash/Read
-# concern handled by `cli_driver.py`'s own env scrub);
-# 2. a model-directed `Read` tool call during a PDF/vision cell
-# cannot reach /proc//environ and pull the creds out
-# of the parent process the way it can today;
-# 3. this matches the PR-gate pytest step in `.circleci/config.yml`,
-# which already runs under `env -i` with the same minimal
-# allowlist.
-#
-# `cli_driver.py` re-allowlists its own subset (PATH/USER/LOGNAME/etc.)
-# when spawning the `claude` binary, so the CLI still finds Node + the
-# claude shim on PATH and gets a fresh isolated HOME per invocation.
-(
- cd "${WORKTREE}" \
- && env -i \
- PATH="${PATH}" \
- HOME="${HOME}" \
- USER="${USER:-mateo}" \
- TERM="${TERM:-dumb}" \
- LANG="${LANG:-C.UTF-8}" \
- LC_ALL="${LC_ALL:-}" \
- TMPDIR="${TMPDIR:-/tmp}" \
- LITELLM_PROXY_BASE_URL="http://127.0.0.1:${PROXY_PORT}" \
- LITELLM_PROXY_API_KEY="${PROXY_API_KEY}" \
- COMPAT_RESULTS_PATH="${RESULTS_JSON}" \
- "${WORKTREE_UV}" run pytest "${PYTEST_ARGS[@]}"
-)
-PYTEST_EXIT=$?
-set -e
-log "pytest exit code: ${PYTEST_EXIT} (failures become 'fail' cells, not script errors)"
-[[ -f "${RESULTS_JSON}" ]] || die "pytest did not produce ${RESULTS_JSON}"
-
-# ---------------------------------------------------------------------------
-# 5. Build the matrix JSON
-# ---------------------------------------------------------------------------
-
-MATRIX_JSON="${WORKDIR}/compatibility-matrix.json"
-log "building ${MATRIX_JSON}"
-(
- cd "${WORKTREE}" \
- && "${WORKTREE_UV}" run python "${POPULATOR_DIR}/build_matrix.py" \
- --manifest "${WORKTREE}/tests/e2e/claude_code/manifest.yaml" \
- --results "${RESULTS_JSON}" \
- --output "${MATRIX_JSON}" \
- --litellm-version "${LITELLM_VERSION}" \
- --claude-code-version "${CLAUDE_CODE_VERSION}"
-)
-
-# ---------------------------------------------------------------------------
-# 6. Open a docs-repo PR
-# ---------------------------------------------------------------------------
-
-if [[ "${SKIP_PUBLISH}" == "1" ]]; then
- cp "${MATRIX_JSON}" "${LITELLM_REPO}/compatibility-matrix.json"
- log "SKIP_PUBLISH=1; matrix written to ${LITELLM_REPO}/compatibility-matrix.json"
- exit 0
-fi
-
-DATE_UTC="$(date -u +%Y-%m-%d)"
-BRANCH_NAME="compat-matrix/${LITELLM_VERSION}-${CLAUDE_CODE_VERSION}-${DATE_UTC}"
-DOCS_CLONE="${WORKDIR}/litellm-docs"
-FORK_OWNER="${FORK_OWNER:-agent-shin}"
-FORK_REPO="${FORK_REPO:-${FORK_OWNER}/litellm-docs}"
-
-log "cloning ${DOCS_REPO}@${DOCS_BRANCH}"
-# Use the agent-shin token inline rather than the host gh-cli config.
-# `BerriAI/litellm-docs` is a public repo so unauthenticated clone
-# would also work, but passing the token explicitly means the systemd
-# unit can hide `~/.config/gh` (`InaccessiblePaths=`) without breaking
-# this clone — closing the model-directed `Read("/home/mateo/.config/gh/...")`
-# exfiltration path on the cron VM.
-GH_TOKEN="${AGENT_SHIN_GITHUB_TOKEN}" \
- gh repo clone "${DOCS_REPO}" "${DOCS_CLONE}" -- --depth 1 --branch "${DOCS_BRANCH}"
-
-cd "${DOCS_CLONE}"
-git config user.email "litellm-bot@berri.ai"
-git config user.name "litellm-compat-matrix-bot"
-git checkout -b "${BRANCH_NAME}"
-
-mkdir -p "$(dirname "${DOCS_TARGET_PATH}")"
-cp "${MATRIX_JSON}" "${DOCS_TARGET_PATH}"
-git add "${DOCS_TARGET_PATH}"
-
-if git diff --cached --quiet; then
- log "matrix JSON unchanged from ${DOCS_BRANCH}; skipping PR"
- exit 0
-fi
-
-GENERATED_AT="$(jq -r '.generated_at' "${MATRIX_JSON}")"
-COMMIT_MSG="$(cat </dev/null || true
-git remote add fork "${FORK_PUSH_URL}"
-git push --force --set-upstream fork "${BRANCH_NAME}"
-git remote remove fork
-unset FORK_PUSH_URL
-
-# Per-feature status table for the PR body. Reviewers triage from this.
-PR_FEATURE_TABLE="$(jq -r '
- .features[] as $f
- | "- **\($f.name)**: " +
- ([ .providers[] as $p
- | "\($p)=\($f.providers[$p].status // "not_tested")"
- ] | join(", "))
-' "${MATRIX_JSON}")"
-
-PR_TITLE="chore(compat-matrix): refresh for ${LITELLM_VERSION} + claude-code ${CLAUDE_CODE_VERSION}"
-PR_BODY="$(cat < ${DOCS_REPO}:${DOCS_BRANCH}"
-# GH_TOKEN here is scoped to this single subshell so we don't bleed the
-# fork token into the rest of the script (release-listing earlier uses
-# ${GITHUB_TOKEN}, which may be a different identity). gh's --head accepts
-# `OWNER:BRANCH` for cross-repo PRs from a fork.
-#
-# Reviewer assignment is done in a *separate* call below: as the PR
-# author from a fork, agent-shin has no write/triage access on
-# ${DOCS_REPO} and the `RequestReviewsByLogin` GraphQL mutation
-# (which backs `gh pr create --reviewer` and `gh pr edit --add-reviewer`)
-# rejects with "does not have the correct permissions". We use the
-# collaborator-scoped ${GITHUB_TOKEN} for that instead. Don't fold
-# --reviewer into `gh pr create` here -- it would fail the whole
-# create on the very first cron run.
-set +e
-PR_OUT="$(
- GH_TOKEN="${AGENT_SHIN_GITHUB_TOKEN}" gh pr create \
- --repo "${DOCS_REPO}" \
- --base "${DOCS_BRANCH}" \
- --head "${FORK_OWNER}:${BRANCH_NAME}" \
- --title "${PR_TITLE}" \
- --body "${PR_BODY}" 2>&1
-)"
-PR_EXIT=$?
-set -e
-echo "${PR_OUT}"
-
-if [[ ${PR_EXIT} -ne 0 ]]; then
- if grep -q "a pull request for branch.*already exists" <<<"${PR_OUT}"; then
- log "PR already exists for ${FORK_OWNER}:${BRANCH_NAME}; updated branch in place"
- else
- die "gh pr create failed (exit ${PR_EXIT})"
- fi
-fi
-
-# Request reviews from PR_REVIEWERS using the collaborator-scoped
-# ${GITHUB_TOKEN} (mateo-berri's token, already provisioned for release
-# listing). This is idempotent: `gh pr edit --add-reviewer` is a no-op
-# on a user who's already in reviewRequests, and silently re-adds
-# anyone whose prior review was dismissed -- so same-day reruns stay
-# clean. Reviewer-add failures are non-fatal: the matrix JSON has
-# already landed on the PR; the worst case is a manual ping.
-if [[ -n "${PR_REVIEWERS}" ]]; then
- if [[ -z "${GITHUB_TOKEN:-}" ]]; then
- log "WARN: PR_REVIEWERS set but GITHUB_TOKEN missing -- cannot request reviews; skipping"
- else
- log "requesting reviews from: ${PR_REVIEWERS}"
- set +e
- GH_TOKEN="${GITHUB_TOKEN}" gh pr edit \
- "${FORK_OWNER}:${BRANCH_NAME}" \
- --repo "${DOCS_REPO}" \
- --add-reviewer "${PR_REVIEWERS}" 2>&1 | sed 's/^/ /'
- REVIEWER_EXIT=${PIPESTATUS[0]}
- set -e
- if [[ ${REVIEWER_EXIT} -ne 0 ]]; then
- log "WARN: gh pr edit --add-reviewer exited ${REVIEWER_EXIT} (non-fatal)"
- fi
- fi
-fi
-
-log "done"
diff --git a/tests/e2e/claude_code/long_context_1m/test_anthropic.py b/tests/e2e/claude_code/long_context_1m/test_anthropic.py
index fb74d5fd40d8..b9bbd1c2fe77 100644
--- a/tests/e2e/claude_code/long_context_1m/test_anthropic.py
+++ b/tests/e2e/claude_code/long_context_1m/test_anthropic.py
@@ -54,25 +54,23 @@
from __future__ import annotations
-import os
from typing import Sequence
import pytest
+from claude_code._env import require_proxy
from claude_code.cli_driver import (
ClaudeCLIError,
failure_diagnostic,
run_claude_models_parallel,
)
-PROXY_BASE_URL_ENV = "LITELLM_PROXY_BASE_URL"
-PROXY_API_KEY_ENV = "LITELLM_PROXY_API_KEY"
# Haiku 4.5 is excluded -- only Sonnet 4.6 and Opus 4.7 support the
# 1M-context beta. See module docstring for the per-cell-aggregator
# rationale.
ANTHROPIC_MODELS: Sequence[str] = (
- "claude-sonnet-4-6",
+ "claude-sonnet-4-5",
"claude-opus-4-7",
)
@@ -155,26 +153,12 @@ def _build_long_prompt(target_tokens: int = TARGET_INPUT_TOKENS) -> str:
return preamble + "".join(pad_lines) + closing
+@pytest.mark.covers("llm.messages.anthropic.long_context_1m.nonstream.works")
def test_long_context_1m_anthropic(compat_result):
"""Drive the `claude` CLI with a ~210k-token prompt and the
`context-1m-2025-08-07` beta header; assert no 400 / 413 and a
non-empty reply for Sonnet + Opus."""
- base_url = os.environ.get(PROXY_BASE_URL_ENV)
- api_key = os.environ.get(PROXY_API_KEY_ENV)
- if not base_url or not api_key:
- compat_result.set(
- {
- "status": "fail",
- "error": (
- f"missing required env: set {PROXY_BASE_URL_ENV} and "
- f"{PROXY_API_KEY_ENV} to point at a running LiteLLM proxy"
- ),
- }
- )
- pytest.fail(
- f"{PROXY_BASE_URL_ENV} / {PROXY_API_KEY_ENV} not configured",
- pytrace=False,
- )
+ base_url, api_key = require_proxy(compat_result)
long_prompt = _build_long_prompt()
diff --git a/tests/e2e/claude_code/long_context_1m/test_azure.py b/tests/e2e/claude_code/long_context_1m/test_azure.py
index 5800fdadbfce..d62214d27587 100644
--- a/tests/e2e/claude_code/long_context_1m/test_azure.py
+++ b/tests/e2e/claude_code/long_context_1m/test_azure.py
@@ -54,25 +54,23 @@
from __future__ import annotations
-import os
from typing import Sequence
import pytest
+from claude_code._env import require_proxy
from claude_code.cli_driver import (
ClaudeCLIError,
failure_diagnostic,
run_claude_models_parallel,
)
-PROXY_BASE_URL_ENV = "LITELLM_PROXY_BASE_URL"
-PROXY_API_KEY_ENV = "LITELLM_PROXY_API_KEY"
# Haiku 4.5 is excluded -- only Sonnet 4.6 and Opus 4.7 support the
# 1M-context beta. See module docstring for the per-cell-aggregator
# rationale.
AZURE_MODELS: Sequence[str] = (
- "claude-sonnet-4-6-azure",
+ "claude-sonnet-4-5-azure",
"claude-opus-4-7-azure",
)
@@ -155,26 +153,12 @@ def _build_long_prompt(target_tokens: int = TARGET_INPUT_TOKENS) -> str:
return preamble + "".join(pad_lines) + closing
+@pytest.mark.covers("llm.messages.azure_foundry.long_context_1m.nonstream.works")
def test_long_context_1m_azure(compat_result):
"""Drive the `claude` CLI (Azure (Microsoft Foundry)) with a ~210k-token prompt and the
`context-1m-2025-08-07` beta header; assert no 400 / 413 and a
non-empty reply for Sonnet + Opus."""
- base_url = os.environ.get(PROXY_BASE_URL_ENV)
- api_key = os.environ.get(PROXY_API_KEY_ENV)
- if not base_url or not api_key:
- compat_result.set(
- {
- "status": "fail",
- "error": (
- f"missing required env: set {PROXY_BASE_URL_ENV} and "
- f"{PROXY_API_KEY_ENV} to point at a running LiteLLM proxy"
- ),
- }
- )
- pytest.fail(
- f"{PROXY_BASE_URL_ENV} / {PROXY_API_KEY_ENV} not configured",
- pytrace=False,
- )
+ base_url, api_key = require_proxy(compat_result)
long_prompt = _build_long_prompt()
diff --git a/tests/e2e/claude_code/long_context_1m/test_bedrock_converse.py b/tests/e2e/claude_code/long_context_1m/test_bedrock_converse.py
index 18587f7c2d63..3c2fd4f02cc5 100644
--- a/tests/e2e/claude_code/long_context_1m/test_bedrock_converse.py
+++ b/tests/e2e/claude_code/long_context_1m/test_bedrock_converse.py
@@ -54,25 +54,23 @@
from __future__ import annotations
-import os
from typing import Sequence
import pytest
+from claude_code._env import require_proxy
from claude_code.cli_driver import (
ClaudeCLIError,
failure_diagnostic,
run_claude_models_parallel,
)
-PROXY_BASE_URL_ENV = "LITELLM_PROXY_BASE_URL"
-PROXY_API_KEY_ENV = "LITELLM_PROXY_API_KEY"
# Haiku 4.5 is excluded -- only Sonnet 4.6 and Opus 4.7 support the
# 1M-context beta. See module docstring for the per-cell-aggregator
# rationale.
BEDROCK_CONVERSE_MODELS: Sequence[str] = (
- "claude-sonnet-4-6-bedrock-converse",
+ "claude-sonnet-4-5-bedrock-converse",
"claude-opus-4-7-bedrock-converse",
)
@@ -155,26 +153,12 @@ def _build_long_prompt(target_tokens: int = TARGET_INPUT_TOKENS) -> str:
return preamble + "".join(pad_lines) + closing
+@pytest.mark.covers("llm.messages.bedrock_converse.long_context_1m.nonstream.works")
def test_long_context_1m_bedrock_converse(compat_result):
"""Drive the `claude` CLI (Bedrock (Converse)) with a ~210k-token prompt and the
`context-1m-2025-08-07` beta header; assert no 400 / 413 and a
non-empty reply for Sonnet + Opus."""
- base_url = os.environ.get(PROXY_BASE_URL_ENV)
- api_key = os.environ.get(PROXY_API_KEY_ENV)
- if not base_url or not api_key:
- compat_result.set(
- {
- "status": "fail",
- "error": (
- f"missing required env: set {PROXY_BASE_URL_ENV} and "
- f"{PROXY_API_KEY_ENV} to point at a running LiteLLM proxy"
- ),
- }
- )
- pytest.fail(
- f"{PROXY_BASE_URL_ENV} / {PROXY_API_KEY_ENV} not configured",
- pytrace=False,
- )
+ base_url, api_key = require_proxy(compat_result)
long_prompt = _build_long_prompt()
diff --git a/tests/e2e/claude_code/long_context_1m/test_bedrock_invoke.py b/tests/e2e/claude_code/long_context_1m/test_bedrock_invoke.py
index 0270197ce2ac..4801d4057601 100644
--- a/tests/e2e/claude_code/long_context_1m/test_bedrock_invoke.py
+++ b/tests/e2e/claude_code/long_context_1m/test_bedrock_invoke.py
@@ -54,25 +54,23 @@
from __future__ import annotations
-import os
from typing import Sequence
import pytest
+from claude_code._env import require_proxy
from claude_code.cli_driver import (
ClaudeCLIError,
failure_diagnostic,
run_claude_models_parallel,
)
-PROXY_BASE_URL_ENV = "LITELLM_PROXY_BASE_URL"
-PROXY_API_KEY_ENV = "LITELLM_PROXY_API_KEY"
# Haiku 4.5 is excluded -- only Sonnet 4.6 and Opus 4.7 support the
# 1M-context beta. See module docstring for the per-cell-aggregator
# rationale.
BEDROCK_INVOKE_MODELS: Sequence[str] = (
- "claude-sonnet-4-6-bedrock-invoke",
+ "claude-sonnet-4-5-bedrock-invoke",
"claude-opus-4-7-bedrock-invoke",
)
@@ -155,26 +153,12 @@ def _build_long_prompt(target_tokens: int = TARGET_INPUT_TOKENS) -> str:
return preamble + "".join(pad_lines) + closing
+@pytest.mark.covers("llm.messages.bedrock_invoke.long_context_1m.nonstream.works")
def test_long_context_1m_bedrock_invoke(compat_result):
"""Drive the `claude` CLI (Bedrock (Invoke)) with a ~210k-token prompt and the
`context-1m-2025-08-07` beta header; assert no 400 / 413 and a
non-empty reply for Sonnet + Opus."""
- base_url = os.environ.get(PROXY_BASE_URL_ENV)
- api_key = os.environ.get(PROXY_API_KEY_ENV)
- if not base_url or not api_key:
- compat_result.set(
- {
- "status": "fail",
- "error": (
- f"missing required env: set {PROXY_BASE_URL_ENV} and "
- f"{PROXY_API_KEY_ENV} to point at a running LiteLLM proxy"
- ),
- }
- )
- pytest.fail(
- f"{PROXY_BASE_URL_ENV} / {PROXY_API_KEY_ENV} not configured",
- pytrace=False,
- )
+ base_url, api_key = require_proxy(compat_result)
long_prompt = _build_long_prompt()
diff --git a/tests/e2e/claude_code/long_context_1m/test_vertex_ai.py b/tests/e2e/claude_code/long_context_1m/test_vertex_ai.py
index d2db4a1b4eec..efa96bf076dc 100644
--- a/tests/e2e/claude_code/long_context_1m/test_vertex_ai.py
+++ b/tests/e2e/claude_code/long_context_1m/test_vertex_ai.py
@@ -54,25 +54,23 @@
from __future__ import annotations
-import os
from typing import Sequence
import pytest
+from claude_code._env import require_proxy
from claude_code.cli_driver import (
ClaudeCLIError,
failure_diagnostic,
run_claude_models_parallel,
)
-PROXY_BASE_URL_ENV = "LITELLM_PROXY_BASE_URL"
-PROXY_API_KEY_ENV = "LITELLM_PROXY_API_KEY"
# Haiku 4.5 is excluded -- only Sonnet 4.6 and Opus 4.7 support the
# 1M-context beta. See module docstring for the per-cell-aggregator
# rationale.
VERTEX_AI_MODELS: Sequence[str] = (
- "claude-sonnet-4-6-vertex",
+ "claude-sonnet-4-5-vertex",
"claude-opus-4-7-vertex",
)
@@ -155,26 +153,12 @@ def _build_long_prompt(target_tokens: int = TARGET_INPUT_TOKENS) -> str:
return preamble + "".join(pad_lines) + closing
+@pytest.mark.covers("llm.messages.vertex.long_context_1m.nonstream.works")
def test_long_context_1m_vertex_ai(compat_result):
"""Drive the `claude` CLI (Vertex AI) with a ~210k-token prompt and the
`context-1m-2025-08-07` beta header; assert no 400 / 413 and a
non-empty reply for Sonnet + Opus."""
- base_url = os.environ.get(PROXY_BASE_URL_ENV)
- api_key = os.environ.get(PROXY_API_KEY_ENV)
- if not base_url or not api_key:
- compat_result.set(
- {
- "status": "fail",
- "error": (
- f"missing required env: set {PROXY_BASE_URL_ENV} and "
- f"{PROXY_API_KEY_ENV} to point at a running LiteLLM proxy"
- ),
- }
- )
- pytest.fail(
- f"{PROXY_BASE_URL_ENV} / {PROXY_API_KEY_ENV} not configured",
- pytrace=False,
- )
+ base_url, api_key = require_proxy(compat_result)
long_prompt = _build_long_prompt()
diff --git a/tests/e2e/claude_code/matrix_builder.py b/tests/e2e/claude_code/matrix_builder.py
index 5641e488da28..d9a13d17ea48 100644
--- a/tests/e2e/claude_code/matrix_builder.py
+++ b/tests/e2e/claude_code/matrix_builder.py
@@ -183,7 +183,7 @@ def build_from_paths(
generated_at: str,
output_path: Optional[Path] = None,
) -> Dict[str, Any]:
- """I/O wrapper around build_matrix used by the publisher script."""
+ """I/O wrapper around ``build_matrix``: reads the manifest and per-test results from disk, calls ``build_matrix``, and (optionally) writes the compat-matrix JSON to ``output_path``. Whatever orchestrator publishes the matrix (currently the ECR image) invokes this."""
manifest = load_manifest(manifest_path)
results = load_results(results_path)
matrix = build_matrix(
diff --git a/tests/e2e/claude_code/pdf_input/test_anthropic.py b/tests/e2e/claude_code/pdf_input/test_anthropic.py
index 36fb69a1db6e..21c8028ef1c5 100644
--- a/tests/e2e/claude_code/pdf_input/test_anthropic.py
+++ b/tests/e2e/claude_code/pdf_input/test_anthropic.py
@@ -22,22 +22,19 @@
from __future__ import annotations
-import os
-
import pytest
+from claude_code._env import require_proxy
from claude_code.cli_driver import (
ClaudeCLIError,
failure_diagnostic,
run_claude_models_parallel,
)
-PROXY_BASE_URL_ENV = "LITELLM_PROXY_BASE_URL"
-PROXY_API_KEY_ENV = "LITELLM_PROXY_API_KEY"
ANTHROPIC_MODELS = [
"claude-haiku-4-5",
- "claude-sonnet-4-6",
+ "claude-sonnet-4-5",
"claude-opus-4-7",
]
@@ -108,24 +105,11 @@ def _build_minimal_pdf(marker: str) -> bytes:
return bytes(out)
+@pytest.mark.covers("llm.messages.anthropic.pdf_input.nonstream.works")
def test_pdf_input_anthropic(compat_result, tmp_path):
"""Drive the `claude` CLI against the LiteLLM proxy with a PDF
attached via the Read tool and assert the reply references it."""
- base_url = os.environ.get(PROXY_BASE_URL_ENV)
- api_key = os.environ.get(PROXY_API_KEY_ENV)
- if not base_url or not api_key:
- compat_result.set(
- {
- "status": "fail",
- "error": (
- f"missing required env: set {PROXY_BASE_URL_ENV} and "
- f"{PROXY_API_KEY_ENV} to point at a running LiteLLM proxy"
- ),
- }
- )
- pytest.fail(
- f"{PROXY_BASE_URL_ENV} / {PROXY_API_KEY_ENV} not configured", pytrace=False
- )
+ base_url, api_key = require_proxy(compat_result)
pdf_path = tmp_path / "marker.pdf"
pdf_path.write_bytes(_build_minimal_pdf(PDF_MARKER))
diff --git a/tests/e2e/claude_code/pdf_input/test_azure.py b/tests/e2e/claude_code/pdf_input/test_azure.py
index 810c857e407e..34ae3732b996 100644
--- a/tests/e2e/claude_code/pdf_input/test_azure.py
+++ b/tests/e2e/claude_code/pdf_input/test_azure.py
@@ -15,22 +15,19 @@
from __future__ import annotations
-import os
-
import pytest
+from claude_code._env import require_proxy
from claude_code.cli_driver import (
ClaudeCLIError,
failure_diagnostic,
run_claude_models_parallel,
)
-PROXY_BASE_URL_ENV = "LITELLM_PROXY_BASE_URL"
-PROXY_API_KEY_ENV = "LITELLM_PROXY_API_KEY"
AZURE_MODELS = [
"claude-haiku-4-5-azure",
- "claude-sonnet-4-6-azure",
+ "claude-sonnet-4-5-azure",
"claude-opus-4-7-azure",
]
@@ -85,22 +82,9 @@ def _build_minimal_pdf(marker: str) -> bytes:
return bytes(out)
+@pytest.mark.covers("llm.messages.azure_foundry.pdf_input.nonstream.works")
def test_pdf_input_azure(compat_result, tmp_path):
- base_url = os.environ.get(PROXY_BASE_URL_ENV)
- api_key = os.environ.get(PROXY_API_KEY_ENV)
- if not base_url or not api_key:
- compat_result.set(
- {
- "status": "fail",
- "error": (
- f"missing required env: set {PROXY_BASE_URL_ENV} and "
- f"{PROXY_API_KEY_ENV} to point at a running LiteLLM proxy"
- ),
- }
- )
- pytest.fail(
- f"{PROXY_BASE_URL_ENV} / {PROXY_API_KEY_ENV} not configured", pytrace=False
- )
+ base_url, api_key = require_proxy(compat_result)
pdf_path = tmp_path / "marker.pdf"
pdf_path.write_bytes(_build_minimal_pdf(PDF_MARKER))
diff --git a/tests/e2e/claude_code/pdf_input/test_bedrock_converse.py b/tests/e2e/claude_code/pdf_input/test_bedrock_converse.py
index 191a27c6d463..76aa84f0f476 100644
--- a/tests/e2e/claude_code/pdf_input/test_bedrock_converse.py
+++ b/tests/e2e/claude_code/pdf_input/test_bedrock_converse.py
@@ -21,22 +21,19 @@
from __future__ import annotations
-import os
-
import pytest
+from claude_code._env import require_proxy
from claude_code.cli_driver import (
ClaudeCLIError,
failure_diagnostic,
run_claude_models_parallel,
)
-PROXY_BASE_URL_ENV = "LITELLM_PROXY_BASE_URL"
-PROXY_API_KEY_ENV = "LITELLM_PROXY_API_KEY"
BEDROCK_CONVERSE_MODELS = [
"claude-haiku-4-5-bedrock-converse",
- "claude-sonnet-4-6-bedrock-converse",
+ "claude-sonnet-4-5-bedrock-converse",
"claude-opus-4-7-bedrock-converse",
]
@@ -91,22 +88,9 @@ def _build_minimal_pdf(marker: str) -> bytes:
return bytes(out)
+@pytest.mark.covers("llm.messages.bedrock_converse.pdf_input.nonstream.works")
def test_pdf_input_bedrock_converse(compat_result, tmp_path):
- base_url = os.environ.get(PROXY_BASE_URL_ENV)
- api_key = os.environ.get(PROXY_API_KEY_ENV)
- if not base_url or not api_key:
- compat_result.set(
- {
- "status": "fail",
- "error": (
- f"missing required env: set {PROXY_BASE_URL_ENV} and "
- f"{PROXY_API_KEY_ENV} to point at a running LiteLLM proxy"
- ),
- }
- )
- pytest.fail(
- f"{PROXY_BASE_URL_ENV} / {PROXY_API_KEY_ENV} not configured", pytrace=False
- )
+ base_url, api_key = require_proxy(compat_result)
pdf_path = tmp_path / "marker.pdf"
pdf_path.write_bytes(_build_minimal_pdf(PDF_MARKER))
diff --git a/tests/e2e/claude_code/pdf_input/test_bedrock_invoke.py b/tests/e2e/claude_code/pdf_input/test_bedrock_invoke.py
index 163cabb45a00..4450266bb6b4 100644
--- a/tests/e2e/claude_code/pdf_input/test_bedrock_invoke.py
+++ b/tests/e2e/claude_code/pdf_input/test_bedrock_invoke.py
@@ -20,22 +20,19 @@
from __future__ import annotations
-import os
-
import pytest
+from claude_code._env import require_proxy
from claude_code.cli_driver import (
ClaudeCLIError,
failure_diagnostic,
run_claude_models_parallel,
)
-PROXY_BASE_URL_ENV = "LITELLM_PROXY_BASE_URL"
-PROXY_API_KEY_ENV = "LITELLM_PROXY_API_KEY"
BEDROCK_INVOKE_MODELS = [
"claude-haiku-4-5-bedrock-invoke",
- "claude-sonnet-4-6-bedrock-invoke",
+ "claude-sonnet-4-5-bedrock-invoke",
"claude-opus-4-7-bedrock-invoke",
]
@@ -90,22 +87,9 @@ def _build_minimal_pdf(marker: str) -> bytes:
return bytes(out)
+@pytest.mark.covers("llm.messages.bedrock_invoke.pdf_input.nonstream.works")
def test_pdf_input_bedrock_invoke(compat_result, tmp_path):
- base_url = os.environ.get(PROXY_BASE_URL_ENV)
- api_key = os.environ.get(PROXY_API_KEY_ENV)
- if not base_url or not api_key:
- compat_result.set(
- {
- "status": "fail",
- "error": (
- f"missing required env: set {PROXY_BASE_URL_ENV} and "
- f"{PROXY_API_KEY_ENV} to point at a running LiteLLM proxy"
- ),
- }
- )
- pytest.fail(
- f"{PROXY_BASE_URL_ENV} / {PROXY_API_KEY_ENV} not configured", pytrace=False
- )
+ base_url, api_key = require_proxy(compat_result)
pdf_path = tmp_path / "marker.pdf"
pdf_path.write_bytes(_build_minimal_pdf(PDF_MARKER))
diff --git a/tests/e2e/claude_code/pdf_input/test_vertex_ai.py b/tests/e2e/claude_code/pdf_input/test_vertex_ai.py
index 0d0573d05b37..b78f58cfda18 100644
--- a/tests/e2e/claude_code/pdf_input/test_vertex_ai.py
+++ b/tests/e2e/claude_code/pdf_input/test_vertex_ai.py
@@ -15,22 +15,19 @@
from __future__ import annotations
-import os
-
import pytest
+from claude_code._env import require_proxy
from claude_code.cli_driver import (
ClaudeCLIError,
failure_diagnostic,
run_claude_models_parallel,
)
-PROXY_BASE_URL_ENV = "LITELLM_PROXY_BASE_URL"
-PROXY_API_KEY_ENV = "LITELLM_PROXY_API_KEY"
VERTEX_AI_MODELS = [
"claude-haiku-4-5-vertex",
- "claude-sonnet-4-6-vertex",
+ "claude-sonnet-4-5-vertex",
"claude-opus-4-7-vertex",
]
@@ -85,22 +82,9 @@ def _build_minimal_pdf(marker: str) -> bytes:
return bytes(out)
+@pytest.mark.covers("llm.messages.vertex.pdf_input.nonstream.works")
def test_pdf_input_vertex_ai(compat_result, tmp_path):
- base_url = os.environ.get(PROXY_BASE_URL_ENV)
- api_key = os.environ.get(PROXY_API_KEY_ENV)
- if not base_url or not api_key:
- compat_result.set(
- {
- "status": "fail",
- "error": (
- f"missing required env: set {PROXY_BASE_URL_ENV} and "
- f"{PROXY_API_KEY_ENV} to point at a running LiteLLM proxy"
- ),
- }
- )
- pytest.fail(
- f"{PROXY_BASE_URL_ENV} / {PROXY_API_KEY_ENV} not configured", pytrace=False
- )
+ base_url, api_key = require_proxy(compat_result)
pdf_path = tmp_path / "marker.pdf"
pdf_path.write_bytes(_build_minimal_pdf(PDF_MARKER))
diff --git a/tests/e2e/claude_code/prompt_caching_1h/test_anthropic.py b/tests/e2e/claude_code/prompt_caching_1h/test_anthropic.py
index d81887231d87..637be1c551d8 100644
--- a/tests/e2e/claude_code/prompt_caching_1h/test_anthropic.py
+++ b/tests/e2e/claude_code/prompt_caching_1h/test_anthropic.py
@@ -24,23 +24,21 @@
from __future__ import annotations
-import os
from typing import Any, Mapping, Optional
import pytest
+from claude_code._env import require_proxy
from claude_code.cli_driver import (
ClaudeCLIError,
failure_diagnostic,
run_claude_models_parallel,
)
-PROXY_BASE_URL_ENV = "LITELLM_PROXY_BASE_URL"
-PROXY_API_KEY_ENV = "LITELLM_PROXY_API_KEY"
ANTHROPIC_MODELS = [
"claude-haiku-4-5",
- "claude-sonnet-4-6",
+ "claude-sonnet-4-5",
"claude-opus-4-7",
]
@@ -65,25 +63,12 @@ def _cache_tokens(usage: Optional[Mapping[str, Any]]) -> int:
return 0
+@pytest.mark.covers("llm.messages.anthropic.prompt_cache_1h.nonstream.works")
def test_prompt_caching_1h_anthropic(compat_result):
"""Drive the `claude` CLI against the LiteLLM proxy with the 1h
TTL opt-in env var set, and assert the upstream usage block
surfaces a non-zero cache token count."""
- base_url = os.environ.get(PROXY_BASE_URL_ENV)
- api_key = os.environ.get(PROXY_API_KEY_ENV)
- if not base_url or not api_key:
- compat_result.set(
- {
- "status": "fail",
- "error": (
- f"missing required env: set {PROXY_BASE_URL_ENV} and "
- f"{PROXY_API_KEY_ENV} to point at a running LiteLLM proxy"
- ),
- }
- )
- pytest.fail(
- f"{PROXY_BASE_URL_ENV} / {PROXY_API_KEY_ENV} not configured", pytrace=False
- )
+ base_url, api_key = require_proxy(compat_result)
outcomes = run_claude_models_parallel(
models=ANTHROPIC_MODELS,
diff --git a/tests/e2e/claude_code/prompt_caching_1h/test_azure.py b/tests/e2e/claude_code/prompt_caching_1h/test_azure.py
index 416757f86916..f34557b3c5fe 100644
--- a/tests/e2e/claude_code/prompt_caching_1h/test_azure.py
+++ b/tests/e2e/claude_code/prompt_caching_1h/test_azure.py
@@ -15,23 +15,21 @@
from __future__ import annotations
-import os
from typing import Any, Mapping, Optional
import pytest
+from claude_code._env import require_proxy
from claude_code.cli_driver import (
ClaudeCLIError,
failure_diagnostic,
run_claude_models_parallel,
)
-PROXY_BASE_URL_ENV = "LITELLM_PROXY_BASE_URL"
-PROXY_API_KEY_ENV = "LITELLM_PROXY_API_KEY"
AZURE_MODELS = [
"claude-haiku-4-5-azure",
- "claude-sonnet-4-6-azure",
+ "claude-sonnet-4-5-azure",
"claude-opus-4-7-azure",
]
@@ -49,22 +47,9 @@ def _cache_tokens(usage: Optional[Mapping[str, Any]]) -> int:
return 0
+@pytest.mark.covers("llm.messages.azure_foundry.prompt_cache_1h.nonstream.works")
def test_prompt_caching_1h_azure(compat_result):
- base_url = os.environ.get(PROXY_BASE_URL_ENV)
- api_key = os.environ.get(PROXY_API_KEY_ENV)
- if not base_url or not api_key:
- compat_result.set(
- {
- "status": "fail",
- "error": (
- f"missing required env: set {PROXY_BASE_URL_ENV} and "
- f"{PROXY_API_KEY_ENV} to point at a running LiteLLM proxy"
- ),
- }
- )
- pytest.fail(
- f"{PROXY_BASE_URL_ENV} / {PROXY_API_KEY_ENV} not configured", pytrace=False
- )
+ base_url, api_key = require_proxy(compat_result)
outcomes = run_claude_models_parallel(
models=AZURE_MODELS,
diff --git a/tests/e2e/claude_code/prompt_caching_1h/test_bedrock_converse.py b/tests/e2e/claude_code/prompt_caching_1h/test_bedrock_converse.py
index 5bc632c6f1b5..bf62a49444c0 100644
--- a/tests/e2e/claude_code/prompt_caching_1h/test_bedrock_converse.py
+++ b/tests/e2e/claude_code/prompt_caching_1h/test_bedrock_converse.py
@@ -20,23 +20,21 @@
from __future__ import annotations
-import os
from typing import Any, Mapping, Optional
import pytest
+from claude_code._env import require_proxy
from claude_code.cli_driver import (
ClaudeCLIError,
failure_diagnostic,
run_claude_models_parallel,
)
-PROXY_BASE_URL_ENV = "LITELLM_PROXY_BASE_URL"
-PROXY_API_KEY_ENV = "LITELLM_PROXY_API_KEY"
BEDROCK_CONVERSE_MODELS = [
"claude-haiku-4-5-bedrock-converse",
- "claude-sonnet-4-6-bedrock-converse",
+ "claude-sonnet-4-5-bedrock-converse",
"claude-opus-4-7-bedrock-converse",
]
@@ -57,22 +55,9 @@ def _cache_tokens(usage: Optional[Mapping[str, Any]]) -> int:
return 0
+@pytest.mark.covers("llm.messages.bedrock_converse.prompt_cache_1h.nonstream.works")
def test_prompt_caching_1h_bedrock_converse(compat_result):
- base_url = os.environ.get(PROXY_BASE_URL_ENV)
- api_key = os.environ.get(PROXY_API_KEY_ENV)
- if not base_url or not api_key:
- compat_result.set(
- {
- "status": "fail",
- "error": (
- f"missing required env: set {PROXY_BASE_URL_ENV} and "
- f"{PROXY_API_KEY_ENV} to point at a running LiteLLM proxy"
- ),
- }
- )
- pytest.fail(
- f"{PROXY_BASE_URL_ENV} / {PROXY_API_KEY_ENV} not configured", pytrace=False
- )
+ base_url, api_key = require_proxy(compat_result)
outcomes = run_claude_models_parallel(
models=BEDROCK_CONVERSE_MODELS,
diff --git a/tests/e2e/claude_code/prompt_caching_1h/test_bedrock_invoke.py b/tests/e2e/claude_code/prompt_caching_1h/test_bedrock_invoke.py
index 4501834956bb..dc3468702d44 100644
--- a/tests/e2e/claude_code/prompt_caching_1h/test_bedrock_invoke.py
+++ b/tests/e2e/claude_code/prompt_caching_1h/test_bedrock_invoke.py
@@ -22,23 +22,21 @@
from __future__ import annotations
-import os
from typing import Any, Mapping, Optional
import pytest
+from claude_code._env import require_proxy
from claude_code.cli_driver import (
ClaudeCLIError,
failure_diagnostic,
run_claude_models_parallel,
)
-PROXY_BASE_URL_ENV = "LITELLM_PROXY_BASE_URL"
-PROXY_API_KEY_ENV = "LITELLM_PROXY_API_KEY"
BEDROCK_INVOKE_MODELS = [
"claude-haiku-4-5-bedrock-invoke",
- "claude-sonnet-4-6-bedrock-invoke",
+ "claude-sonnet-4-5-bedrock-invoke",
"claude-opus-4-7-bedrock-invoke",
]
@@ -61,22 +59,9 @@ def _cache_tokens(usage: Optional[Mapping[str, Any]]) -> int:
return 0
+@pytest.mark.covers("llm.messages.bedrock_invoke.prompt_cache_1h.nonstream.works")
def test_prompt_caching_1h_bedrock_invoke(compat_result):
- base_url = os.environ.get(PROXY_BASE_URL_ENV)
- api_key = os.environ.get(PROXY_API_KEY_ENV)
- if not base_url or not api_key:
- compat_result.set(
- {
- "status": "fail",
- "error": (
- f"missing required env: set {PROXY_BASE_URL_ENV} and "
- f"{PROXY_API_KEY_ENV} to point at a running LiteLLM proxy"
- ),
- }
- )
- pytest.fail(
- f"{PROXY_BASE_URL_ENV} / {PROXY_API_KEY_ENV} not configured", pytrace=False
- )
+ base_url, api_key = require_proxy(compat_result)
outcomes = run_claude_models_parallel(
models=BEDROCK_INVOKE_MODELS,
diff --git a/tests/e2e/claude_code/prompt_caching_1h/test_vertex_ai.py b/tests/e2e/claude_code/prompt_caching_1h/test_vertex_ai.py
index 09ded634b451..66cf961fcfc0 100644
--- a/tests/e2e/claude_code/prompt_caching_1h/test_vertex_ai.py
+++ b/tests/e2e/claude_code/prompt_caching_1h/test_vertex_ai.py
@@ -15,23 +15,21 @@
from __future__ import annotations
-import os
from typing import Any, Mapping, Optional
import pytest
+from claude_code._env import require_proxy
from claude_code.cli_driver import (
ClaudeCLIError,
failure_diagnostic,
run_claude_models_parallel,
)
-PROXY_BASE_URL_ENV = "LITELLM_PROXY_BASE_URL"
-PROXY_API_KEY_ENV = "LITELLM_PROXY_API_KEY"
VERTEX_AI_MODELS = [
"claude-haiku-4-5-vertex",
- "claude-sonnet-4-6-vertex",
+ "claude-sonnet-4-5-vertex",
"claude-opus-4-7-vertex",
]
@@ -49,22 +47,9 @@ def _cache_tokens(usage: Optional[Mapping[str, Any]]) -> int:
return 0
+@pytest.mark.covers("llm.messages.vertex.prompt_cache_1h.nonstream.works")
def test_prompt_caching_1h_vertex_ai(compat_result):
- base_url = os.environ.get(PROXY_BASE_URL_ENV)
- api_key = os.environ.get(PROXY_API_KEY_ENV)
- if not base_url or not api_key:
- compat_result.set(
- {
- "status": "fail",
- "error": (
- f"missing required env: set {PROXY_BASE_URL_ENV} and "
- f"{PROXY_API_KEY_ENV} to point at a running LiteLLM proxy"
- ),
- }
- )
- pytest.fail(
- f"{PROXY_BASE_URL_ENV} / {PROXY_API_KEY_ENV} not configured", pytrace=False
- )
+ base_url, api_key = require_proxy(compat_result)
outcomes = run_claude_models_parallel(
models=VERTEX_AI_MODELS,
diff --git a/tests/e2e/claude_code/prompt_caching_5m/test_anthropic.py b/tests/e2e/claude_code/prompt_caching_5m/test_anthropic.py
index 4b20a65f31b8..ef551beb45c3 100644
--- a/tests/e2e/claude_code/prompt_caching_5m/test_anthropic.py
+++ b/tests/e2e/claude_code/prompt_caching_5m/test_anthropic.py
@@ -22,23 +22,21 @@
from __future__ import annotations
-import os
from typing import Any, Mapping, Optional
import pytest
+from claude_code._env import require_proxy
from claude_code.cli_driver import (
ClaudeCLIError,
failure_diagnostic,
run_claude_models_parallel,
)
-PROXY_BASE_URL_ENV = "LITELLM_PROXY_BASE_URL"
-PROXY_API_KEY_ENV = "LITELLM_PROXY_API_KEY"
ANTHROPIC_MODELS = [
"claude-haiku-4-5",
- "claude-sonnet-4-6",
+ "claude-sonnet-4-5",
"claude-opus-4-7",
]
@@ -56,24 +54,11 @@ def _cache_tokens(usage: Optional[Mapping[str, Any]]) -> int:
return 0
+@pytest.mark.covers("llm.messages.anthropic.prompt_cache_5m.nonstream.works")
def test_prompt_caching_5m_anthropic(compat_result):
"""Drive the `claude` CLI against the LiteLLM proxy and assert the
upstream usage block surfaces a non-zero cache token count."""
- base_url = os.environ.get(PROXY_BASE_URL_ENV)
- api_key = os.environ.get(PROXY_API_KEY_ENV)
- if not base_url or not api_key:
- compat_result.set(
- {
- "status": "fail",
- "error": (
- f"missing required env: set {PROXY_BASE_URL_ENV} and "
- f"{PROXY_API_KEY_ENV} to point at a running LiteLLM proxy"
- ),
- }
- )
- pytest.fail(
- f"{PROXY_BASE_URL_ENV} / {PROXY_API_KEY_ENV} not configured", pytrace=False
- )
+ base_url, api_key = require_proxy(compat_result)
outcomes = run_claude_models_parallel(
models=ANTHROPIC_MODELS,
diff --git a/tests/e2e/claude_code/prompt_caching_5m/test_azure.py b/tests/e2e/claude_code/prompt_caching_5m/test_azure.py
index 22bd5aa70483..9d4137e07265 100644
--- a/tests/e2e/claude_code/prompt_caching_5m/test_azure.py
+++ b/tests/e2e/claude_code/prompt_caching_5m/test_azure.py
@@ -22,23 +22,21 @@
from __future__ import annotations
-import os
from typing import Any, Mapping, Optional
import pytest
+from claude_code._env import require_proxy
from claude_code.cli_driver import (
ClaudeCLIError,
failure_diagnostic,
run_claude_models_parallel,
)
-PROXY_BASE_URL_ENV = "LITELLM_PROXY_BASE_URL"
-PROXY_API_KEY_ENV = "LITELLM_PROXY_API_KEY"
AZURE_MODELS = [
"claude-haiku-4-5-azure",
- "claude-sonnet-4-6-azure",
+ "claude-sonnet-4-5-azure",
"claude-opus-4-7-azure",
]
@@ -54,24 +52,11 @@ def _cache_tokens(usage: Optional[Mapping[str, Any]]) -> int:
return 0
+@pytest.mark.covers("llm.messages.azure_foundry.prompt_cache_5m.nonstream.works")
def test_prompt_caching_5m_azure(compat_result):
"""Drive the `claude` CLI against the LiteLLM proxy and assert the
upstream usage block surfaces a non-zero cache token count."""
- base_url = os.environ.get(PROXY_BASE_URL_ENV)
- api_key = os.environ.get(PROXY_API_KEY_ENV)
- if not base_url or not api_key:
- compat_result.set(
- {
- "status": "fail",
- "error": (
- f"missing required env: set {PROXY_BASE_URL_ENV} and "
- f"{PROXY_API_KEY_ENV} to point at a running LiteLLM proxy"
- ),
- }
- )
- pytest.fail(
- f"{PROXY_BASE_URL_ENV} / {PROXY_API_KEY_ENV} not configured", pytrace=False
- )
+ base_url, api_key = require_proxy(compat_result)
outcomes = run_claude_models_parallel(
models=AZURE_MODELS,
diff --git a/tests/e2e/claude_code/prompt_caching_5m/test_bedrock_converse.py b/tests/e2e/claude_code/prompt_caching_5m/test_bedrock_converse.py
index 681a6ecce10b..c9b34c010b08 100644
--- a/tests/e2e/claude_code/prompt_caching_5m/test_bedrock_converse.py
+++ b/tests/e2e/claude_code/prompt_caching_5m/test_bedrock_converse.py
@@ -15,23 +15,21 @@
from __future__ import annotations
-import os
from typing import Any, Mapping, Optional
import pytest
+from claude_code._env import require_proxy
from claude_code.cli_driver import (
ClaudeCLIError,
failure_diagnostic,
run_claude_models_parallel,
)
-PROXY_BASE_URL_ENV = "LITELLM_PROXY_BASE_URL"
-PROXY_API_KEY_ENV = "LITELLM_PROXY_API_KEY"
BEDROCK_CONVERSE_MODELS = [
"claude-haiku-4-5-bedrock-converse",
- "claude-sonnet-4-6-bedrock-converse",
+ "claude-sonnet-4-5-bedrock-converse",
"claude-opus-4-7-bedrock-converse",
]
@@ -47,24 +45,11 @@ def _cache_tokens(usage: Optional[Mapping[str, Any]]) -> int:
return 0
+@pytest.mark.covers("llm.messages.bedrock_converse.prompt_cache_5m.nonstream.works")
def test_prompt_caching_5m_bedrock_converse(compat_result):
"""Drive the `claude` CLI against the LiteLLM proxy and assert the
upstream usage block surfaces a non-zero cache token count."""
- base_url = os.environ.get(PROXY_BASE_URL_ENV)
- api_key = os.environ.get(PROXY_API_KEY_ENV)
- if not base_url or not api_key:
- compat_result.set(
- {
- "status": "fail",
- "error": (
- f"missing required env: set {PROXY_BASE_URL_ENV} and "
- f"{PROXY_API_KEY_ENV} to point at a running LiteLLM proxy"
- ),
- }
- )
- pytest.fail(
- f"{PROXY_BASE_URL_ENV} / {PROXY_API_KEY_ENV} not configured", pytrace=False
- )
+ base_url, api_key = require_proxy(compat_result)
outcomes = run_claude_models_parallel(
models=BEDROCK_CONVERSE_MODELS,
diff --git a/tests/e2e/claude_code/prompt_caching_5m/test_bedrock_invoke.py b/tests/e2e/claude_code/prompt_caching_5m/test_bedrock_invoke.py
index f1a3109b3a1e..b95c509ba3c5 100644
--- a/tests/e2e/claude_code/prompt_caching_5m/test_bedrock_invoke.py
+++ b/tests/e2e/claude_code/prompt_caching_5m/test_bedrock_invoke.py
@@ -15,23 +15,21 @@
from __future__ import annotations
-import os
from typing import Any, Mapping, Optional
import pytest
+from claude_code._env import require_proxy
from claude_code.cli_driver import (
ClaudeCLIError,
failure_diagnostic,
run_claude_models_parallel,
)
-PROXY_BASE_URL_ENV = "LITELLM_PROXY_BASE_URL"
-PROXY_API_KEY_ENV = "LITELLM_PROXY_API_KEY"
BEDROCK_INVOKE_MODELS = [
"claude-haiku-4-5-bedrock-invoke",
- "claude-sonnet-4-6-bedrock-invoke",
+ "claude-sonnet-4-5-bedrock-invoke",
"claude-opus-4-7-bedrock-invoke",
]
@@ -47,24 +45,11 @@ def _cache_tokens(usage: Optional[Mapping[str, Any]]) -> int:
return 0
+@pytest.mark.covers("llm.messages.bedrock_invoke.prompt_cache_5m.nonstream.works")
def test_prompt_caching_5m_bedrock_invoke(compat_result):
"""Drive the `claude` CLI against the LiteLLM proxy and assert the
upstream usage block surfaces a non-zero cache token count."""
- base_url = os.environ.get(PROXY_BASE_URL_ENV)
- api_key = os.environ.get(PROXY_API_KEY_ENV)
- if not base_url or not api_key:
- compat_result.set(
- {
- "status": "fail",
- "error": (
- f"missing required env: set {PROXY_BASE_URL_ENV} and "
- f"{PROXY_API_KEY_ENV} to point at a running LiteLLM proxy"
- ),
- }
- )
- pytest.fail(
- f"{PROXY_BASE_URL_ENV} / {PROXY_API_KEY_ENV} not configured", pytrace=False
- )
+ base_url, api_key = require_proxy(compat_result)
outcomes = run_claude_models_parallel(
models=BEDROCK_INVOKE_MODELS,
diff --git a/tests/e2e/claude_code/prompt_caching_5m/test_vertex_ai.py b/tests/e2e/claude_code/prompt_caching_5m/test_vertex_ai.py
index cc5d337dfbec..f79377b73721 100644
--- a/tests/e2e/claude_code/prompt_caching_5m/test_vertex_ai.py
+++ b/tests/e2e/claude_code/prompt_caching_5m/test_vertex_ai.py
@@ -15,23 +15,21 @@
from __future__ import annotations
-import os
from typing import Any, Mapping, Optional
import pytest
+from claude_code._env import require_proxy
from claude_code.cli_driver import (
ClaudeCLIError,
failure_diagnostic,
run_claude_models_parallel,
)
-PROXY_BASE_URL_ENV = "LITELLM_PROXY_BASE_URL"
-PROXY_API_KEY_ENV = "LITELLM_PROXY_API_KEY"
VERTEX_AI_MODELS = [
"claude-haiku-4-5-vertex",
- "claude-sonnet-4-6-vertex",
+ "claude-sonnet-4-5-vertex",
"claude-opus-4-7-vertex",
]
@@ -47,24 +45,11 @@ def _cache_tokens(usage: Optional[Mapping[str, Any]]) -> int:
return 0
+@pytest.mark.covers("llm.messages.vertex.prompt_cache_5m.nonstream.works")
def test_prompt_caching_5m_vertex_ai(compat_result):
"""Drive the `claude` CLI against the LiteLLM proxy and assert the
upstream usage block surfaces a non-zero cache token count."""
- base_url = os.environ.get(PROXY_BASE_URL_ENV)
- api_key = os.environ.get(PROXY_API_KEY_ENV)
- if not base_url or not api_key:
- compat_result.set(
- {
- "status": "fail",
- "error": (
- f"missing required env: set {PROXY_BASE_URL_ENV} and "
- f"{PROXY_API_KEY_ENV} to point at a running LiteLLM proxy"
- ),
- }
- )
- pytest.fail(
- f"{PROXY_BASE_URL_ENV} / {PROXY_API_KEY_ENV} not configured", pytrace=False
- )
+ base_url, api_key = require_proxy(compat_result)
outcomes = run_claude_models_parallel(
models=VERTEX_AI_MODELS,
diff --git a/tests/e2e/claude_code/run_compat.sh b/tests/e2e/claude_code/run_compat.sh
index 4d8d0b6d7b26..07e6ca368f4c 100755
--- a/tests/e2e/claude_code/run_compat.sh
+++ b/tests/e2e/claude_code/run_compat.sh
@@ -11,9 +11,10 @@
# 4. If a provider has `rate_limited > 0`, halve its rate; else, double it.
# 5. Repeat until the highest no-429 rate is found.
#
-# Required env (proxy connection):
-# LITELLM_PROXY_BASE_URL e.g. http://localhost:4000
-# LITELLM_PROXY_API_KEY e.g. sk-1234
+# Required env (proxy connection). Either the primary suite-wide names
+# or the legacy claude_code-specific names; primary wins on tie.
+# LITELLM_PROXY_URL e.g. http://localhost:4000 (or LITELLM_PROXY_BASE_URL)
+# LITELLM_MASTER_KEY e.g. sk-1234 (or LITELLM_PROXY_API_KEY)
#
# Optional env (rate limits, all default to 5 req/s; 0 disables a column):
# LITELLM_COMPAT_RATE_ANTHROPIC
@@ -32,10 +33,14 @@
set -euo pipefail
-if [[ -z "${LITELLM_PROXY_BASE_URL:-}" || -z "${LITELLM_PROXY_API_KEY:-}" ]]; then
- echo "error: LITELLM_PROXY_BASE_URL and LITELLM_PROXY_API_KEY must be set" >&2
+proxy_base_url="${LITELLM_PROXY_URL:-${LITELLM_PROXY_BASE_URL:-}}"
+proxy_api_key="${LITELLM_MASTER_KEY:-${LITELLM_PROXY_API_KEY:-}}"
+if [[ -z "$proxy_base_url" || -z "$proxy_api_key" ]]; then
+ echo "error: LITELLM_PROXY_URL and LITELLM_MASTER_KEY (or the legacy LITELLM_PROXY_BASE_URL and LITELLM_PROXY_API_KEY) must be set" >&2
exit 64
fi
+export LITELLM_PROXY_URL="$proxy_base_url"
+export LITELLM_MASTER_KEY="$proxy_api_key"
# Reset the cross-process rate-limiter state from any prior run. Stale
# token-bucket files would let a previous run's accumulated budget bleed
diff --git a/tests/e2e/claude_code/structured_outputs/test_anthropic.py b/tests/e2e/claude_code/structured_outputs/test_anthropic.py
index 610d8433b729..3dc4c7ab8f27 100644
--- a/tests/e2e/claude_code/structured_outputs/test_anthropic.py
+++ b/tests/e2e/claude_code/structured_outputs/test_anthropic.py
@@ -48,24 +48,22 @@
from __future__ import annotations
import json
-import os
import re
from typing import Any, Mapping, Optional, Sequence, Tuple
import pytest
+from claude_code._env import require_proxy
from claude_code.cli_driver import (
ClaudeCLIError,
failure_diagnostic,
run_claude_models_parallel,
)
-PROXY_BASE_URL_ENV = "LITELLM_PROXY_BASE_URL"
-PROXY_API_KEY_ENV = "LITELLM_PROXY_API_KEY"
ANTHROPIC_MODELS = [
"claude-haiku-4-5",
- "claude-sonnet-4-6",
+ "claude-sonnet-4-5",
"claude-opus-4-7",
]
@@ -152,26 +150,12 @@ def _validate_against_schema(
return None
+@pytest.mark.covers("llm.messages.anthropic.structured_output.nonstream.works")
def test_structured_outputs_anthropic(compat_result):
"""Drive `claude --json-schema ...` against the LiteLLM proxy and
assert the trailing `result` event contains a schema-conforming
`structured_output`."""
- base_url = os.environ.get(PROXY_BASE_URL_ENV)
- api_key = os.environ.get(PROXY_API_KEY_ENV)
- if not base_url or not api_key:
- compat_result.set(
- {
- "status": "fail",
- "error": (
- f"missing required env: set {PROXY_BASE_URL_ENV} and "
- f"{PROXY_API_KEY_ENV} to point at a running LiteLLM proxy"
- ),
- }
- )
- pytest.fail(
- f"{PROXY_BASE_URL_ENV} / {PROXY_API_KEY_ENV} not configured",
- pytrace=False,
- )
+ base_url, api_key = require_proxy(compat_result)
outcomes = run_claude_models_parallel(
models=ANTHROPIC_MODELS,
diff --git a/tests/e2e/claude_code/structured_outputs/test_azure.py b/tests/e2e/claude_code/structured_outputs/test_azure.py
index 290f91569101..7a776ed55ada 100644
--- a/tests/e2e/claude_code/structured_outputs/test_azure.py
+++ b/tests/e2e/claude_code/structured_outputs/test_azure.py
@@ -48,24 +48,22 @@
from __future__ import annotations
import json
-import os
import re
from typing import Any, Mapping, Optional, Sequence, Tuple
import pytest
+from claude_code._env import require_proxy
from claude_code.cli_driver import (
ClaudeCLIError,
failure_diagnostic,
run_claude_models_parallel,
)
-PROXY_BASE_URL_ENV = "LITELLM_PROXY_BASE_URL"
-PROXY_API_KEY_ENV = "LITELLM_PROXY_API_KEY"
AZURE_MODELS = [
"claude-haiku-4-5-azure",
- "claude-sonnet-4-6-azure",
+ "claude-sonnet-4-5-azure",
"claude-opus-4-7-azure",
]
@@ -152,26 +150,12 @@ def _validate_against_schema(
return None
+@pytest.mark.covers("llm.messages.azure_foundry.structured_output.nonstream.works")
def test_structured_outputs_azure(compat_result):
"""Drive `claude --json-schema ...` against the LiteLLM proxy and
assert the trailing `result` event contains a schema-conforming
`structured_output`."""
- base_url = os.environ.get(PROXY_BASE_URL_ENV)
- api_key = os.environ.get(PROXY_API_KEY_ENV)
- if not base_url or not api_key:
- compat_result.set(
- {
- "status": "fail",
- "error": (
- f"missing required env: set {PROXY_BASE_URL_ENV} and "
- f"{PROXY_API_KEY_ENV} to point at a running LiteLLM proxy"
- ),
- }
- )
- pytest.fail(
- f"{PROXY_BASE_URL_ENV} / {PROXY_API_KEY_ENV} not configured",
- pytrace=False,
- )
+ base_url, api_key = require_proxy(compat_result)
outcomes = run_claude_models_parallel(
models=AZURE_MODELS,
diff --git a/tests/e2e/claude_code/structured_outputs/test_bedrock_converse.py b/tests/e2e/claude_code/structured_outputs/test_bedrock_converse.py
index 5179014773c6..345d7c327cfa 100644
--- a/tests/e2e/claude_code/structured_outputs/test_bedrock_converse.py
+++ b/tests/e2e/claude_code/structured_outputs/test_bedrock_converse.py
@@ -48,24 +48,22 @@
from __future__ import annotations
import json
-import os
import re
from typing import Any, Mapping, Optional, Sequence, Tuple
import pytest
+from claude_code._env import require_proxy
from claude_code.cli_driver import (
ClaudeCLIError,
failure_diagnostic,
run_claude_models_parallel,
)
-PROXY_BASE_URL_ENV = "LITELLM_PROXY_BASE_URL"
-PROXY_API_KEY_ENV = "LITELLM_PROXY_API_KEY"
BEDROCK_CONVERSE_MODELS = [
"claude-haiku-4-5-bedrock-converse",
- "claude-sonnet-4-6-bedrock-converse",
+ "claude-sonnet-4-5-bedrock-converse",
"claude-opus-4-7-bedrock-converse",
]
@@ -152,26 +150,12 @@ def _validate_against_schema(
return None
+@pytest.mark.covers("llm.messages.bedrock_converse.structured_output.nonstream.works")
def test_structured_outputs_bedrock_converse(compat_result):
"""Drive `claude --json-schema ...` against the LiteLLM proxy and
assert the trailing `result` event contains a schema-conforming
`structured_output`."""
- base_url = os.environ.get(PROXY_BASE_URL_ENV)
- api_key = os.environ.get(PROXY_API_KEY_ENV)
- if not base_url or not api_key:
- compat_result.set(
- {
- "status": "fail",
- "error": (
- f"missing required env: set {PROXY_BASE_URL_ENV} and "
- f"{PROXY_API_KEY_ENV} to point at a running LiteLLM proxy"
- ),
- }
- )
- pytest.fail(
- f"{PROXY_BASE_URL_ENV} / {PROXY_API_KEY_ENV} not configured",
- pytrace=False,
- )
+ base_url, api_key = require_proxy(compat_result)
outcomes = run_claude_models_parallel(
models=BEDROCK_CONVERSE_MODELS,
diff --git a/tests/e2e/claude_code/structured_outputs/test_bedrock_invoke.py b/tests/e2e/claude_code/structured_outputs/test_bedrock_invoke.py
index 313a714be34e..0cf48c72d4f1 100644
--- a/tests/e2e/claude_code/structured_outputs/test_bedrock_invoke.py
+++ b/tests/e2e/claude_code/structured_outputs/test_bedrock_invoke.py
@@ -48,24 +48,22 @@
from __future__ import annotations
import json
-import os
import re
from typing import Any, Mapping, Optional, Sequence, Tuple
import pytest
+from claude_code._env import require_proxy
from claude_code.cli_driver import (
ClaudeCLIError,
failure_diagnostic,
run_claude_models_parallel,
)
-PROXY_BASE_URL_ENV = "LITELLM_PROXY_BASE_URL"
-PROXY_API_KEY_ENV = "LITELLM_PROXY_API_KEY"
BEDROCK_INVOKE_MODELS = [
"claude-haiku-4-5-bedrock-invoke",
- "claude-sonnet-4-6-bedrock-invoke",
+ "claude-sonnet-4-5-bedrock-invoke",
"claude-opus-4-7-bedrock-invoke",
]
@@ -152,26 +150,12 @@ def _validate_against_schema(
return None
+@pytest.mark.covers("llm.messages.bedrock_invoke.structured_output.nonstream.works")
def test_structured_outputs_bedrock_invoke(compat_result):
"""Drive `claude --json-schema ...` against the LiteLLM proxy and
assert the trailing `result` event contains a schema-conforming
`structured_output`."""
- base_url = os.environ.get(PROXY_BASE_URL_ENV)
- api_key = os.environ.get(PROXY_API_KEY_ENV)
- if not base_url or not api_key:
- compat_result.set(
- {
- "status": "fail",
- "error": (
- f"missing required env: set {PROXY_BASE_URL_ENV} and "
- f"{PROXY_API_KEY_ENV} to point at a running LiteLLM proxy"
- ),
- }
- )
- pytest.fail(
- f"{PROXY_BASE_URL_ENV} / {PROXY_API_KEY_ENV} not configured",
- pytrace=False,
- )
+ base_url, api_key = require_proxy(compat_result)
outcomes = run_claude_models_parallel(
models=BEDROCK_INVOKE_MODELS,
diff --git a/tests/e2e/claude_code/structured_outputs/test_vertex_ai.py b/tests/e2e/claude_code/structured_outputs/test_vertex_ai.py
index ec04c7241936..24f5a0c35d4b 100644
--- a/tests/e2e/claude_code/structured_outputs/test_vertex_ai.py
+++ b/tests/e2e/claude_code/structured_outputs/test_vertex_ai.py
@@ -48,24 +48,22 @@
from __future__ import annotations
import json
-import os
import re
from typing import Any, Mapping, Optional, Sequence, Tuple
import pytest
+from claude_code._env import require_proxy
from claude_code.cli_driver import (
ClaudeCLIError,
failure_diagnostic,
run_claude_models_parallel,
)
-PROXY_BASE_URL_ENV = "LITELLM_PROXY_BASE_URL"
-PROXY_API_KEY_ENV = "LITELLM_PROXY_API_KEY"
VERTEX_AI_MODELS = [
"claude-haiku-4-5-vertex",
- "claude-sonnet-4-6-vertex",
+ "claude-sonnet-4-5-vertex",
"claude-opus-4-7-vertex",
]
@@ -152,26 +150,12 @@ def _validate_against_schema(
return None
+@pytest.mark.covers("llm.messages.vertex.structured_output.nonstream.works")
def test_structured_outputs_vertex_ai(compat_result):
"""Drive `claude --json-schema ...` against the LiteLLM proxy and
assert the trailing `result` event contains a schema-conforming
`structured_output`."""
- base_url = os.environ.get(PROXY_BASE_URL_ENV)
- api_key = os.environ.get(PROXY_API_KEY_ENV)
- if not base_url or not api_key:
- compat_result.set(
- {
- "status": "fail",
- "error": (
- f"missing required env: set {PROXY_BASE_URL_ENV} and "
- f"{PROXY_API_KEY_ENV} to point at a running LiteLLM proxy"
- ),
- }
- )
- pytest.fail(
- f"{PROXY_BASE_URL_ENV} / {PROXY_API_KEY_ENV} not configured",
- pytrace=False,
- )
+ base_url, api_key = require_proxy(compat_result)
outcomes = run_claude_models_parallel(
models=VERTEX_AI_MODELS,
diff --git a/tests/e2e/claude_code/test_config.yaml b/tests/e2e/claude_code/test_config.yaml
index eec68d11dcfc..7631709e1ca4 100644
--- a/tests/e2e/claude_code/test_config.yaml
+++ b/tests/e2e/claude_code/test_config.yaml
@@ -21,76 +21,96 @@ model_list:
litellm_params:
model: anthropic/claude-haiku-4-5
api_key: os.environ/ANTHROPIC_API_KEY
- - model_name: claude-sonnet-4-6
+ - model_name: claude-sonnet-4-5
litellm_params:
- model: anthropic/claude-sonnet-4-6
+ model: anthropic/claude-sonnet-4-5
api_key: os.environ/ANTHROPIC_API_KEY
+ extra_headers:
+ anthropic-beta: "context-1m-2025-08-07"
- model_name: claude-opus-4-7
litellm_params:
model: anthropic/claude-opus-4-7
api_key: os.environ/ANTHROPIC_API_KEY
+ extra_headers:
+ anthropic-beta: "context-1m-2025-08-07"
# ---- Bedrock (InvokeModel) ----
- model_name: claude-haiku-4-5-bedrock-invoke
litellm_params:
model: bedrock/us.anthropic.claude-haiku-4-5-20251001-v1:0
aws_region_name: us-east-1
- - model_name: claude-sonnet-4-6-bedrock-invoke
+ - model_name: claude-sonnet-4-5-bedrock-invoke
litellm_params:
- model: bedrock/us.anthropic.claude-sonnet-4-6
+ model: bedrock/us.anthropic.claude-sonnet-4-5-20250929-v1:0
aws_region_name: us-east-1
+ extra_headers:
+ anthropic-beta: "context-1m-2025-08-07"
- model_name: claude-opus-4-7-bedrock-invoke
litellm_params:
model: bedrock/us.anthropic.claude-opus-4-7
aws_region_name: us-east-1
+ extra_headers:
+ anthropic-beta: "context-1m-2025-08-07"
# ---- Bedrock (Converse) ----
- model_name: claude-haiku-4-5-bedrock-converse
litellm_params:
model: bedrock/converse/us.anthropic.claude-haiku-4-5-20251001-v1:0
aws_region_name: us-east-1
- - model_name: claude-sonnet-4-6-bedrock-converse
+ - model_name: claude-sonnet-4-5-bedrock-converse
litellm_params:
- model: bedrock/converse/us.anthropic.claude-sonnet-4-6
+ model: bedrock/converse/us.anthropic.claude-sonnet-4-5-20250929-v1:0
aws_region_name: us-east-1
+ extra_headers:
+ anthropic-beta: "context-1m-2025-08-07"
- model_name: claude-opus-4-7-bedrock-converse
litellm_params:
model: bedrock/converse/us.anthropic.claude-opus-4-7
aws_region_name: us-east-1
+ extra_headers:
+ anthropic-beta: "context-1m-2025-08-07"
# ---- Vertex AI ----
- model_name: claude-haiku-4-5-vertex
litellm_params:
model: vertex_ai/claude-haiku-4-5
vertex_ai_project: os.environ/VERTEXAI_PROJECT
- vertex_ai_location: os.environ/VERTEXAI_LOCATION
- - model_name: claude-sonnet-4-6-vertex
+ vertex_ai_location: us-east5
+ - model_name: claude-sonnet-4-5-vertex
litellm_params:
- model: vertex_ai/claude-sonnet-4-6
+ model: vertex_ai/claude-sonnet-4-5
vertex_ai_project: os.environ/VERTEXAI_PROJECT
- vertex_ai_location: os.environ/VERTEXAI_LOCATION
+ vertex_ai_location: us-east5
+ extra_headers:
+ anthropic-beta: "context-1m-2025-08-07"
- model_name: claude-opus-4-7-vertex
litellm_params:
model: vertex_ai/claude-opus-4-7
vertex_ai_project: os.environ/VERTEXAI_PROJECT
- vertex_ai_location: os.environ/VERTEXAI_LOCATION
+ vertex_ai_location: us-east5
+ extra_headers:
+ anthropic-beta: "context-1m-2025-08-07"
# ---- Microsoft Foundry (Anthropic deployments on Azure) ----
- model_name: claude-haiku-4-5-azure
litellm_params:
model: azure_ai/claude-haiku-4-5
- api_base: os.environ/AZURE_FOUNDRY_API_BASE
- api_key: os.environ/AZURE_FOUNDRY_API_KEY
- - model_name: claude-sonnet-4-6-azure
+ api_base: os.environ/AZURE_AI_API_BASE
+ api_key: os.environ/AZURE_AI_API_KEY
+ - model_name: claude-sonnet-4-5-azure
litellm_params:
- model: azure_ai/claude-sonnet-4-6
- api_base: os.environ/AZURE_FOUNDRY_API_BASE
- api_key: os.environ/AZURE_FOUNDRY_API_KEY
+ model: azure_ai/claude-sonnet-4-5
+ api_base: os.environ/AZURE_AI_API_BASE
+ api_key: os.environ/AZURE_AI_API_KEY
+ extra_headers:
+ anthropic-beta: "context-1m-2025-08-07"
- model_name: claude-opus-4-7-azure
litellm_params:
model: azure_ai/claude-opus-4-7
- api_base: os.environ/AZURE_FOUNDRY_API_BASE
- api_key: os.environ/AZURE_FOUNDRY_API_KEY
+ api_base: os.environ/AZURE_AI_API_BASE
+ api_key: os.environ/AZURE_AI_API_KEY
+ extra_headers:
+ anthropic-beta: "context-1m-2025-08-07"
general_settings:
# Claude Code sends provider-specific headers (e.g. anthropic-beta) we
diff --git a/tests/e2e/claude_code/thinking/test_anthropic.py b/tests/e2e/claude_code/thinking/test_anthropic.py
index 1090d1b384e3..ebb2445fb6d2 100644
--- a/tests/e2e/claude_code/thinking/test_anthropic.py
+++ b/tests/e2e/claude_code/thinking/test_anthropic.py
@@ -20,23 +20,21 @@
from __future__ import annotations
-import os
from typing import Any, Mapping, Sequence
import pytest
+from claude_code._env import require_proxy
from claude_code.cli_driver import (
ClaudeCLIError,
failure_diagnostic,
run_claude_models_parallel,
)
-PROXY_BASE_URL_ENV = "LITELLM_PROXY_BASE_URL"
-PROXY_API_KEY_ENV = "LITELLM_PROXY_API_KEY"
ANTHROPIC_MODELS = [
"claude-haiku-4-5",
- "claude-sonnet-4-6",
+ "claude-sonnet-4-5",
"claude-opus-4-7",
]
@@ -76,24 +74,11 @@ def _has_thinking_block(events: Sequence[Mapping[str, Any]]) -> bool:
return False
+@pytest.mark.covers("llm.messages.anthropic.thinking.nonstream.works")
def test_thinking_anthropic(compat_result):
"""Drive the `claude` CLI against the LiteLLM proxy with thinking
enabled and assert a `thinking` content block was emitted."""
- base_url = os.environ.get(PROXY_BASE_URL_ENV)
- api_key = os.environ.get(PROXY_API_KEY_ENV)
- if not base_url or not api_key:
- compat_result.set(
- {
- "status": "fail",
- "error": (
- f"missing required env: set {PROXY_BASE_URL_ENV} and "
- f"{PROXY_API_KEY_ENV} to point at a running LiteLLM proxy"
- ),
- }
- )
- pytest.fail(
- f"{PROXY_BASE_URL_ENV} / {PROXY_API_KEY_ENV} not configured", pytrace=False
- )
+ base_url, api_key = require_proxy(compat_result)
outcomes = run_claude_models_parallel(
models=ANTHROPIC_MODELS,
diff --git a/tests/e2e/claude_code/thinking/test_azure.py b/tests/e2e/claude_code/thinking/test_azure.py
index 1fd5138d574c..ffd5ca92df02 100644
--- a/tests/e2e/claude_code/thinking/test_azure.py
+++ b/tests/e2e/claude_code/thinking/test_azure.py
@@ -23,23 +23,21 @@
from __future__ import annotations
-import os
from typing import Any, Mapping, Sequence
import pytest
+from claude_code._env import require_proxy
from claude_code.cli_driver import (
ClaudeCLIError,
failure_diagnostic,
run_claude_models_parallel,
)
-PROXY_BASE_URL_ENV = "LITELLM_PROXY_BASE_URL"
-PROXY_API_KEY_ENV = "LITELLM_PROXY_API_KEY"
AZURE_MODELS = [
"claude-haiku-4-5-azure",
- "claude-sonnet-4-6-azure",
+ "claude-sonnet-4-5-azure",
"claude-opus-4-7-azure",
]
@@ -64,24 +62,11 @@ def _has_thinking_block(events: Sequence[Mapping[str, Any]]) -> bool:
return False
+@pytest.mark.covers("llm.messages.azure_foundry.thinking.nonstream.works")
def test_thinking_azure(compat_result):
"""Drive the `claude` CLI against the LiteLLM proxy with thinking
enabled and assert a `thinking` content block was emitted."""
- base_url = os.environ.get(PROXY_BASE_URL_ENV)
- api_key = os.environ.get(PROXY_API_KEY_ENV)
- if not base_url or not api_key:
- compat_result.set(
- {
- "status": "fail",
- "error": (
- f"missing required env: set {PROXY_BASE_URL_ENV} and "
- f"{PROXY_API_KEY_ENV} to point at a running LiteLLM proxy"
- ),
- }
- )
- pytest.fail(
- f"{PROXY_BASE_URL_ENV} / {PROXY_API_KEY_ENV} not configured", pytrace=False
- )
+ base_url, api_key = require_proxy(compat_result)
outcomes = run_claude_models_parallel(
models=AZURE_MODELS,
diff --git a/tests/e2e/claude_code/thinking/test_bedrock_converse.py b/tests/e2e/claude_code/thinking/test_bedrock_converse.py
index 793ce8542daf..0b409f18ea7e 100644
--- a/tests/e2e/claude_code/thinking/test_bedrock_converse.py
+++ b/tests/e2e/claude_code/thinking/test_bedrock_converse.py
@@ -15,23 +15,21 @@
from __future__ import annotations
-import os
from typing import Any, Mapping, Sequence
import pytest
+from claude_code._env import require_proxy
from claude_code.cli_driver import (
ClaudeCLIError,
failure_diagnostic,
run_claude_models_parallel,
)
-PROXY_BASE_URL_ENV = "LITELLM_PROXY_BASE_URL"
-PROXY_API_KEY_ENV = "LITELLM_PROXY_API_KEY"
BEDROCK_CONVERSE_MODELS = [
"claude-haiku-4-5-bedrock-converse",
- "claude-sonnet-4-6-bedrock-converse",
+ "claude-sonnet-4-5-bedrock-converse",
"claude-opus-4-7-bedrock-converse",
]
@@ -56,24 +54,11 @@ def _has_thinking_block(events: Sequence[Mapping[str, Any]]) -> bool:
return False
+@pytest.mark.covers("llm.messages.bedrock_converse.thinking.nonstream.works")
def test_thinking_bedrock_converse(compat_result):
"""Drive the `claude` CLI against the LiteLLM proxy with thinking
enabled and assert a `thinking` content block was emitted."""
- base_url = os.environ.get(PROXY_BASE_URL_ENV)
- api_key = os.environ.get(PROXY_API_KEY_ENV)
- if not base_url or not api_key:
- compat_result.set(
- {
- "status": "fail",
- "error": (
- f"missing required env: set {PROXY_BASE_URL_ENV} and "
- f"{PROXY_API_KEY_ENV} to point at a running LiteLLM proxy"
- ),
- }
- )
- pytest.fail(
- f"{PROXY_BASE_URL_ENV} / {PROXY_API_KEY_ENV} not configured", pytrace=False
- )
+ base_url, api_key = require_proxy(compat_result)
outcomes = run_claude_models_parallel(
models=BEDROCK_CONVERSE_MODELS,
diff --git a/tests/e2e/claude_code/thinking/test_bedrock_invoke.py b/tests/e2e/claude_code/thinking/test_bedrock_invoke.py
index e31b60eb004f..a2c97eae321c 100644
--- a/tests/e2e/claude_code/thinking/test_bedrock_invoke.py
+++ b/tests/e2e/claude_code/thinking/test_bedrock_invoke.py
@@ -15,23 +15,21 @@
from __future__ import annotations
-import os
from typing import Any, Mapping, Sequence
import pytest
+from claude_code._env import require_proxy
from claude_code.cli_driver import (
ClaudeCLIError,
failure_diagnostic,
run_claude_models_parallel,
)
-PROXY_BASE_URL_ENV = "LITELLM_PROXY_BASE_URL"
-PROXY_API_KEY_ENV = "LITELLM_PROXY_API_KEY"
BEDROCK_INVOKE_MODELS = [
"claude-haiku-4-5-bedrock-invoke",
- "claude-sonnet-4-6-bedrock-invoke",
+ "claude-sonnet-4-5-bedrock-invoke",
"claude-opus-4-7-bedrock-invoke",
]
@@ -56,24 +54,11 @@ def _has_thinking_block(events: Sequence[Mapping[str, Any]]) -> bool:
return False
+@pytest.mark.covers("llm.messages.bedrock_invoke.thinking.nonstream.works")
def test_thinking_bedrock_invoke(compat_result):
"""Drive the `claude` CLI against the LiteLLM proxy with thinking
enabled and assert a `thinking` content block was emitted."""
- base_url = os.environ.get(PROXY_BASE_URL_ENV)
- api_key = os.environ.get(PROXY_API_KEY_ENV)
- if not base_url or not api_key:
- compat_result.set(
- {
- "status": "fail",
- "error": (
- f"missing required env: set {PROXY_BASE_URL_ENV} and "
- f"{PROXY_API_KEY_ENV} to point at a running LiteLLM proxy"
- ),
- }
- )
- pytest.fail(
- f"{PROXY_BASE_URL_ENV} / {PROXY_API_KEY_ENV} not configured", pytrace=False
- )
+ base_url, api_key = require_proxy(compat_result)
outcomes = run_claude_models_parallel(
models=BEDROCK_INVOKE_MODELS,
diff --git a/tests/e2e/claude_code/thinking/test_vertex_ai.py b/tests/e2e/claude_code/thinking/test_vertex_ai.py
index c5c7df1f9b88..f1a1c5b6ceed 100644
--- a/tests/e2e/claude_code/thinking/test_vertex_ai.py
+++ b/tests/e2e/claude_code/thinking/test_vertex_ai.py
@@ -15,23 +15,21 @@
from __future__ import annotations
-import os
from typing import Any, Mapping, Sequence
import pytest
+from claude_code._env import require_proxy
from claude_code.cli_driver import (
ClaudeCLIError,
failure_diagnostic,
run_claude_models_parallel,
)
-PROXY_BASE_URL_ENV = "LITELLM_PROXY_BASE_URL"
-PROXY_API_KEY_ENV = "LITELLM_PROXY_API_KEY"
VERTEX_AI_MODELS = [
"claude-haiku-4-5-vertex",
- "claude-sonnet-4-6-vertex",
+ "claude-sonnet-4-5-vertex",
"claude-opus-4-7-vertex",
]
@@ -56,24 +54,11 @@ def _has_thinking_block(events: Sequence[Mapping[str, Any]]) -> bool:
return False
+@pytest.mark.covers("llm.messages.vertex.thinking.nonstream.works")
def test_thinking_vertex_ai(compat_result):
"""Drive the `claude` CLI against the LiteLLM proxy with thinking
enabled and assert a `thinking` content block was emitted."""
- base_url = os.environ.get(PROXY_BASE_URL_ENV)
- api_key = os.environ.get(PROXY_API_KEY_ENV)
- if not base_url or not api_key:
- compat_result.set(
- {
- "status": "fail",
- "error": (
- f"missing required env: set {PROXY_BASE_URL_ENV} and "
- f"{PROXY_API_KEY_ENV} to point at a running LiteLLM proxy"
- ),
- }
- )
- pytest.fail(
- f"{PROXY_BASE_URL_ENV} / {PROXY_API_KEY_ENV} not configured", pytrace=False
- )
+ base_url, api_key = require_proxy(compat_result)
outcomes = run_claude_models_parallel(
models=VERTEX_AI_MODELS,
diff --git a/tests/e2e/claude_code/thinking_with_tool_use/test_anthropic.py b/tests/e2e/claude_code/thinking_with_tool_use/test_anthropic.py
index 2c573ea039e5..7e39ea26d42e 100644
--- a/tests/e2e/claude_code/thinking_with_tool_use/test_anthropic.py
+++ b/tests/e2e/claude_code/thinking_with_tool_use/test_anthropic.py
@@ -24,23 +24,21 @@
from __future__ import annotations
-import os
from typing import Any, Mapping, Sequence
import pytest
+from claude_code._env import require_proxy
from claude_code.cli_driver import (
ClaudeCLIError,
failure_diagnostic,
run_claude_models_parallel,
)
-PROXY_BASE_URL_ENV = "LITELLM_PROXY_BASE_URL"
-PROXY_API_KEY_ENV = "LITELLM_PROXY_API_KEY"
ANTHROPIC_MODELS = [
"claude-haiku-4-5",
- "claude-sonnet-4-6",
+ "claude-sonnet-4-5",
"claude-opus-4-7",
]
@@ -90,25 +88,12 @@ def _has_block_type(
return False
+@pytest.mark.covers("llm.messages.anthropic.thinking_with_tool_use.nonstream.works")
def test_thinking_with_tool_use_anthropic(compat_result):
"""Drive the `claude` CLI against the LiteLLM proxy with thinking
enabled and tool use, and assert both `thinking` and `tool_use`
content blocks landed in the same turn."""
- base_url = os.environ.get(PROXY_BASE_URL_ENV)
- api_key = os.environ.get(PROXY_API_KEY_ENV)
- if not base_url or not api_key:
- compat_result.set(
- {
- "status": "fail",
- "error": (
- f"missing required env: set {PROXY_BASE_URL_ENV} and "
- f"{PROXY_API_KEY_ENV} to point at a running LiteLLM proxy"
- ),
- }
- )
- pytest.fail(
- f"{PROXY_BASE_URL_ENV} / {PROXY_API_KEY_ENV} not configured", pytrace=False
- )
+ base_url, api_key = require_proxy(compat_result)
outcomes = run_claude_models_parallel(
models=ANTHROPIC_MODELS,
diff --git a/tests/e2e/claude_code/thinking_with_tool_use/test_azure.py b/tests/e2e/claude_code/thinking_with_tool_use/test_azure.py
index 3d65e82cdeca..0371a10f8a66 100644
--- a/tests/e2e/claude_code/thinking_with_tool_use/test_azure.py
+++ b/tests/e2e/claude_code/thinking_with_tool_use/test_azure.py
@@ -18,23 +18,21 @@
from __future__ import annotations
-import os
from typing import Any, Mapping, Sequence
import pytest
+from claude_code._env import require_proxy
from claude_code.cli_driver import (
ClaudeCLIError,
failure_diagnostic,
run_claude_models_parallel,
)
-PROXY_BASE_URL_ENV = "LITELLM_PROXY_BASE_URL"
-PROXY_API_KEY_ENV = "LITELLM_PROXY_API_KEY"
AZURE_MODELS = [
"claude-haiku-4-5-azure",
- "claude-sonnet-4-6-azure",
+ "claude-sonnet-4-5-azure",
"claude-opus-4-7-azure",
]
@@ -71,22 +69,9 @@ def _has_block_type(
return False
+@pytest.mark.covers("llm.messages.azure_foundry.thinking_with_tool_use.nonstream.works")
def test_thinking_with_tool_use_azure(compat_result):
- base_url = os.environ.get(PROXY_BASE_URL_ENV)
- api_key = os.environ.get(PROXY_API_KEY_ENV)
- if not base_url or not api_key:
- compat_result.set(
- {
- "status": "fail",
- "error": (
- f"missing required env: set {PROXY_BASE_URL_ENV} and "
- f"{PROXY_API_KEY_ENV} to point at a running LiteLLM proxy"
- ),
- }
- )
- pytest.fail(
- f"{PROXY_BASE_URL_ENV} / {PROXY_API_KEY_ENV} not configured", pytrace=False
- )
+ base_url, api_key = require_proxy(compat_result)
outcomes = run_claude_models_parallel(
models=AZURE_MODELS,
diff --git a/tests/e2e/claude_code/thinking_with_tool_use/test_bedrock_converse.py b/tests/e2e/claude_code/thinking_with_tool_use/test_bedrock_converse.py
index eb9163235465..026d2a3707fe 100644
--- a/tests/e2e/claude_code/thinking_with_tool_use/test_bedrock_converse.py
+++ b/tests/e2e/claude_code/thinking_with_tool_use/test_bedrock_converse.py
@@ -23,23 +23,21 @@
from __future__ import annotations
-import os
from typing import Any, Mapping, Sequence
import pytest
+from claude_code._env import require_proxy
from claude_code.cli_driver import (
ClaudeCLIError,
failure_diagnostic,
run_claude_models_parallel,
)
-PROXY_BASE_URL_ENV = "LITELLM_PROXY_BASE_URL"
-PROXY_API_KEY_ENV = "LITELLM_PROXY_API_KEY"
BEDROCK_CONVERSE_MODELS = [
"claude-haiku-4-5-bedrock-converse",
- "claude-sonnet-4-6-bedrock-converse",
+ "claude-sonnet-4-5-bedrock-converse",
"claude-opus-4-7-bedrock-converse",
]
@@ -76,22 +74,9 @@ def _has_block_type(
return False
+@pytest.mark.covers("llm.messages.bedrock_converse.thinking_with_tool_use.nonstream.works")
def test_thinking_with_tool_use_bedrock_converse(compat_result):
- base_url = os.environ.get(PROXY_BASE_URL_ENV)
- api_key = os.environ.get(PROXY_API_KEY_ENV)
- if not base_url or not api_key:
- compat_result.set(
- {
- "status": "fail",
- "error": (
- f"missing required env: set {PROXY_BASE_URL_ENV} and "
- f"{PROXY_API_KEY_ENV} to point at a running LiteLLM proxy"
- ),
- }
- )
- pytest.fail(
- f"{PROXY_BASE_URL_ENV} / {PROXY_API_KEY_ENV} not configured", pytrace=False
- )
+ base_url, api_key = require_proxy(compat_result)
outcomes = run_claude_models_parallel(
models=BEDROCK_CONVERSE_MODELS,
diff --git a/tests/e2e/claude_code/thinking_with_tool_use/test_bedrock_invoke.py b/tests/e2e/claude_code/thinking_with_tool_use/test_bedrock_invoke.py
index d1a61a597724..1dd4cf0a73ca 100644
--- a/tests/e2e/claude_code/thinking_with_tool_use/test_bedrock_invoke.py
+++ b/tests/e2e/claude_code/thinking_with_tool_use/test_bedrock_invoke.py
@@ -25,23 +25,21 @@
from __future__ import annotations
-import os
from typing import Any, Mapping, Sequence
import pytest
+from claude_code._env import require_proxy
from claude_code.cli_driver import (
ClaudeCLIError,
failure_diagnostic,
run_claude_models_parallel,
)
-PROXY_BASE_URL_ENV = "LITELLM_PROXY_BASE_URL"
-PROXY_API_KEY_ENV = "LITELLM_PROXY_API_KEY"
BEDROCK_INVOKE_MODELS = [
"claude-haiku-4-5-bedrock-invoke",
- "claude-sonnet-4-6-bedrock-invoke",
+ "claude-sonnet-4-5-bedrock-invoke",
"claude-opus-4-7-bedrock-invoke",
]
@@ -78,22 +76,9 @@ def _has_block_type(
return False
+@pytest.mark.covers("llm.messages.bedrock_invoke.thinking_with_tool_use.nonstream.works")
def test_thinking_with_tool_use_bedrock_invoke(compat_result):
- base_url = os.environ.get(PROXY_BASE_URL_ENV)
- api_key = os.environ.get(PROXY_API_KEY_ENV)
- if not base_url or not api_key:
- compat_result.set(
- {
- "status": "fail",
- "error": (
- f"missing required env: set {PROXY_BASE_URL_ENV} and "
- f"{PROXY_API_KEY_ENV} to point at a running LiteLLM proxy"
- ),
- }
- )
- pytest.fail(
- f"{PROXY_BASE_URL_ENV} / {PROXY_API_KEY_ENV} not configured", pytrace=False
- )
+ base_url, api_key = require_proxy(compat_result)
outcomes = run_claude_models_parallel(
models=BEDROCK_INVOKE_MODELS,
diff --git a/tests/e2e/claude_code/thinking_with_tool_use/test_vertex_ai.py b/tests/e2e/claude_code/thinking_with_tool_use/test_vertex_ai.py
index 285419c67f72..b25228edb55d 100644
--- a/tests/e2e/claude_code/thinking_with_tool_use/test_vertex_ai.py
+++ b/tests/e2e/claude_code/thinking_with_tool_use/test_vertex_ai.py
@@ -23,23 +23,21 @@
from __future__ import annotations
-import os
from typing import Any, Mapping, Sequence
import pytest
+from claude_code._env import require_proxy
from claude_code.cli_driver import (
ClaudeCLIError,
failure_diagnostic,
run_claude_models_parallel,
)
-PROXY_BASE_URL_ENV = "LITELLM_PROXY_BASE_URL"
-PROXY_API_KEY_ENV = "LITELLM_PROXY_API_KEY"
VERTEX_AI_MODELS = [
"claude-haiku-4-5-vertex",
- "claude-sonnet-4-6-vertex",
+ "claude-sonnet-4-5-vertex",
"claude-opus-4-7-vertex",
]
@@ -76,22 +74,9 @@ def _has_block_type(
return False
+@pytest.mark.covers("llm.messages.vertex.thinking_with_tool_use.nonstream.works")
def test_thinking_with_tool_use_vertex_ai(compat_result):
- base_url = os.environ.get(PROXY_BASE_URL_ENV)
- api_key = os.environ.get(PROXY_API_KEY_ENV)
- if not base_url or not api_key:
- compat_result.set(
- {
- "status": "fail",
- "error": (
- f"missing required env: set {PROXY_BASE_URL_ENV} and "
- f"{PROXY_API_KEY_ENV} to point at a running LiteLLM proxy"
- ),
- }
- )
- pytest.fail(
- f"{PROXY_BASE_URL_ENV} / {PROXY_API_KEY_ENV} not configured", pytrace=False
- )
+ base_url, api_key = require_proxy(compat_result)
outcomes = run_claude_models_parallel(
models=VERTEX_AI_MODELS,
diff --git a/tests/e2e/claude_code/tool_search/test_anthropic.py b/tests/e2e/claude_code/tool_search/test_anthropic.py
index 3495c882e063..7b8ea07aa074 100644
--- a/tests/e2e/claude_code/tool_search/test_anthropic.py
+++ b/tests/e2e/claude_code/tool_search/test_anthropic.py
@@ -43,45 +43,28 @@
from __future__ import annotations
-import os
-
import pytest
+from claude_code._env import require_proxy
from claude_code.http_probe import (
assert_tool_search_shape,
probe_tool_search,
)
-PROXY_BASE_URL_ENV = "LITELLM_PROXY_BASE_URL"
-PROXY_API_KEY_ENV = "LITELLM_PROXY_API_KEY"
ANTHROPIC_MODELS = [
"claude-haiku-4-5",
- "claude-sonnet-4-6",
+ "claude-sonnet-4-5",
"claude-opus-4-7",
]
+@pytest.mark.covers("llm.messages.anthropic.tool_search.nonstream.works")
def test_tool_search_anthropic(compat_result):
"""Probe `/v1/messages` with a `tool_search_tool_regex_20251119`
tool and assert the proxy + upstream accept it for every Anthropic
tier."""
- base_url = os.environ.get(PROXY_BASE_URL_ENV)
- api_key = os.environ.get(PROXY_API_KEY_ENV)
- if not base_url or not api_key:
- compat_result.set(
- {
- "status": "fail",
- "error": (
- f"missing required env: set {PROXY_BASE_URL_ENV} and "
- f"{PROXY_API_KEY_ENV} to point at a running LiteLLM proxy"
- ),
- }
- )
- pytest.fail(
- f"{PROXY_BASE_URL_ENV} / {PROXY_API_KEY_ENV} not configured",
- pytrace=False,
- )
+ base_url, api_key = require_proxy(compat_result)
failures = []
for model in ANTHROPIC_MODELS:
diff --git a/tests/e2e/claude_code/tool_search/test_azure.py b/tests/e2e/claude_code/tool_search/test_azure.py
index 1d9cb5673c50..4eee13e4ecc2 100644
--- a/tests/e2e/claude_code/tool_search/test_azure.py
+++ b/tests/e2e/claude_code/tool_search/test_azure.py
@@ -43,45 +43,28 @@
from __future__ import annotations
-import os
-
import pytest
+from claude_code._env import require_proxy
from claude_code.http_probe import (
assert_tool_search_shape,
probe_tool_search,
)
-PROXY_BASE_URL_ENV = "LITELLM_PROXY_BASE_URL"
-PROXY_API_KEY_ENV = "LITELLM_PROXY_API_KEY"
AZURE_MODELS = [
"claude-haiku-4-5-azure",
- "claude-sonnet-4-6-azure",
+ "claude-sonnet-4-5-azure",
"claude-opus-4-7-azure",
]
+@pytest.mark.covers("llm.messages.azure_foundry.tool_search.nonstream.works")
def test_tool_search_azure(compat_result):
"""Probe `/v1/messages` with a `tool_search_tool_regex_20251119`
tool and assert the proxy + upstream accept it for every Azure (Microsoft Foundry)
tier."""
- base_url = os.environ.get(PROXY_BASE_URL_ENV)
- api_key = os.environ.get(PROXY_API_KEY_ENV)
- if not base_url or not api_key:
- compat_result.set(
- {
- "status": "fail",
- "error": (
- f"missing required env: set {PROXY_BASE_URL_ENV} and "
- f"{PROXY_API_KEY_ENV} to point at a running LiteLLM proxy"
- ),
- }
- )
- pytest.fail(
- f"{PROXY_BASE_URL_ENV} / {PROXY_API_KEY_ENV} not configured",
- pytrace=False,
- )
+ base_url, api_key = require_proxy(compat_result)
failures = []
for model in AZURE_MODELS:
diff --git a/tests/e2e/claude_code/tool_search/test_bedrock_converse.py b/tests/e2e/claude_code/tool_search/test_bedrock_converse.py
index 5ca0792529aa..7951f8ecdb42 100644
--- a/tests/e2e/claude_code/tool_search/test_bedrock_converse.py
+++ b/tests/e2e/claude_code/tool_search/test_bedrock_converse.py
@@ -43,45 +43,28 @@
from __future__ import annotations
-import os
-
import pytest
+from claude_code._env import require_proxy
from claude_code.http_probe import (
assert_tool_search_shape,
probe_tool_search,
)
-PROXY_BASE_URL_ENV = "LITELLM_PROXY_BASE_URL"
-PROXY_API_KEY_ENV = "LITELLM_PROXY_API_KEY"
BEDROCK_CONVERSE_MODELS = [
"claude-haiku-4-5-bedrock-converse",
- "claude-sonnet-4-6-bedrock-converse",
+ "claude-sonnet-4-5-bedrock-converse",
"claude-opus-4-7-bedrock-converse",
]
+@pytest.mark.covers("llm.messages.bedrock_converse.tool_search.nonstream.works")
def test_tool_search_bedrock_converse(compat_result):
"""Probe `/v1/messages` with a `tool_search_tool_regex_20251119`
tool and assert the proxy + upstream accept it for every Bedrock (Converse)
tier."""
- base_url = os.environ.get(PROXY_BASE_URL_ENV)
- api_key = os.environ.get(PROXY_API_KEY_ENV)
- if not base_url or not api_key:
- compat_result.set(
- {
- "status": "fail",
- "error": (
- f"missing required env: set {PROXY_BASE_URL_ENV} and "
- f"{PROXY_API_KEY_ENV} to point at a running LiteLLM proxy"
- ),
- }
- )
- pytest.fail(
- f"{PROXY_BASE_URL_ENV} / {PROXY_API_KEY_ENV} not configured",
- pytrace=False,
- )
+ base_url, api_key = require_proxy(compat_result)
failures = []
for model in BEDROCK_CONVERSE_MODELS:
diff --git a/tests/e2e/claude_code/tool_search/test_bedrock_invoke.py b/tests/e2e/claude_code/tool_search/test_bedrock_invoke.py
index 21bb33e34bd1..654c2aa18d1d 100644
--- a/tests/e2e/claude_code/tool_search/test_bedrock_invoke.py
+++ b/tests/e2e/claude_code/tool_search/test_bedrock_invoke.py
@@ -43,45 +43,28 @@
from __future__ import annotations
-import os
-
import pytest
+from claude_code._env import require_proxy
from claude_code.http_probe import (
assert_tool_search_shape,
probe_tool_search,
)
-PROXY_BASE_URL_ENV = "LITELLM_PROXY_BASE_URL"
-PROXY_API_KEY_ENV = "LITELLM_PROXY_API_KEY"
BEDROCK_INVOKE_MODELS = [
"claude-haiku-4-5-bedrock-invoke",
- "claude-sonnet-4-6-bedrock-invoke",
+ "claude-sonnet-4-5-bedrock-invoke",
"claude-opus-4-7-bedrock-invoke",
]
+@pytest.mark.covers("llm.messages.bedrock_invoke.tool_search.nonstream.works")
def test_tool_search_bedrock_invoke(compat_result):
"""Probe `/v1/messages` with a `tool_search_tool_regex_20251119`
tool and assert the proxy + upstream accept it for every Bedrock (Invoke)
tier."""
- base_url = os.environ.get(PROXY_BASE_URL_ENV)
- api_key = os.environ.get(PROXY_API_KEY_ENV)
- if not base_url or not api_key:
- compat_result.set(
- {
- "status": "fail",
- "error": (
- f"missing required env: set {PROXY_BASE_URL_ENV} and "
- f"{PROXY_API_KEY_ENV} to point at a running LiteLLM proxy"
- ),
- }
- )
- pytest.fail(
- f"{PROXY_BASE_URL_ENV} / {PROXY_API_KEY_ENV} not configured",
- pytrace=False,
- )
+ base_url, api_key = require_proxy(compat_result)
failures = []
for model in BEDROCK_INVOKE_MODELS:
diff --git a/tests/e2e/claude_code/tool_search/test_vertex_ai.py b/tests/e2e/claude_code/tool_search/test_vertex_ai.py
index f91400b18170..f6ff855fa783 100644
--- a/tests/e2e/claude_code/tool_search/test_vertex_ai.py
+++ b/tests/e2e/claude_code/tool_search/test_vertex_ai.py
@@ -43,45 +43,28 @@
from __future__ import annotations
-import os
-
import pytest
+from claude_code._env import require_proxy
from claude_code.http_probe import (
assert_tool_search_shape,
probe_tool_search,
)
-PROXY_BASE_URL_ENV = "LITELLM_PROXY_BASE_URL"
-PROXY_API_KEY_ENV = "LITELLM_PROXY_API_KEY"
VERTEX_AI_MODELS = [
"claude-haiku-4-5-vertex",
- "claude-sonnet-4-6-vertex",
+ "claude-sonnet-4-5-vertex",
"claude-opus-4-7-vertex",
]
+@pytest.mark.covers("llm.messages.vertex.tool_search.nonstream.works")
def test_tool_search_vertex_ai(compat_result):
"""Probe `/v1/messages` with a `tool_search_tool_regex_20251119`
tool and assert the proxy + upstream accept it for every Vertex AI
tier."""
- base_url = os.environ.get(PROXY_BASE_URL_ENV)
- api_key = os.environ.get(PROXY_API_KEY_ENV)
- if not base_url or not api_key:
- compat_result.set(
- {
- "status": "fail",
- "error": (
- f"missing required env: set {PROXY_BASE_URL_ENV} and "
- f"{PROXY_API_KEY_ENV} to point at a running LiteLLM proxy"
- ),
- }
- )
- pytest.fail(
- f"{PROXY_BASE_URL_ENV} / {PROXY_API_KEY_ENV} not configured",
- pytrace=False,
- )
+ base_url, api_key = require_proxy(compat_result)
failures = []
for model in VERTEX_AI_MODELS:
diff --git a/tests/e2e/claude_code/tool_use/test_anthropic.py b/tests/e2e/claude_code/tool_use/test_anthropic.py
index 7d2aa4be683b..9ff4c58907ff 100644
--- a/tests/e2e/claude_code/tool_use/test_anthropic.py
+++ b/tests/e2e/claude_code/tool_use/test_anthropic.py
@@ -15,23 +15,21 @@
from __future__ import annotations
-import os
from typing import Any, Mapping, Sequence
import pytest
+from claude_code._env import require_proxy
from claude_code.cli_driver import (
ClaudeCLIError,
failure_diagnostic,
run_claude_models_parallel,
)
-PROXY_BASE_URL_ENV = "LITELLM_PROXY_BASE_URL"
-PROXY_API_KEY_ENV = "LITELLM_PROXY_API_KEY"
ANTHROPIC_MODELS = [
"claude-haiku-4-5",
- "claude-sonnet-4-6",
+ "claude-sonnet-4-5",
"claude-opus-4-7",
]
@@ -73,24 +71,11 @@ def _has_tool_use_event(events: Sequence[Mapping[str, Any]]) -> bool:
return False
+@pytest.mark.covers("llm.messages.anthropic.tool_use.nonstream.works")
def test_tool_use_anthropic(compat_result):
"""Drive the `claude` CLI against the LiteLLM proxy and assert a
tool call was emitted on the wire."""
- base_url = os.environ.get(PROXY_BASE_URL_ENV)
- api_key = os.environ.get(PROXY_API_KEY_ENV)
- if not base_url or not api_key:
- compat_result.set(
- {
- "status": "fail",
- "error": (
- f"missing required env: set {PROXY_BASE_URL_ENV} and "
- f"{PROXY_API_KEY_ENV} to point at a running LiteLLM proxy"
- ),
- }
- )
- pytest.fail(
- f"{PROXY_BASE_URL_ENV} / {PROXY_API_KEY_ENV} not configured", pytrace=False
- )
+ base_url, api_key = require_proxy(compat_result)
outcomes = run_claude_models_parallel(
models=ANTHROPIC_MODELS,
diff --git a/tests/e2e/claude_code/tool_use/test_azure.py b/tests/e2e/claude_code/tool_use/test_azure.py
index 484f50a55080..9e7398267c45 100644
--- a/tests/e2e/claude_code/tool_use/test_azure.py
+++ b/tests/e2e/claude_code/tool_use/test_azure.py
@@ -19,23 +19,21 @@
from __future__ import annotations
-import os
from typing import Any, Mapping, Sequence
import pytest
+from claude_code._env import require_proxy
from claude_code.cli_driver import (
ClaudeCLIError,
failure_diagnostic,
run_claude_models_parallel,
)
-PROXY_BASE_URL_ENV = "LITELLM_PROXY_BASE_URL"
-PROXY_API_KEY_ENV = "LITELLM_PROXY_API_KEY"
AZURE_MODELS = [
"claude-haiku-4-5-azure",
- "claude-sonnet-4-6-azure",
+ "claude-sonnet-4-5-azure",
"claude-opus-4-7-azure",
]
@@ -67,24 +65,11 @@ def _has_tool_use_event(events: Sequence[Mapping[str, Any]]) -> bool:
return False
+@pytest.mark.covers("llm.messages.azure_foundry.tool_use.nonstream.works")
def test_tool_use_azure(compat_result):
"""Drive the `claude` CLI against the LiteLLM proxy and assert a
tool call was emitted on the wire."""
- base_url = os.environ.get(PROXY_BASE_URL_ENV)
- api_key = os.environ.get(PROXY_API_KEY_ENV)
- if not base_url or not api_key:
- compat_result.set(
- {
- "status": "fail",
- "error": (
- f"missing required env: set {PROXY_BASE_URL_ENV} and "
- f"{PROXY_API_KEY_ENV} to point at a running LiteLLM proxy"
- ),
- }
- )
- pytest.fail(
- f"{PROXY_BASE_URL_ENV} / {PROXY_API_KEY_ENV} not configured", pytrace=False
- )
+ base_url, api_key = require_proxy(compat_result)
outcomes = run_claude_models_parallel(
models=AZURE_MODELS,
diff --git a/tests/e2e/claude_code/tool_use/test_bedrock_converse.py b/tests/e2e/claude_code/tool_use/test_bedrock_converse.py
index 7d1b58fce90b..33d4d3820d25 100644
--- a/tests/e2e/claude_code/tool_use/test_bedrock_converse.py
+++ b/tests/e2e/claude_code/tool_use/test_bedrock_converse.py
@@ -15,23 +15,21 @@
from __future__ import annotations
-import os
from typing import Any, Mapping, Sequence
import pytest
+from claude_code._env import require_proxy
from claude_code.cli_driver import (
ClaudeCLIError,
failure_diagnostic,
run_claude_models_parallel,
)
-PROXY_BASE_URL_ENV = "LITELLM_PROXY_BASE_URL"
-PROXY_API_KEY_ENV = "LITELLM_PROXY_API_KEY"
BEDROCK_CONVERSE_MODELS = [
"claude-haiku-4-5-bedrock-converse",
- "claude-sonnet-4-6-bedrock-converse",
+ "claude-sonnet-4-5-bedrock-converse",
"claude-opus-4-7-bedrock-converse",
]
@@ -63,24 +61,11 @@ def _has_tool_use_event(events: Sequence[Mapping[str, Any]]) -> bool:
return False
+@pytest.mark.covers("llm.messages.bedrock_converse.tool_use.nonstream.works")
def test_tool_use_bedrock_converse(compat_result):
"""Drive the `claude` CLI against the LiteLLM proxy and assert a
tool call was emitted on the wire."""
- base_url = os.environ.get(PROXY_BASE_URL_ENV)
- api_key = os.environ.get(PROXY_API_KEY_ENV)
- if not base_url or not api_key:
- compat_result.set(
- {
- "status": "fail",
- "error": (
- f"missing required env: set {PROXY_BASE_URL_ENV} and "
- f"{PROXY_API_KEY_ENV} to point at a running LiteLLM proxy"
- ),
- }
- )
- pytest.fail(
- f"{PROXY_BASE_URL_ENV} / {PROXY_API_KEY_ENV} not configured", pytrace=False
- )
+ base_url, api_key = require_proxy(compat_result)
outcomes = run_claude_models_parallel(
models=BEDROCK_CONVERSE_MODELS,
diff --git a/tests/e2e/claude_code/tool_use/test_bedrock_invoke.py b/tests/e2e/claude_code/tool_use/test_bedrock_invoke.py
index 7d2b72b951d1..47ae3aef1dac 100644
--- a/tests/e2e/claude_code/tool_use/test_bedrock_invoke.py
+++ b/tests/e2e/claude_code/tool_use/test_bedrock_invoke.py
@@ -15,23 +15,21 @@
from __future__ import annotations
-import os
from typing import Any, Mapping, Sequence
import pytest
+from claude_code._env import require_proxy
from claude_code.cli_driver import (
ClaudeCLIError,
failure_diagnostic,
run_claude_models_parallel,
)
-PROXY_BASE_URL_ENV = "LITELLM_PROXY_BASE_URL"
-PROXY_API_KEY_ENV = "LITELLM_PROXY_API_KEY"
BEDROCK_INVOKE_MODELS = [
"claude-haiku-4-5-bedrock-invoke",
- "claude-sonnet-4-6-bedrock-invoke",
+ "claude-sonnet-4-5-bedrock-invoke",
"claude-opus-4-7-bedrock-invoke",
]
@@ -63,24 +61,11 @@ def _has_tool_use_event(events: Sequence[Mapping[str, Any]]) -> bool:
return False
+@pytest.mark.covers("llm.messages.bedrock_invoke.tool_use.nonstream.works")
def test_tool_use_bedrock_invoke(compat_result):
"""Drive the `claude` CLI against the LiteLLM proxy and assert a
tool call was emitted on the wire."""
- base_url = os.environ.get(PROXY_BASE_URL_ENV)
- api_key = os.environ.get(PROXY_API_KEY_ENV)
- if not base_url or not api_key:
- compat_result.set(
- {
- "status": "fail",
- "error": (
- f"missing required env: set {PROXY_BASE_URL_ENV} and "
- f"{PROXY_API_KEY_ENV} to point at a running LiteLLM proxy"
- ),
- }
- )
- pytest.fail(
- f"{PROXY_BASE_URL_ENV} / {PROXY_API_KEY_ENV} not configured", pytrace=False
- )
+ base_url, api_key = require_proxy(compat_result)
outcomes = run_claude_models_parallel(
models=BEDROCK_INVOKE_MODELS,
diff --git a/tests/e2e/claude_code/tool_use/test_vertex_ai.py b/tests/e2e/claude_code/tool_use/test_vertex_ai.py
index 0a8ecc9f7a7a..79a3016345cd 100644
--- a/tests/e2e/claude_code/tool_use/test_vertex_ai.py
+++ b/tests/e2e/claude_code/tool_use/test_vertex_ai.py
@@ -15,23 +15,21 @@
from __future__ import annotations
-import os
from typing import Any, Mapping, Sequence
import pytest
+from claude_code._env import require_proxy
from claude_code.cli_driver import (
ClaudeCLIError,
failure_diagnostic,
run_claude_models_parallel,
)
-PROXY_BASE_URL_ENV = "LITELLM_PROXY_BASE_URL"
-PROXY_API_KEY_ENV = "LITELLM_PROXY_API_KEY"
VERTEX_AI_MODELS = [
"claude-haiku-4-5-vertex",
- "claude-sonnet-4-6-vertex",
+ "claude-sonnet-4-5-vertex",
"claude-opus-4-7-vertex",
]
@@ -63,24 +61,11 @@ def _has_tool_use_event(events: Sequence[Mapping[str, Any]]) -> bool:
return False
+@pytest.mark.covers("llm.messages.vertex.tool_use.nonstream.works")
def test_tool_use_vertex_ai(compat_result):
"""Drive the `claude` CLI against the LiteLLM proxy and assert a
tool call was emitted on the wire."""
- base_url = os.environ.get(PROXY_BASE_URL_ENV)
- api_key = os.environ.get(PROXY_API_KEY_ENV)
- if not base_url or not api_key:
- compat_result.set(
- {
- "status": "fail",
- "error": (
- f"missing required env: set {PROXY_BASE_URL_ENV} and "
- f"{PROXY_API_KEY_ENV} to point at a running LiteLLM proxy"
- ),
- }
- )
- pytest.fail(
- f"{PROXY_BASE_URL_ENV} / {PROXY_API_KEY_ENV} not configured", pytrace=False
- )
+ base_url, api_key = require_proxy(compat_result)
outcomes = run_claude_models_parallel(
models=VERTEX_AI_MODELS,
diff --git a/tests/e2e/claude_code/tool_use_streaming/test_anthropic.py b/tests/e2e/claude_code/tool_use_streaming/test_anthropic.py
index 9aa94c892412..152652dcf3c8 100644
--- a/tests/e2e/claude_code/tool_use_streaming/test_anthropic.py
+++ b/tests/e2e/claude_code/tool_use_streaming/test_anthropic.py
@@ -25,23 +25,21 @@
from __future__ import annotations
-import os
from typing import Any, Mapping, Sequence
import pytest
+from claude_code._env import require_proxy
from claude_code.cli_driver import (
ClaudeCLIError,
failure_diagnostic,
run_claude_models_parallel,
)
-PROXY_BASE_URL_ENV = "LITELLM_PROXY_BASE_URL"
-PROXY_API_KEY_ENV = "LITELLM_PROXY_API_KEY"
ANTHROPIC_MODELS = [
"claude-haiku-4-5",
- "claude-sonnet-4-6",
+ "claude-sonnet-4-5",
"claude-opus-4-7",
]
@@ -99,24 +97,11 @@ def _count_input_json_deltas(events: Sequence[Mapping[str, Any]]) -> int:
)
+@pytest.mark.covers("llm.messages.anthropic.tool_use.stream.works")
def test_tool_use_streaming_anthropic(compat_result):
"""Drive the `claude` CLI against the LiteLLM proxy and assert the
proxy preserves fine-grained tool streaming end-to-end."""
- base_url = os.environ.get(PROXY_BASE_URL_ENV)
- api_key = os.environ.get(PROXY_API_KEY_ENV)
- if not base_url or not api_key:
- compat_result.set(
- {
- "status": "fail",
- "error": (
- f"missing required env: set {PROXY_BASE_URL_ENV} and "
- f"{PROXY_API_KEY_ENV} to point at a running LiteLLM proxy"
- ),
- }
- )
- pytest.fail(
- f"{PROXY_BASE_URL_ENV} / {PROXY_API_KEY_ENV} not configured", pytrace=False
- )
+ base_url, api_key = require_proxy(compat_result)
outcomes = run_claude_models_parallel(
models=ANTHROPIC_MODELS,
diff --git a/tests/e2e/claude_code/tool_use_streaming/test_azure.py b/tests/e2e/claude_code/tool_use_streaming/test_azure.py
index c73062b72cd5..8a1cc1852dd0 100644
--- a/tests/e2e/claude_code/tool_use_streaming/test_azure.py
+++ b/tests/e2e/claude_code/tool_use_streaming/test_azure.py
@@ -17,23 +17,21 @@
from __future__ import annotations
-import os
from typing import Any, Mapping, Sequence
import pytest
+from claude_code._env import require_proxy
from claude_code.cli_driver import (
ClaudeCLIError,
failure_diagnostic,
run_claude_models_parallel,
)
-PROXY_BASE_URL_ENV = "LITELLM_PROXY_BASE_URL"
-PROXY_API_KEY_ENV = "LITELLM_PROXY_API_KEY"
AZURE_MODELS = [
"claude-haiku-4-5-azure",
- "claude-sonnet-4-6-azure",
+ "claude-sonnet-4-5-azure",
"claude-opus-4-7-azure",
]
@@ -84,22 +82,9 @@ def _count_input_json_deltas(events: Sequence[Mapping[str, Any]]) -> int:
)
+@pytest.mark.covers("llm.messages.azure_foundry.tool_use.stream.works")
def test_tool_use_streaming_azure(compat_result):
- base_url = os.environ.get(PROXY_BASE_URL_ENV)
- api_key = os.environ.get(PROXY_API_KEY_ENV)
- if not base_url or not api_key:
- compat_result.set(
- {
- "status": "fail",
- "error": (
- f"missing required env: set {PROXY_BASE_URL_ENV} and "
- f"{PROXY_API_KEY_ENV} to point at a running LiteLLM proxy"
- ),
- }
- )
- pytest.fail(
- f"{PROXY_BASE_URL_ENV} / {PROXY_API_KEY_ENV} not configured", pytrace=False
- )
+ base_url, api_key = require_proxy(compat_result)
outcomes = run_claude_models_parallel(
models=AZURE_MODELS,
diff --git a/tests/e2e/claude_code/tool_use_streaming/test_bedrock_converse.py b/tests/e2e/claude_code/tool_use_streaming/test_bedrock_converse.py
index 3642551c7c3e..3b04ed5962fb 100644
--- a/tests/e2e/claude_code/tool_use_streaming/test_bedrock_converse.py
+++ b/tests/e2e/claude_code/tool_use_streaming/test_bedrock_converse.py
@@ -23,23 +23,21 @@
from __future__ import annotations
-import os
from typing import Any, Mapping, Sequence
import pytest
+from claude_code._env import require_proxy
from claude_code.cli_driver import (
ClaudeCLIError,
failure_diagnostic,
run_claude_models_parallel,
)
-PROXY_BASE_URL_ENV = "LITELLM_PROXY_BASE_URL"
-PROXY_API_KEY_ENV = "LITELLM_PROXY_API_KEY"
BEDROCK_CONVERSE_MODELS = [
"claude-haiku-4-5-bedrock-converse",
- "claude-sonnet-4-6-bedrock-converse",
+ "claude-sonnet-4-5-bedrock-converse",
"claude-opus-4-7-bedrock-converse",
]
@@ -90,22 +88,9 @@ def _count_input_json_deltas(events: Sequence[Mapping[str, Any]]) -> int:
)
+@pytest.mark.covers("llm.messages.bedrock_converse.tool_use.stream.works")
def test_tool_use_streaming_bedrock_converse(compat_result):
- base_url = os.environ.get(PROXY_BASE_URL_ENV)
- api_key = os.environ.get(PROXY_API_KEY_ENV)
- if not base_url or not api_key:
- compat_result.set(
- {
- "status": "fail",
- "error": (
- f"missing required env: set {PROXY_BASE_URL_ENV} and "
- f"{PROXY_API_KEY_ENV} to point at a running LiteLLM proxy"
- ),
- }
- )
- pytest.fail(
- f"{PROXY_BASE_URL_ENV} / {PROXY_API_KEY_ENV} not configured", pytrace=False
- )
+ base_url, api_key = require_proxy(compat_result)
outcomes = run_claude_models_parallel(
models=BEDROCK_CONVERSE_MODELS,
diff --git a/tests/e2e/claude_code/tool_use_streaming/test_bedrock_invoke.py b/tests/e2e/claude_code/tool_use_streaming/test_bedrock_invoke.py
index af4689b2847a..c7b61129782a 100644
--- a/tests/e2e/claude_code/tool_use_streaming/test_bedrock_invoke.py
+++ b/tests/e2e/claude_code/tool_use_streaming/test_bedrock_invoke.py
@@ -21,23 +21,21 @@
from __future__ import annotations
-import os
from typing import Any, Mapping, Sequence
import pytest
+from claude_code._env import require_proxy
from claude_code.cli_driver import (
ClaudeCLIError,
failure_diagnostic,
run_claude_models_parallel,
)
-PROXY_BASE_URL_ENV = "LITELLM_PROXY_BASE_URL"
-PROXY_API_KEY_ENV = "LITELLM_PROXY_API_KEY"
BEDROCK_INVOKE_MODELS = [
"claude-haiku-4-5-bedrock-invoke",
- "claude-sonnet-4-6-bedrock-invoke",
+ "claude-sonnet-4-5-bedrock-invoke",
"claude-opus-4-7-bedrock-invoke",
]
@@ -88,22 +86,9 @@ def _count_input_json_deltas(events: Sequence[Mapping[str, Any]]) -> int:
)
+@pytest.mark.covers("llm.messages.bedrock_invoke.tool_use.stream.works")
def test_tool_use_streaming_bedrock_invoke(compat_result):
- base_url = os.environ.get(PROXY_BASE_URL_ENV)
- api_key = os.environ.get(PROXY_API_KEY_ENV)
- if not base_url or not api_key:
- compat_result.set(
- {
- "status": "fail",
- "error": (
- f"missing required env: set {PROXY_BASE_URL_ENV} and "
- f"{PROXY_API_KEY_ENV} to point at a running LiteLLM proxy"
- ),
- }
- )
- pytest.fail(
- f"{PROXY_BASE_URL_ENV} / {PROXY_API_KEY_ENV} not configured", pytrace=False
- )
+ base_url, api_key = require_proxy(compat_result)
outcomes = run_claude_models_parallel(
models=BEDROCK_INVOKE_MODELS,
diff --git a/tests/e2e/claude_code/tool_use_streaming/test_vertex_ai.py b/tests/e2e/claude_code/tool_use_streaming/test_vertex_ai.py
index 19ef9a4e90ef..2912e3aae3d2 100644
--- a/tests/e2e/claude_code/tool_use_streaming/test_vertex_ai.py
+++ b/tests/e2e/claude_code/tool_use_streaming/test_vertex_ai.py
@@ -20,23 +20,21 @@
from __future__ import annotations
-import os
from typing import Any, Mapping, Sequence
import pytest
+from claude_code._env import require_proxy
from claude_code.cli_driver import (
ClaudeCLIError,
failure_diagnostic,
run_claude_models_parallel,
)
-PROXY_BASE_URL_ENV = "LITELLM_PROXY_BASE_URL"
-PROXY_API_KEY_ENV = "LITELLM_PROXY_API_KEY"
VERTEX_AI_MODELS = [
"claude-haiku-4-5-vertex",
- "claude-sonnet-4-6-vertex",
+ "claude-sonnet-4-5-vertex",
"claude-opus-4-7-vertex",
]
@@ -87,22 +85,9 @@ def _count_input_json_deltas(events: Sequence[Mapping[str, Any]]) -> int:
)
+@pytest.mark.covers("llm.messages.vertex.tool_use.stream.works")
def test_tool_use_streaming_vertex_ai(compat_result):
- base_url = os.environ.get(PROXY_BASE_URL_ENV)
- api_key = os.environ.get(PROXY_API_KEY_ENV)
- if not base_url or not api_key:
- compat_result.set(
- {
- "status": "fail",
- "error": (
- f"missing required env: set {PROXY_BASE_URL_ENV} and "
- f"{PROXY_API_KEY_ENV} to point at a running LiteLLM proxy"
- ),
- }
- )
- pytest.fail(
- f"{PROXY_BASE_URL_ENV} / {PROXY_API_KEY_ENV} not configured", pytrace=False
- )
+ base_url, api_key = require_proxy(compat_result)
outcomes = run_claude_models_parallel(
models=VERTEX_AI_MODELS,
diff --git a/tests/e2e/claude_code/vision/test_anthropic.py b/tests/e2e/claude_code/vision/test_anthropic.py
index 650940248ead..f681b2be5aee 100644
--- a/tests/e2e/claude_code/vision/test_anthropic.py
+++ b/tests/e2e/claude_code/vision/test_anthropic.py
@@ -25,22 +25,19 @@
from __future__ import annotations
import json
-import os
-
import pytest
+from claude_code._env import require_proxy
from claude_code.cli_driver import (
ClaudeCLIError,
failure_diagnostic,
run_claude_models_parallel,
)
-PROXY_BASE_URL_ENV = "LITELLM_PROXY_BASE_URL"
-PROXY_API_KEY_ENV = "LITELLM_PROXY_API_KEY"
ANTHROPIC_MODELS = [
"claude-haiku-4-5",
- "claude-sonnet-4-6",
+ "claude-sonnet-4-5",
"claude-opus-4-7",
]
@@ -85,24 +82,11 @@ def _build_stdin_input() -> str:
return json.dumps(user_event) + "\n"
+@pytest.mark.covers("llm.messages.anthropic.vision.nonstream.works")
def test_vision_anthropic(compat_result):
"""Drive the `claude` CLI against the LiteLLM proxy with an image
attached via stream-json input and assert a non-empty reply."""
- base_url = os.environ.get(PROXY_BASE_URL_ENV)
- api_key = os.environ.get(PROXY_API_KEY_ENV)
- if not base_url or not api_key:
- compat_result.set(
- {
- "status": "fail",
- "error": (
- f"missing required env: set {PROXY_BASE_URL_ENV} and "
- f"{PROXY_API_KEY_ENV} to point at a running LiteLLM proxy"
- ),
- }
- )
- pytest.fail(
- f"{PROXY_BASE_URL_ENV} / {PROXY_API_KEY_ENV} not configured", pytrace=False
- )
+ base_url, api_key = require_proxy(compat_result)
outcomes = run_claude_models_parallel(
models=ANTHROPIC_MODELS,
diff --git a/tests/e2e/claude_code/vision/test_azure.py b/tests/e2e/claude_code/vision/test_azure.py
index 3b03c0f2b355..f0eaaad84a23 100644
--- a/tests/e2e/claude_code/vision/test_azure.py
+++ b/tests/e2e/claude_code/vision/test_azure.py
@@ -25,22 +25,19 @@
from __future__ import annotations
import json
-import os
-
import pytest
+from claude_code._env import require_proxy
from claude_code.cli_driver import (
ClaudeCLIError,
failure_diagnostic,
run_claude_models_parallel,
)
-PROXY_BASE_URL_ENV = "LITELLM_PROXY_BASE_URL"
-PROXY_API_KEY_ENV = "LITELLM_PROXY_API_KEY"
AZURE_MODELS = [
"claude-haiku-4-5-azure",
- "claude-sonnet-4-6-azure",
+ "claude-sonnet-4-5-azure",
"claude-opus-4-7-azure",
]
@@ -85,24 +82,11 @@ def _build_stdin_input() -> str:
return json.dumps(user_event) + "\n"
+@pytest.mark.covers("llm.messages.azure_foundry.vision.nonstream.works")
def test_vision_azure(compat_result):
"""Drive the `claude` CLI against the LiteLLM proxy with an image
attached via stream-json input and assert a non-empty reply."""
- base_url = os.environ.get(PROXY_BASE_URL_ENV)
- api_key = os.environ.get(PROXY_API_KEY_ENV)
- if not base_url or not api_key:
- compat_result.set(
- {
- "status": "fail",
- "error": (
- f"missing required env: set {PROXY_BASE_URL_ENV} and "
- f"{PROXY_API_KEY_ENV} to point at a running LiteLLM proxy"
- ),
- }
- )
- pytest.fail(
- f"{PROXY_BASE_URL_ENV} / {PROXY_API_KEY_ENV} not configured", pytrace=False
- )
+ base_url, api_key = require_proxy(compat_result)
outcomes = run_claude_models_parallel(
models=AZURE_MODELS,
diff --git a/tests/e2e/claude_code/vision/test_bedrock_converse.py b/tests/e2e/claude_code/vision/test_bedrock_converse.py
index 4201f9e64fcd..2a5aba5a3930 100644
--- a/tests/e2e/claude_code/vision/test_bedrock_converse.py
+++ b/tests/e2e/claude_code/vision/test_bedrock_converse.py
@@ -25,22 +25,19 @@
from __future__ import annotations
import json
-import os
-
import pytest
+from claude_code._env import require_proxy
from claude_code.cli_driver import (
ClaudeCLIError,
failure_diagnostic,
run_claude_models_parallel,
)
-PROXY_BASE_URL_ENV = "LITELLM_PROXY_BASE_URL"
-PROXY_API_KEY_ENV = "LITELLM_PROXY_API_KEY"
BEDROCK_CONVERSE_MODELS = [
"claude-haiku-4-5-bedrock-converse",
- "claude-sonnet-4-6-bedrock-converse",
+ "claude-sonnet-4-5-bedrock-converse",
"claude-opus-4-7-bedrock-converse",
]
@@ -85,24 +82,11 @@ def _build_stdin_input() -> str:
return json.dumps(user_event) + "\n"
+@pytest.mark.covers("llm.messages.bedrock_converse.vision.nonstream.works")
def test_vision_bedrock_converse(compat_result):
"""Drive the `claude` CLI against the LiteLLM proxy with an image
attached via stream-json input and assert a non-empty reply."""
- base_url = os.environ.get(PROXY_BASE_URL_ENV)
- api_key = os.environ.get(PROXY_API_KEY_ENV)
- if not base_url or not api_key:
- compat_result.set(
- {
- "status": "fail",
- "error": (
- f"missing required env: set {PROXY_BASE_URL_ENV} and "
- f"{PROXY_API_KEY_ENV} to point at a running LiteLLM proxy"
- ),
- }
- )
- pytest.fail(
- f"{PROXY_BASE_URL_ENV} / {PROXY_API_KEY_ENV} not configured", pytrace=False
- )
+ base_url, api_key = require_proxy(compat_result)
outcomes = run_claude_models_parallel(
models=BEDROCK_CONVERSE_MODELS,
diff --git a/tests/e2e/claude_code/vision/test_bedrock_invoke.py b/tests/e2e/claude_code/vision/test_bedrock_invoke.py
index d2e641f14622..5c995cd479ea 100644
--- a/tests/e2e/claude_code/vision/test_bedrock_invoke.py
+++ b/tests/e2e/claude_code/vision/test_bedrock_invoke.py
@@ -25,22 +25,19 @@
from __future__ import annotations
import json
-import os
-
import pytest
+from claude_code._env import require_proxy
from claude_code.cli_driver import (
ClaudeCLIError,
failure_diagnostic,
run_claude_models_parallel,
)
-PROXY_BASE_URL_ENV = "LITELLM_PROXY_BASE_URL"
-PROXY_API_KEY_ENV = "LITELLM_PROXY_API_KEY"
BEDROCK_INVOKE_MODELS = [
"claude-haiku-4-5-bedrock-invoke",
- "claude-sonnet-4-6-bedrock-invoke",
+ "claude-sonnet-4-5-bedrock-invoke",
"claude-opus-4-7-bedrock-invoke",
]
@@ -85,24 +82,11 @@ def _build_stdin_input() -> str:
return json.dumps(user_event) + "\n"
+@pytest.mark.covers("llm.messages.bedrock_invoke.vision.nonstream.works")
def test_vision_bedrock_invoke(compat_result):
"""Drive the `claude` CLI against the LiteLLM proxy with an image
attached via stream-json input and assert a non-empty reply."""
- base_url = os.environ.get(PROXY_BASE_URL_ENV)
- api_key = os.environ.get(PROXY_API_KEY_ENV)
- if not base_url or not api_key:
- compat_result.set(
- {
- "status": "fail",
- "error": (
- f"missing required env: set {PROXY_BASE_URL_ENV} and "
- f"{PROXY_API_KEY_ENV} to point at a running LiteLLM proxy"
- ),
- }
- )
- pytest.fail(
- f"{PROXY_BASE_URL_ENV} / {PROXY_API_KEY_ENV} not configured", pytrace=False
- )
+ base_url, api_key = require_proxy(compat_result)
outcomes = run_claude_models_parallel(
models=BEDROCK_INVOKE_MODELS,
diff --git a/tests/e2e/claude_code/vision/test_vertex_ai.py b/tests/e2e/claude_code/vision/test_vertex_ai.py
index a39ef1a34b71..8d385e295d09 100644
--- a/tests/e2e/claude_code/vision/test_vertex_ai.py
+++ b/tests/e2e/claude_code/vision/test_vertex_ai.py
@@ -25,22 +25,19 @@
from __future__ import annotations
import json
-import os
-
import pytest
+from claude_code._env import require_proxy
from claude_code.cli_driver import (
ClaudeCLIError,
failure_diagnostic,
run_claude_models_parallel,
)
-PROXY_BASE_URL_ENV = "LITELLM_PROXY_BASE_URL"
-PROXY_API_KEY_ENV = "LITELLM_PROXY_API_KEY"
VERTEX_AI_MODELS = [
"claude-haiku-4-5-vertex",
- "claude-sonnet-4-6-vertex",
+ "claude-sonnet-4-5-vertex",
"claude-opus-4-7-vertex",
]
@@ -85,24 +82,11 @@ def _build_stdin_input() -> str:
return json.dumps(user_event) + "\n"
+@pytest.mark.covers("llm.messages.vertex.vision.nonstream.works")
def test_vision_vertex_ai(compat_result):
"""Drive the `claude` CLI against the LiteLLM proxy with an image
attached via stream-json input and assert a non-empty reply."""
- base_url = os.environ.get(PROXY_BASE_URL_ENV)
- api_key = os.environ.get(PROXY_API_KEY_ENV)
- if not base_url or not api_key:
- compat_result.set(
- {
- "status": "fail",
- "error": (
- f"missing required env: set {PROXY_BASE_URL_ENV} and "
- f"{PROXY_API_KEY_ENV} to point at a running LiteLLM proxy"
- ),
- }
- )
- pytest.fail(
- f"{PROXY_BASE_URL_ENV} / {PROXY_API_KEY_ENV} not configured", pytrace=False
- )
+ base_url, api_key = require_proxy(compat_result)
outcomes = run_claude_models_parallel(
models=VERTEX_AI_MODELS,
diff --git a/tests/e2e/claude_code/web_search/test_anthropic.py b/tests/e2e/claude_code/web_search/test_anthropic.py
index b8fa806f9238..a20a2133dc96 100644
--- a/tests/e2e/claude_code/web_search/test_anthropic.py
+++ b/tests/e2e/claude_code/web_search/test_anthropic.py
@@ -27,23 +27,21 @@
from __future__ import annotations
-import os
from typing import Any, Mapping, Sequence
import pytest
+from claude_code._env import require_proxy
from claude_code.cli_driver import (
ClaudeCLIError,
failure_diagnostic,
run_claude_models_parallel,
)
-PROXY_BASE_URL_ENV = "LITELLM_PROXY_BASE_URL"
-PROXY_API_KEY_ENV = "LITELLM_PROXY_API_KEY"
ANTHROPIC_MODELS = [
"claude-haiku-4-5",
- "claude-sonnet-4-6",
+ "claude-sonnet-4-5",
"claude-opus-4-7",
]
@@ -86,26 +84,13 @@ def _has_web_search_tool_use(events: Sequence[Mapping[str, Any]]) -> bool:
return False
+@pytest.mark.covers("llm.messages.anthropic.web_search.nonstream.works")
def test_web_search_anthropic(compat_result):
"""Drive the `claude` CLI against the LiteLLM proxy and assert the
upstream emitted a `tool_use` block calling `WebSearch`, proving
the proxy preserved both the request-side tool definition and the
response-side tool_use block."""
- base_url = os.environ.get(PROXY_BASE_URL_ENV)
- api_key = os.environ.get(PROXY_API_KEY_ENV)
- if not base_url or not api_key:
- compat_result.set(
- {
- "status": "fail",
- "error": (
- f"missing required env: set {PROXY_BASE_URL_ENV} and "
- f"{PROXY_API_KEY_ENV} to point at a running LiteLLM proxy"
- ),
- }
- )
- pytest.fail(
- f"{PROXY_BASE_URL_ENV} / {PROXY_API_KEY_ENV} not configured", pytrace=False
- )
+ base_url, api_key = require_proxy(compat_result)
outcomes = run_claude_models_parallel(
models=ANTHROPIC_MODELS,
diff --git a/tests/e2e/claude_code/web_search/test_azure.py b/tests/e2e/claude_code/web_search/test_azure.py
index e70dc848dcf4..8f9f638fbeeb 100644
--- a/tests/e2e/claude_code/web_search/test_azure.py
+++ b/tests/e2e/claude_code/web_search/test_azure.py
@@ -27,23 +27,21 @@
from __future__ import annotations
-import os
from typing import Any, Mapping, Sequence
import pytest
+from claude_code._env import require_proxy
from claude_code.cli_driver import (
ClaudeCLIError,
failure_diagnostic,
run_claude_models_parallel,
)
-PROXY_BASE_URL_ENV = "LITELLM_PROXY_BASE_URL"
-PROXY_API_KEY_ENV = "LITELLM_PROXY_API_KEY"
AZURE_MODELS = [
"claude-haiku-4-5-azure",
- "claude-sonnet-4-6-azure",
+ "claude-sonnet-4-5-azure",
"claude-opus-4-7-azure",
]
@@ -86,26 +84,13 @@ def _has_web_search_tool_use(events: Sequence[Mapping[str, Any]]) -> bool:
return False
+@pytest.mark.covers("llm.messages.azure_foundry.web_search.nonstream.works")
def test_web_search_azure(compat_result):
"""Drive the `claude` CLI against the LiteLLM proxy and assert the
upstream emitted a `tool_use` block calling `WebSearch`, proving
the proxy preserved both the request-side tool definition and the
response-side tool_use block."""
- base_url = os.environ.get(PROXY_BASE_URL_ENV)
- api_key = os.environ.get(PROXY_API_KEY_ENV)
- if not base_url or not api_key:
- compat_result.set(
- {
- "status": "fail",
- "error": (
- f"missing required env: set {PROXY_BASE_URL_ENV} and "
- f"{PROXY_API_KEY_ENV} to point at a running LiteLLM proxy"
- ),
- }
- )
- pytest.fail(
- f"{PROXY_BASE_URL_ENV} / {PROXY_API_KEY_ENV} not configured", pytrace=False
- )
+ base_url, api_key = require_proxy(compat_result)
outcomes = run_claude_models_parallel(
models=AZURE_MODELS,
diff --git a/tests/e2e/claude_code/web_search/test_bedrock_converse.py b/tests/e2e/claude_code/web_search/test_bedrock_converse.py
index cbeea03df40d..32f37b2be79d 100644
--- a/tests/e2e/claude_code/web_search/test_bedrock_converse.py
+++ b/tests/e2e/claude_code/web_search/test_bedrock_converse.py
@@ -27,23 +27,21 @@
from __future__ import annotations
-import os
from typing import Any, Mapping, Sequence
import pytest
+from claude_code._env import require_proxy
from claude_code.cli_driver import (
ClaudeCLIError,
failure_diagnostic,
run_claude_models_parallel,
)
-PROXY_BASE_URL_ENV = "LITELLM_PROXY_BASE_URL"
-PROXY_API_KEY_ENV = "LITELLM_PROXY_API_KEY"
BEDROCK_CONVERSE_MODELS = [
"claude-haiku-4-5-bedrock-converse",
- "claude-sonnet-4-6-bedrock-converse",
+ "claude-sonnet-4-5-bedrock-converse",
"claude-opus-4-7-bedrock-converse",
]
@@ -86,26 +84,13 @@ def _has_web_search_tool_use(events: Sequence[Mapping[str, Any]]) -> bool:
return False
+@pytest.mark.covers("llm.messages.bedrock_converse.web_search.nonstream.works")
def test_web_search_bedrock_converse(compat_result):
"""Drive the `claude` CLI against the LiteLLM proxy and assert the
upstream emitted a `tool_use` block calling `WebSearch`, proving
the proxy preserved both the request-side tool definition and the
response-side tool_use block."""
- base_url = os.environ.get(PROXY_BASE_URL_ENV)
- api_key = os.environ.get(PROXY_API_KEY_ENV)
- if not base_url or not api_key:
- compat_result.set(
- {
- "status": "fail",
- "error": (
- f"missing required env: set {PROXY_BASE_URL_ENV} and "
- f"{PROXY_API_KEY_ENV} to point at a running LiteLLM proxy"
- ),
- }
- )
- pytest.fail(
- f"{PROXY_BASE_URL_ENV} / {PROXY_API_KEY_ENV} not configured", pytrace=False
- )
+ base_url, api_key = require_proxy(compat_result)
outcomes = run_claude_models_parallel(
models=BEDROCK_CONVERSE_MODELS,
diff --git a/tests/e2e/claude_code/web_search/test_bedrock_invoke.py b/tests/e2e/claude_code/web_search/test_bedrock_invoke.py
index 86068e1e22bd..68d1b30e83fc 100644
--- a/tests/e2e/claude_code/web_search/test_bedrock_invoke.py
+++ b/tests/e2e/claude_code/web_search/test_bedrock_invoke.py
@@ -27,23 +27,21 @@
from __future__ import annotations
-import os
from typing import Any, Mapping, Sequence
import pytest
+from claude_code._env import require_proxy
from claude_code.cli_driver import (
ClaudeCLIError,
failure_diagnostic,
run_claude_models_parallel,
)
-PROXY_BASE_URL_ENV = "LITELLM_PROXY_BASE_URL"
-PROXY_API_KEY_ENV = "LITELLM_PROXY_API_KEY"
BEDROCK_INVOKE_MODELS = [
"claude-haiku-4-5-bedrock-invoke",
- "claude-sonnet-4-6-bedrock-invoke",
+ "claude-sonnet-4-5-bedrock-invoke",
"claude-opus-4-7-bedrock-invoke",
]
@@ -86,26 +84,13 @@ def _has_web_search_tool_use(events: Sequence[Mapping[str, Any]]) -> bool:
return False
+@pytest.mark.covers("llm.messages.bedrock_invoke.web_search.nonstream.works")
def test_web_search_bedrock_invoke(compat_result):
"""Drive the `claude` CLI against the LiteLLM proxy and assert the
upstream emitted a `tool_use` block calling `WebSearch`, proving
the proxy preserved both the request-side tool definition and the
response-side tool_use block."""
- base_url = os.environ.get(PROXY_BASE_URL_ENV)
- api_key = os.environ.get(PROXY_API_KEY_ENV)
- if not base_url or not api_key:
- compat_result.set(
- {
- "status": "fail",
- "error": (
- f"missing required env: set {PROXY_BASE_URL_ENV} and "
- f"{PROXY_API_KEY_ENV} to point at a running LiteLLM proxy"
- ),
- }
- )
- pytest.fail(
- f"{PROXY_BASE_URL_ENV} / {PROXY_API_KEY_ENV} not configured", pytrace=False
- )
+ base_url, api_key = require_proxy(compat_result)
outcomes = run_claude_models_parallel(
models=BEDROCK_INVOKE_MODELS,
diff --git a/tests/e2e/claude_code/web_search/test_vertex_ai.py b/tests/e2e/claude_code/web_search/test_vertex_ai.py
index a33515771f3d..540a8396c981 100644
--- a/tests/e2e/claude_code/web_search/test_vertex_ai.py
+++ b/tests/e2e/claude_code/web_search/test_vertex_ai.py
@@ -27,23 +27,21 @@
from __future__ import annotations
-import os
from typing import Any, Mapping, Sequence
import pytest
+from claude_code._env import require_proxy
from claude_code.cli_driver import (
ClaudeCLIError,
failure_diagnostic,
run_claude_models_parallel,
)
-PROXY_BASE_URL_ENV = "LITELLM_PROXY_BASE_URL"
-PROXY_API_KEY_ENV = "LITELLM_PROXY_API_KEY"
VERTEX_AI_MODELS = [
"claude-haiku-4-5-vertex",
- "claude-sonnet-4-6-vertex",
+ "claude-sonnet-4-5-vertex",
"claude-opus-4-7-vertex",
]
@@ -86,26 +84,13 @@ def _has_web_search_tool_use(events: Sequence[Mapping[str, Any]]) -> bool:
return False
+@pytest.mark.covers("llm.messages.vertex.web_search.nonstream.works")
def test_web_search_vertex_ai(compat_result):
"""Drive the `claude` CLI against the LiteLLM proxy and assert the
upstream emitted a `tool_use` block calling `WebSearch`, proving
the proxy preserved both the request-side tool definition and the
response-side tool_use block."""
- base_url = os.environ.get(PROXY_BASE_URL_ENV)
- api_key = os.environ.get(PROXY_API_KEY_ENV)
- if not base_url or not api_key:
- compat_result.set(
- {
- "status": "fail",
- "error": (
- f"missing required env: set {PROXY_BASE_URL_ENV} and "
- f"{PROXY_API_KEY_ENV} to point at a running LiteLLM proxy"
- ),
- }
- )
- pytest.fail(
- f"{PROXY_BASE_URL_ENV} / {PROXY_API_KEY_ENV} not configured", pytrace=False
- )
+ base_url, api_key = require_proxy(compat_result)
outcomes = run_claude_models_parallel(
models=VERTEX_AI_MODELS,
diff --git a/tests/e2e/coverage_registry/llm_claude_code_compat.yaml b/tests/e2e/coverage_registry/llm_claude_code_compat.yaml
new file mode 100644
index 000000000000..6edf890f7ec6
--- /dev/null
+++ b/tests/e2e/coverage_registry/llm_claude_code_compat.yaml
@@ -0,0 +1,110 @@
+# Claude Code compatibility matrix: /v1/messages coverage across the five provider surfaces
+# claude-code drives (anthropic direct, azure ai foundry, bedrock invoke, bedrock converse,
+# vertex ai). Each row is one (feature x provider) cell in the matrix. The seven anthropic-direct
+# rows already declared in llm_conversational.yaml are NOT duplicated here; the four other
+# provider surfaces plus every feature not already listed for anthropic direct are declared below.
+#
+# Grammar: llm.messages....works
+# route : anthropic | azure_foundry | bedrock_converse | bedrock_invoke | vertex
+# capability : basic | tool_use | vision | thinking | prompt_cache_5m | prompt_cache_1h
+# | structured_output | pdf_input | long_context_1m
+# | thinking_with_tool_use | tool_search | count_tokens | web_search
+# streaming : stream | nonstream
+
+# ---- basic / non-streaming ----
+- {id: llm.messages.azure_foundry.basic.nonstream.works, module: llm, tier: P1, subject_endpoint: messages, route: azure_foundry, capability: basic, streaming: nonstream, assertions: [works], source: "claude_code compat matrix", rationale: "Basic messaging over Azure AI Foundry Anthropic deployments"}
+- {id: llm.messages.bedrock_converse.basic.nonstream.works, module: llm, tier: P1, subject_endpoint: messages, route: bedrock_converse, capability: basic, streaming: nonstream, assertions: [works], source: "claude_code compat matrix", rationale: "Basic messaging over Bedrock Converse Anthropic"}
+- {id: llm.messages.bedrock_invoke.basic.nonstream.works, module: llm, tier: P1, subject_endpoint: messages, route: bedrock_invoke, capability: basic, streaming: nonstream, assertions: [works], source: "claude_code compat matrix", rationale: "Basic messaging over Bedrock Invoke Anthropic"}
+- {id: llm.messages.vertex.basic.nonstream.works, module: llm, tier: P1, subject_endpoint: messages, route: vertex, capability: basic, streaming: nonstream, assertions: [works], source: "claude_code compat matrix", rationale: "Basic messaging over Vertex AI Anthropic"}
+
+# ---- basic / streaming ----
+- {id: llm.messages.azure_foundry.basic.stream.works, module: llm, tier: P1, subject_endpoint: messages, route: azure_foundry, capability: basic, streaming: stream, assertions: [works], source: "claude_code compat matrix", rationale: "Basic streaming over Azure AI Foundry"}
+- {id: llm.messages.bedrock_converse.basic.stream.works, module: llm, tier: P1, subject_endpoint: messages, route: bedrock_converse, capability: basic, streaming: stream, assertions: [works], source: "claude_code compat matrix", rationale: "Basic streaming over Bedrock Converse"}
+- {id: llm.messages.bedrock_invoke.basic.stream.works, module: llm, tier: P1, subject_endpoint: messages, route: bedrock_invoke, capability: basic, streaming: stream, assertions: [works], source: "claude_code compat matrix", rationale: "Basic streaming over Bedrock Invoke"}
+- {id: llm.messages.vertex.basic.stream.works, module: llm, tier: P1, subject_endpoint: messages, route: vertex, capability: basic, streaming: stream, assertions: [works], source: "claude_code compat matrix", rationale: "Basic streaming over Vertex AI"}
+
+# ---- tool_use / non-streaming ----
+- {id: llm.messages.azure_foundry.tool_use.nonstream.works, module: llm, tier: P1, subject_endpoint: messages, route: azure_foundry, capability: tool_use, streaming: nonstream, assertions: [works], source: "claude_code compat matrix", rationale: "Tool use over Azure AI Foundry"}
+- {id: llm.messages.bedrock_converse.tool_use.nonstream.works, module: llm, tier: P1, subject_endpoint: messages, route: bedrock_converse, capability: tool_use, streaming: nonstream, assertions: [works], source: "claude_code compat matrix", rationale: "Tool use over Bedrock Converse"}
+- {id: llm.messages.bedrock_invoke.tool_use.nonstream.works, module: llm, tier: P1, subject_endpoint: messages, route: bedrock_invoke, capability: tool_use, streaming: nonstream, assertions: [works], source: "claude_code compat matrix", rationale: "Tool use over Bedrock Invoke"}
+- {id: llm.messages.vertex.tool_use.nonstream.works, module: llm, tier: P1, subject_endpoint: messages, route: vertex, capability: tool_use, streaming: nonstream, assertions: [works], source: "claude_code compat matrix", rationale: "Tool use over Vertex AI"}
+
+# ---- tool_use / streaming ----
+- {id: llm.messages.azure_foundry.tool_use.stream.works, module: llm, tier: P1, subject_endpoint: messages, route: azure_foundry, capability: tool_use, streaming: stream, assertions: [works], source: "claude_code compat matrix", rationale: "Streaming tool use over Azure AI Foundry"}
+- {id: llm.messages.bedrock_converse.tool_use.stream.works, module: llm, tier: P1, subject_endpoint: messages, route: bedrock_converse, capability: tool_use, streaming: stream, assertions: [works], source: "claude_code compat matrix", rationale: "Streaming tool use over Bedrock Converse"}
+- {id: llm.messages.bedrock_invoke.tool_use.stream.works, module: llm, tier: P1, subject_endpoint: messages, route: bedrock_invoke, capability: tool_use, streaming: stream, assertions: [works], source: "claude_code compat matrix", rationale: "Streaming tool use over Bedrock Invoke"}
+- {id: llm.messages.vertex.tool_use.stream.works, module: llm, tier: P1, subject_endpoint: messages, route: vertex, capability: tool_use, streaming: stream, assertions: [works], source: "claude_code compat matrix", rationale: "Streaming tool use over Vertex AI"}
+
+# ---- vision ----
+- {id: llm.messages.azure_foundry.vision.nonstream.works, module: llm, tier: P1, subject_endpoint: messages, route: azure_foundry, capability: vision, streaming: nonstream, assertions: [works], source: "claude_code compat matrix", rationale: "Vision over Azure AI Foundry"}
+- {id: llm.messages.bedrock_converse.vision.nonstream.works, module: llm, tier: P1, subject_endpoint: messages, route: bedrock_converse, capability: vision, streaming: nonstream, assertions: [works], source: "claude_code compat matrix", rationale: "Vision over Bedrock Converse"}
+- {id: llm.messages.bedrock_invoke.vision.nonstream.works, module: llm, tier: P1, subject_endpoint: messages, route: bedrock_invoke, capability: vision, streaming: nonstream, assertions: [works], source: "claude_code compat matrix", rationale: "Vision over Bedrock Invoke"}
+- {id: llm.messages.vertex.vision.nonstream.works, module: llm, tier: P1, subject_endpoint: messages, route: vertex, capability: vision, streaming: nonstream, assertions: [works], source: "claude_code compat matrix", rationale: "Vision over Vertex AI"}
+
+# ---- thinking ----
+- {id: llm.messages.azure_foundry.thinking.nonstream.works, module: llm, tier: P1, subject_endpoint: messages, route: azure_foundry, capability: thinking, streaming: nonstream, assertions: [works], source: "claude_code compat matrix", rationale: "Extended thinking over Azure AI Foundry"}
+- {id: llm.messages.bedrock_converse.thinking.nonstream.works, module: llm, tier: P1, subject_endpoint: messages, route: bedrock_converse, capability: thinking, streaming: nonstream, assertions: [works], source: "claude_code compat matrix", rationale: "Extended thinking over Bedrock Converse"}
+- {id: llm.messages.bedrock_invoke.thinking.nonstream.works, module: llm, tier: P1, subject_endpoint: messages, route: bedrock_invoke, capability: thinking, streaming: nonstream, assertions: [works], source: "claude_code compat matrix", rationale: "Extended thinking over Bedrock Invoke"}
+- {id: llm.messages.vertex.thinking.nonstream.works, module: llm, tier: P1, subject_endpoint: messages, route: vertex, capability: thinking, streaming: nonstream, assertions: [works], source: "claude_code compat matrix", rationale: "Extended thinking over Vertex AI"}
+
+# ---- prompt_cache_5m ----
+- {id: llm.messages.azure_foundry.prompt_cache_5m.nonstream.works, module: llm, tier: P1, subject_endpoint: messages, route: azure_foundry, capability: prompt_cache_5m, streaming: nonstream, assertions: [works], source: "claude_code compat matrix", rationale: "5m prompt cache over Azure AI Foundry"}
+- {id: llm.messages.bedrock_converse.prompt_cache_5m.nonstream.works, module: llm, tier: P1, subject_endpoint: messages, route: bedrock_converse, capability: prompt_cache_5m, streaming: nonstream, assertions: [works], source: "claude_code compat matrix", rationale: "5m prompt cache over Bedrock Converse"}
+- {id: llm.messages.bedrock_invoke.prompt_cache_5m.nonstream.works, module: llm, tier: P1, subject_endpoint: messages, route: bedrock_invoke, capability: prompt_cache_5m, streaming: nonstream, assertions: [works], source: "claude_code compat matrix", rationale: "5m prompt cache over Bedrock Invoke"}
+- {id: llm.messages.vertex.prompt_cache_5m.nonstream.works, module: llm, tier: P1, subject_endpoint: messages, route: vertex, capability: prompt_cache_5m, streaming: nonstream, assertions: [works], source: "claude_code compat matrix", rationale: "5m prompt cache over Vertex AI"}
+
+# ---- prompt_cache_1h ----
+- {id: llm.messages.anthropic.prompt_cache_1h.nonstream.works, module: llm, tier: P1, subject_endpoint: messages, route: anthropic, capability: prompt_cache_1h, streaming: nonstream, assertions: [works], source: "claude_code compat matrix", rationale: "1h prompt cache over Anthropic direct"}
+- {id: llm.messages.azure_foundry.prompt_cache_1h.nonstream.works, module: llm, tier: P1, subject_endpoint: messages, route: azure_foundry, capability: prompt_cache_1h, streaming: nonstream, assertions: [works], source: "claude_code compat matrix", rationale: "1h prompt cache over Azure AI Foundry"}
+- {id: llm.messages.bedrock_converse.prompt_cache_1h.nonstream.works, module: llm, tier: P1, subject_endpoint: messages, route: bedrock_converse, capability: prompt_cache_1h, streaming: nonstream, assertions: [works], source: "claude_code compat matrix", rationale: "1h prompt cache over Bedrock Converse"}
+- {id: llm.messages.bedrock_invoke.prompt_cache_1h.nonstream.works, module: llm, tier: P1, subject_endpoint: messages, route: bedrock_invoke, capability: prompt_cache_1h, streaming: nonstream, assertions: [works], source: "claude_code compat matrix", rationale: "1h prompt cache over Bedrock Invoke"}
+- {id: llm.messages.vertex.prompt_cache_1h.nonstream.works, module: llm, tier: P1, subject_endpoint: messages, route: vertex, capability: prompt_cache_1h, streaming: nonstream, assertions: [works], source: "claude_code compat matrix", rationale: "1h prompt cache over Vertex AI"}
+
+# ---- structured_output ----
+- {id: llm.messages.anthropic.structured_output.nonstream.works, module: llm, tier: P1, subject_endpoint: messages, route: anthropic, capability: structured_output, streaming: nonstream, assertions: [works], source: "claude_code compat matrix", rationale: "Structured outputs (--json-schema) over Anthropic direct"}
+- {id: llm.messages.azure_foundry.structured_output.nonstream.works, module: llm, tier: P1, subject_endpoint: messages, route: azure_foundry, capability: structured_output, streaming: nonstream, assertions: [works], source: "claude_code compat matrix", rationale: "Structured outputs over Azure AI Foundry"}
+- {id: llm.messages.bedrock_converse.structured_output.nonstream.works, module: llm, tier: P1, subject_endpoint: messages, route: bedrock_converse, capability: structured_output, streaming: nonstream, assertions: [works], source: "claude_code compat matrix", rationale: "Structured outputs over Bedrock Converse"}
+- {id: llm.messages.bedrock_invoke.structured_output.nonstream.works, module: llm, tier: P1, subject_endpoint: messages, route: bedrock_invoke, capability: structured_output, streaming: nonstream, assertions: [works], source: "claude_code compat matrix", rationale: "Structured outputs over Bedrock Invoke"}
+- {id: llm.messages.vertex.structured_output.nonstream.works, module: llm, tier: P1, subject_endpoint: messages, route: vertex, capability: structured_output, streaming: nonstream, assertions: [works], source: "claude_code compat matrix", rationale: "Structured outputs over Vertex AI"}
+
+# ---- pdf_input ----
+- {id: llm.messages.anthropic.pdf_input.nonstream.works, module: llm, tier: P1, subject_endpoint: messages, route: anthropic, capability: pdf_input, streaming: nonstream, assertions: [works], source: "claude_code compat matrix", rationale: "PDF document input over Anthropic direct"}
+- {id: llm.messages.azure_foundry.pdf_input.nonstream.works, module: llm, tier: P1, subject_endpoint: messages, route: azure_foundry, capability: pdf_input, streaming: nonstream, assertions: [works], source: "claude_code compat matrix", rationale: "PDF document input over Azure AI Foundry"}
+- {id: llm.messages.bedrock_converse.pdf_input.nonstream.works, module: llm, tier: P1, subject_endpoint: messages, route: bedrock_converse, capability: pdf_input, streaming: nonstream, assertions: [works], source: "claude_code compat matrix", rationale: "PDF document input over Bedrock Converse"}
+- {id: llm.messages.bedrock_invoke.pdf_input.nonstream.works, module: llm, tier: P1, subject_endpoint: messages, route: bedrock_invoke, capability: pdf_input, streaming: nonstream, assertions: [works], source: "claude_code compat matrix", rationale: "PDF document input over Bedrock Invoke"}
+- {id: llm.messages.vertex.pdf_input.nonstream.works, module: llm, tier: P1, subject_endpoint: messages, route: vertex, capability: pdf_input, streaming: nonstream, assertions: [works], source: "claude_code compat matrix", rationale: "PDF document input over Vertex AI"}
+
+# ---- long_context_1m ----
+- {id: llm.messages.anthropic.long_context_1m.nonstream.works, module: llm, tier: P1, subject_endpoint: messages, route: anthropic, capability: long_context_1m, streaming: nonstream, assertions: [works], source: "claude_code compat matrix", rationale: "1M context beta over Anthropic direct"}
+- {id: llm.messages.azure_foundry.long_context_1m.nonstream.works, module: llm, tier: P1, subject_endpoint: messages, route: azure_foundry, capability: long_context_1m, streaming: nonstream, assertions: [works], source: "claude_code compat matrix", rationale: "1M context beta over Azure AI Foundry"}
+- {id: llm.messages.bedrock_converse.long_context_1m.nonstream.works, module: llm, tier: P1, subject_endpoint: messages, route: bedrock_converse, capability: long_context_1m, streaming: nonstream, assertions: [works], source: "claude_code compat matrix", rationale: "1M context beta over Bedrock Converse"}
+- {id: llm.messages.bedrock_invoke.long_context_1m.nonstream.works, module: llm, tier: P1, subject_endpoint: messages, route: bedrock_invoke, capability: long_context_1m, streaming: nonstream, assertions: [works], source: "claude_code compat matrix", rationale: "1M context beta over Bedrock Invoke"}
+- {id: llm.messages.vertex.long_context_1m.nonstream.works, module: llm, tier: P1, subject_endpoint: messages, route: vertex, capability: long_context_1m, streaming: nonstream, assertions: [works], source: "claude_code compat matrix", rationale: "1M context beta over Vertex AI"}
+
+# ---- thinking_with_tool_use ----
+- {id: llm.messages.anthropic.thinking_with_tool_use.nonstream.works, module: llm, tier: P1, subject_endpoint: messages, route: anthropic, capability: thinking_with_tool_use, streaming: nonstream, assertions: [works], source: "claude_code compat matrix", rationale: "Thinking + tool_use interleaved over Anthropic direct"}
+- {id: llm.messages.azure_foundry.thinking_with_tool_use.nonstream.works, module: llm, tier: P1, subject_endpoint: messages, route: azure_foundry, capability: thinking_with_tool_use, streaming: nonstream, assertions: [works], source: "claude_code compat matrix", rationale: "Thinking + tool_use interleaved over Azure AI Foundry"}
+- {id: llm.messages.bedrock_converse.thinking_with_tool_use.nonstream.works, module: llm, tier: P1, subject_endpoint: messages, route: bedrock_converse, capability: thinking_with_tool_use, streaming: nonstream, assertions: [works], source: "claude_code compat matrix", rationale: "Thinking + tool_use interleaved over Bedrock Converse"}
+- {id: llm.messages.bedrock_invoke.thinking_with_tool_use.nonstream.works, module: llm, tier: P1, subject_endpoint: messages, route: bedrock_invoke, capability: thinking_with_tool_use, streaming: nonstream, assertions: [works], source: "claude_code compat matrix", rationale: "Thinking + tool_use interleaved over Bedrock Invoke"}
+- {id: llm.messages.vertex.thinking_with_tool_use.nonstream.works, module: llm, tier: P1, subject_endpoint: messages, route: vertex, capability: thinking_with_tool_use, streaming: nonstream, assertions: [works], source: "claude_code compat matrix", rationale: "Thinking + tool_use interleaved over Vertex AI"}
+
+# ---- tool_search ----
+- {id: llm.messages.anthropic.tool_search.nonstream.works, module: llm, tier: P1, subject_endpoint: messages, route: anthropic, capability: tool_search, streaming: nonstream, assertions: [works], source: "claude_code compat matrix", rationale: "tool_search_tool_regex_20251119 discovery tool over Anthropic direct"}
+- {id: llm.messages.azure_foundry.tool_search.nonstream.works, module: llm, tier: P1, subject_endpoint: messages, route: azure_foundry, capability: tool_search, streaming: nonstream, assertions: [works], source: "claude_code compat matrix", rationale: "tool_search discovery tool over Azure AI Foundry"}
+- {id: llm.messages.bedrock_converse.tool_search.nonstream.works, module: llm, tier: P1, subject_endpoint: messages, route: bedrock_converse, capability: tool_search, streaming: nonstream, assertions: [works], source: "claude_code compat matrix", rationale: "tool_search discovery tool over Bedrock Converse"}
+- {id: llm.messages.bedrock_invoke.tool_search.nonstream.works, module: llm, tier: P1, subject_endpoint: messages, route: bedrock_invoke, capability: tool_search, streaming: nonstream, assertions: [works], source: "claude_code compat matrix", rationale: "tool_search discovery tool over Bedrock Invoke"}
+- {id: llm.messages.vertex.tool_search.nonstream.works, module: llm, tier: P1, subject_endpoint: messages, route: vertex, capability: tool_search, streaming: nonstream, assertions: [works], source: "claude_code compat matrix", rationale: "tool_search discovery tool over Vertex AI"}
+
+# ---- count_tokens ----
+- {id: llm.messages.anthropic.count_tokens.nonstream.works, module: llm, tier: P1, subject_endpoint: messages, route: anthropic, capability: count_tokens, streaming: nonstream, assertions: [works], source: "claude_code compat matrix", rationale: "/v1/messages/count_tokens over Anthropic direct"}
+- {id: llm.messages.azure_foundry.count_tokens.nonstream.works, module: llm, tier: P1, subject_endpoint: messages, route: azure_foundry, capability: count_tokens, streaming: nonstream, assertions: [works], source: "claude_code compat matrix", rationale: "/v1/messages/count_tokens over Azure AI Foundry"}
+- {id: llm.messages.bedrock_converse.count_tokens.nonstream.works, module: llm, tier: P1, subject_endpoint: messages, route: bedrock_converse, capability: count_tokens, streaming: nonstream, assertions: [works], source: "claude_code compat matrix", rationale: "/v1/messages/count_tokens over Bedrock Converse"}
+- {id: llm.messages.bedrock_invoke.count_tokens.nonstream.works, module: llm, tier: P1, subject_endpoint: messages, route: bedrock_invoke, capability: count_tokens, streaming: nonstream, assertions: [works], source: "claude_code compat matrix", rationale: "/v1/messages/count_tokens over Bedrock Invoke"}
+- {id: llm.messages.vertex.count_tokens.nonstream.works, module: llm, tier: P1, subject_endpoint: messages, route: vertex, capability: count_tokens, streaming: nonstream, assertions: [works], source: "claude_code compat matrix", rationale: "/v1/messages/count_tokens over Vertex AI"}
+
+# ---- web_search ----
+- {id: llm.messages.anthropic.web_search.nonstream.works, module: llm, tier: P1, subject_endpoint: messages, route: anthropic, capability: web_search, streaming: nonstream, assertions: [works], source: "claude_code compat matrix", rationale: "Web search server tool over Anthropic direct"}
+- {id: llm.messages.azure_foundry.web_search.nonstream.works, module: llm, tier: P1, subject_endpoint: messages, route: azure_foundry, capability: web_search, streaming: nonstream, assertions: [works], source: "claude_code compat matrix", rationale: "Web search server tool over Azure AI Foundry"}
+- {id: llm.messages.bedrock_converse.web_search.nonstream.works, module: llm, tier: P1, subject_endpoint: messages, route: bedrock_converse, capability: web_search, streaming: nonstream, assertions: [works], source: "claude_code compat matrix", rationale: "Web search server tool over Bedrock Converse"}
+- {id: llm.messages.bedrock_invoke.web_search.nonstream.works, module: llm, tier: P1, subject_endpoint: messages, route: bedrock_invoke, capability: web_search, streaming: nonstream, assertions: [works], source: "claude_code compat matrix", rationale: "Web search server tool over Bedrock Invoke"}
+- {id: llm.messages.vertex.web_search.nonstream.works, module: llm, tier: P1, subject_endpoint: messages, route: vertex, capability: web_search, streaming: nonstream, assertions: [works], source: "claude_code compat matrix", rationale: "Web search server tool over Vertex AI"}
diff --git a/tests/e2e/coverage_registry/schema.py b/tests/e2e/coverage_registry/schema.py
index 7482088d93fc..a76774c3bdeb 100644
--- a/tests/e2e/coverage_registry/schema.py
+++ b/tests/e2e/coverage_registry/schema.py
@@ -54,13 +54,20 @@ class FailBeforeFix(str, Enum):
LlmCapability = Literal[
"basic",
+ "count_tokens",
+ "long_context_1m",
"mid_conversation_system",
+ "pdf_input",
+ "prompt_cache_1h",
"prompt_cache_5m",
"service_tier",
"structured_output",
"thinking",
+ "thinking_with_tool_use",
+ "tool_search",
"tool_use",
"vision",
+ "web_search",
]
diff --git a/tests/e2e/docker-compose.yml b/tests/e2e/docker-compose.yml
index e2bb6ca89332..87075d63dc36 100644
--- a/tests/e2e/docker-compose.yml
+++ b/tests/e2e/docker-compose.yml
@@ -99,6 +99,8 @@ services:
MISTRAL_API_KEY: ${MISTRAL_API_KEY:-}
AZURE_API_BASE: ${AZURE_API_BASE:-}
AZURE_API_KEY: ${AZURE_API_KEY:-}
+ AZURE_AI_API_BASE: ${AZURE_AI_API_BASE:-}
+ AZURE_AI_API_KEY: ${AZURE_AI_API_KEY:-}
ports:
- "4000:4000"
configs:
diff --git a/tests/e2e/e2e_gateway.py b/tests/e2e/e2e_gateway.py
index d40b96d60fab..ad8b2e833a80 100644
--- a/tests/e2e/e2e_gateway.py
+++ b/tests/e2e/e2e_gateway.py
@@ -319,21 +319,31 @@ def probe(self, path: str, *, params: NoBody) -> ProbeResult:
return self.transport.probe(path, params=params)
-def build_gateway() -> Gateway:
+def build_gateway(
+ *,
+ base_url: str = PROXY_BASE_URL,
+ master_key: str = MASTER_KEY,
+ control_plane_base_url: str = CONTROL_PLANE_BASE_URL,
+) -> Gateway:
"""The Gateway every suite's client is built from: a SplitTransport that routes
LLM calls to the data plane (PROXY_BASE_URL) and management/admin calls to the
control plane (CONTROL_PLANE_BASE_URL), with the shared poll budget. The two
- base URLs are the same for a monolithic proxy, so routing is then a no-op."""
+ base URLs are the same for a monolithic proxy, so routing is then a no-op.
+
+ The endpoints are injectable for callers that resolve the proxy some other
+ way than ``e2e_config``'s env names (see ``claude_code/_env.py``); they must
+ pass all three together, since a caller that overrides only the data plane
+ would leave management calls pointed at the env default."""
return Gateway(
transport=SplitTransport(
data=HttpTransport(
- base_url=PROXY_BASE_URL,
- master_key=MASTER_KEY,
+ base_url=base_url,
+ master_key=master_key,
request_timeout=REQUEST_TIMEOUT,
),
control=HttpTransport(
- base_url=CONTROL_PLANE_BASE_URL,
- master_key=MASTER_KEY,
+ base_url=control_plane_base_url,
+ master_key=master_key,
request_timeout=REQUEST_TIMEOUT,
),
),
diff --git a/tests/e2e/models.py b/tests/e2e/models.py
index 4140967f3e0c..56ed1c8b62c2 100644
--- a/tests/e2e/models.py
+++ b/tests/e2e/models.py
@@ -440,6 +440,7 @@ class LiteLLMParamsBody(BaseModel):
aws_batch_role_arn: str | None = None
input_cost_per_token: float | None = None
output_cost_per_token: float | None = None
+ extra_headers: dict[str, str] | None = None
ModelMode = Literal["batch", "realtime", "image_generation"]