litellm/tests/e2e/router/conftest.py
mubashir1osmani 224fe67f10
test: e2e staging leftovers (#33613)
* test(e2e): read datadog log delivery back from the real datadog api (#33604)

* test(e2e): read datadog log delivery back from the real datadog api

* test(e2e): compare datadog-read cost with math.isclose, not bit-equality

The response_cost now round-trips through DataDog's attribute indexing
pipeline, whose float serialization is not guaranteed to preserve the
exact bit pattern the proxy shipped. rel_tol=1e-9 (equal to 9 significant
digits) still fails on any real cost discrepancy while tolerating
representation drift. Addresses the Greptile P2 on this PR.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(e2e): widen the duplicate-settle window to 30s for real DataDog

Against the local sink one poll interval (5s) after the first hit was
enough to catch a same-call duplicate, because both events arrived in the
same flush batch. Against real DataDog, ingestion jitter can make one
call's two events searchable tens of seconds apart, so a 5s settle could
let the LIT-4447 duplicate slip past the exactly-one assertion. The reader
now keeps re-reading for DD_SETTLE_SECONDS (default 30s, env-overridable
via E2E_DD_SETTLE_SECONDS) after the first event appears, returning early
only when a duplicate is already visible - more waiting cannot clear it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(e2e): point UI tests at dashboard service; register complexity router

Stage gateway 404s /ui; the Next.js dashboard is litellm-ui:3000. Drive
playwright against E2E_UI_BASE_URL and wait on login placeholders after
client render. Register complexity-smart-router via /model/new when the
proxy does not already list it so stage matches compose config

* docs(e2e): clarify E2E_UI_BASE_URL should be ALB when ingress splits UI

* docs(e2e): prefer single path-routing host for control plane and UI

CONTROL_PLANE and UI already default to PROXY_BASE_URL; clarify that
stage should set one ALB host rather than three endpoints

* fix(e2e): always capture complexity router model_id for teardown

Split /model/new from the data-plane wait so a propagation timeout still
deletes the control-plane registration (greptile orphan-model concern)

* fix(e2e): click exact Login button so SSO control is not matched

Playwright strict mode matched both Login and Login with SSO

* fix(router): score complexity by difficulty not request length

The LLM classifier prompt treated short wording as SIMPLE, so probes like
"Is P equal to NP?" stayed on the SIMPLE backend even though the classifier
ran. Judge intellectual difficulty so short hard questions route higher

* fix(e2e): open key edit via Key ID and wait for team models

Key Alias text is not the row open control on the virtual keys table;
KeyInfoView opens from the Key ID button in that row. Also wait for a
real team model in the edit Models dropdown so we do not race the async
availableModels fetch that only has All Team Models on first paint

* fix(e2e): keep settled DD events on empty search; bump mcp for OSV

Do not let a transient empty DataDog search wipe events already seen in
the settle window (Greptile P1). Make the logs-search from window
env-overridable via E2E_DD_SEARCH_FROM (Greptile P2). Prefer the mono
Key ID button when opening key edit. Bump mcp 1.26.0 -> 1.28.1 so OSV
clears the three high GHSA findings on the staging PR

* revert: drop mcp lock bump from e2e staging PR

OSV mcp upgrade is unrelated to the e2e fixes; leave the dep pin alone

---------

Co-authored-by: yucheng-berri <yucheng@berri.ai>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 17:52:41 -07:00

122 lines
3.9 KiB
Python

"""Router suite's `client` fixture.
The shared lifecycle (resources/scoped_key), proxy liveness skip, and e2e marker
live in the parent tests/e2e/conftest.py. ComplexityRouterClient holds the shared
Gateway, 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
import time
from collections.abc import Iterator
import pytest
from requests import RequestException
from complexity_router_client import ComplexityRouterClient, build_client
from e2e_gateway import Gateway
from e2e_http import NoBody, Success, unwrap
from models import (
LiteLLMParamsBody,
ModelInfoBody,
ModelNewBody,
ModelNewResponse,
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",
},
},
)
@pytest.fixture(scope="session")
def client() -> ComplexityRouterClient:
return build_client()
def _model_is_servable(gateway: Gateway, model_name: str) -> bool:
result = gateway.transport.get(
"/v1/models",
headers=gateway.transport.master,
params=NoBody(),
response_type=ModelsListResponse,
)
return isinstance(result, Success) and any(entry.id == model_name for entry in result.data.data)
def _register_router_model(gateway: Gateway) -> str:
"""POST /model/new only; returns the proxy model_id before data-plane wait.
Split from create_model so a slow control→data propagation timeout still
leaves us a model_id for teardown (avoids orphaning complexity-smart-router).
"""
return unwrap(
gateway.transport.post(
"/model/new",
headers=gateway.transport.master,
json=ModelNewBody(
model_name=ROUTER_MODEL,
litellm_params=ROUTER_PARAMS,
model_info=ModelInfoBody(),
),
response_type=ModelNewResponse,
)
).model_id
def _await_router_model_servable(gateway: Gateway) -> None:
deadline = time.monotonic() + gateway.poll_timeout
while time.monotonic() < deadline:
if _model_is_servable(gateway, ROUTER_MODEL):
return
time.sleep(gateway.poll_interval)
raise AssertionError(
f"model {ROUTER_MODEL!r} was created but never became servable on the data "
f"plane within {gateway.poll_timeout}s of /model/new"
)
@pytest.fixture(scope="session", autouse=True)
def _ensure_complexity_smart_router( # pyright: ignore[reportUnusedFunction] # pytest autouse session fixture, 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 /model/new when missing and tear down only what we created.
"""
gateway = client.gateway
if _model_is_servable(gateway, ROUTER_MODEL):
yield
return
try:
model_id = _register_router_model(gateway)
except (AssertionError, RequestException) as exc:
if _model_is_servable(gateway, ROUTER_MODEL):
yield
return
raise AssertionError(
f"failed to register {ROUTER_MODEL!r} for the complexity router e2e "
f"(not listed on /v1/models and /model/new failed): {exc}"
) from exc
try:
_await_router_model_servable(gateway)
yield
finally:
gateway.delete_model(model_id)