litellm/tests/e2e/router/conftest.py
Yassin Kortam 3810130105
test(e2e): add reliability suite covering fallback, timeout, and cache behavior (#34023)
* test(e2e): add reliability suite covering fallback, timeout, and cache behavior

* test(e2e): move reliability suite under router and drive it with real deployments

* test(e2e): make the router complexity fixture opt-in so reliability tests can coexist
2026-07-20 23:06:46 +00:00

127 lines
4.5 KiB
Python

"""Router suite's `client` fixture.
The shared lifecycle (resources/scoped_key), proxy liveness gate, and e2e marker
live in the parent tests/e2e/conftest.py. ComplexityRouterClient holds the shared
ProxyClient, so the `resources` fixture cleans up keys this suite creates.
Also registers `complexity-smart-router` via management /model/new when the
proxy does not already list it (compose has it in static config; stage does not).
"""
from __future__ import annotations
from collections.abc import Iterator
import pytest
from requests import RequestException
from complexity_router_client import ComplexityRouterClient, build_client
from proxy_client import ProxyClient
from e2e_http import NoBody, Success
from lifecycle import ResourceManager
from models import (
ChatBody,
ChatMessage,
KeyGenerateBody,
LiteLLMParamsBody,
ModelsListResponse,
)
ROUTER_MODEL = "complexity-smart-router"
ROUTER_PARAMS = LiteLLMParamsBody(
model="auto_router/complexity_router",
complexity_router_config={
"classifier_type": "llm",
"classifier_llm_config": {"model": "gpt-5.5"},
"tiers": {
"SIMPLE": "gpt-5.5",
"MEDIUM": "claude-haiku-4-5",
"COMPLEX": "claude-haiku-4-5",
"REASONING": "claude-haiku-4-5",
},
},
)
# Key must be allowed to call the virtual router and both tier backends.
ROUTER_KEY_MODELS = [ROUTER_MODEL, "gpt-5.5", "claude-haiku-4-5"]
@pytest.fixture(scope="session")
def client(proxy: ProxyClient) -> ComplexityRouterClient:
return build_client(proxy)
def _model_is_servable(proxy: ProxyClient, model_name: str) -> bool:
result = proxy.transport.get(
"/v1/models",
headers=proxy.transport.master,
params=NoBody(),
response_type=ModelsListResponse,
)
return isinstance(result, Success) and any(entry.id == model_name for entry in result.data.data)
def _router_is_callable(proxy: ProxyClient) -> bool:
"""True only when a short chat against the virtual router succeeds; every error
(the Invalid-model-name reload race, but also 401, 5xx, and network) counts as
not-callable so infra/auth blips can't be mistaken for a working router."""
key = proxy.generate_key(KeyGenerateBody(models=ROUTER_KEY_MODELS, user_id="e2e-complexity-probe"))
try:
result = proxy.chat(
key,
ChatBody(
model=ROUTER_MODEL,
messages=[ChatMessage(role="user", content="hi")],
max_tokens=1,
),
)
finally:
proxy.delete_key(key)
return isinstance(result, Success)
@pytest.fixture(scope="session")
def _ensure_complexity_smart_router( # pyright: ignore[reportUnusedFunction] # requested by the complexity test via usefixtures, wired by name
client: ComplexityRouterClient,
) -> Iterator[None]:
"""Ensure the complexity router virtual model exists for this session.
Compose already declares it in docker-compose.yml; stage does not. Register
via ProxyClient.create_model (waits for data-plane /v1/models) when missing, then
probe a real chat so a list-only false positive cannot pass the fixture.
"""
proxy = client.proxy
if _model_is_servable(proxy, ROUTER_MODEL) and _router_is_callable(proxy):
yield
return
try:
model_id = proxy.create_model(ROUTER_MODEL, ROUTER_PARAMS)
except (AssertionError, RequestException) as exc:
if _model_is_servable(proxy, ROUTER_MODEL) and _router_is_callable(proxy):
yield
return
raise AssertionError(
f"failed to register {ROUTER_MODEL!r} for the complexity router e2e "
f"(not listed/callable on the data plane and /model/new failed): {exc}"
) from exc
try:
if not _router_is_callable(proxy):
raise AssertionError(
f"{ROUTER_MODEL!r} registered as {model_id!r} and listed on "
f"/v1/models but chat still returns Invalid model name; "
f"data-plane router reload incomplete"
)
yield
finally:
proxy.delete_model(model_id)
@pytest.fixture
def complexity_key(resources: ResourceManager, client: ComplexityRouterClient) -> str:
"""Per-test key allowed to call the complexity router and its tier backends."""
key = client.proxy.generate_key(
KeyGenerateBody(models=ROUTER_KEY_MODELS, user_id="e2e-complexity-router")
)
resources.defer(lambda: client.proxy.delete_key(key))
return key