litellm/tests/integration/authorization/test_wildcard_model_access.py
yuneng-jiang 44d99475a5
test(integration): stop GET /__observations from draining the upstream log (#47256)
* test(integration): stop GET /__observations from draining the upstream log

GET /__observations used to empty the queue, so a second read in a test
saw nothing and a "never reached upstream" check could pass whether or not
the request was forwarded. GET now returns every request since the last
reset and DELETE /__observations is the reset. An autouse fixture resets
the worker upstream before each test, which also covers tests that read
without the gateway fixture.

Discarded reads used as resets become DELETEs, helpers that accumulated
drained reads now read one snapshot, and tests with several phases reset
between them. The three vacuous negative checks (rag allowlist, guardrail
opt-out chaos, xAI batch output) now assert the absence on the same read
that shows a request which did reach the upstream.

* style(integration): space the observations unit test like its neighbours
2026-10-18 09:58:05 +00:00

91 lines
3.8 KiB
Python

import uuid
from collections.abc import Iterator
from pathlib import Path
from typing import Final
import httpx
import pytest
import yaml
from integration._support.client import Gateway, gateway_from_environment
from integration._support.process import owned_proxy
from pydantic import JsonValue
pytestmark: Final = pytest.mark.timeout(180)
def _deployment(model_name: str, model: str, upstream_url: str) -> dict[str, JsonValue]:
return {
"model_name": model_name,
"litellm_params": {"model": model, "api_base": f"{upstream_url}/v1", "api_key": "synthetic-wildcard-key"},
}
@pytest.fixture(scope="module")
def candidate(tmp_path_factory: pytest.TempPathFactory) -> Iterator[Gateway]:
directory: Final = tmp_path_factory.mktemp("wildcard-access")
with gateway_from_environment() as base:
config: Final = yaml.safe_load(Path("tests/integration/proxy_config.yaml").read_text())
config["model_list"] = [
_deployment("*", "openai/*", base.upstream_url),
_deployment("anthropic/*", "openai/*", base.upstream_url),
_deployment("groq/*", "openai/*", base.upstream_url),
_deployment("good-model", "openai/good-model-upstream", base.upstream_url),
]
path: Final = directory / "wildcard-access.yaml"
path.write_text(yaml.safe_dump(config))
with owned_proxy(base, directory, {}, config=path) as proxy:
yield proxy
@pytest.fixture
def upstream(candidate: Gateway) -> Iterator[httpx.Client]:
with httpx.Client(base_url=candidate.upstream_url, timeout=5, trust_env=False) as client:
client.delete("/__observations").raise_for_status()
yield client
def _chat(gateway: Gateway, model: str, key: str) -> httpx.Response:
return gateway.request(
"POST",
"/v1/chat/completions",
{"model": model, "messages": [{"role": "user", "content": f"wildcard {uuid.uuid4().hex}"}]},
key=key,
)
def _observed_models(upstream: httpx.Client) -> list[JsonValue]:
return [request["body"]["model"] for request in upstream.get("/__observations").json()["requests"]]
def test_an_all_models_key_reaches_a_model_served_only_by_the_catch_all_deployment(
candidate: Gateway, upstream: httpx.Client
) -> None:
with candidate.scenario() as scenario:
key: Final = scenario.key(models=["*"])
unlisted: Final = f"unlisted-{uuid.uuid4().hex}"
response: Final = _chat(candidate, unlisted, key)
assert response.status_code == 200, response.text
assert _observed_models(upstream) == [unlisted]
def test_a_key_without_models_inherits_the_users_exact_and_wildcard_grants(
candidate: Gateway, upstream: httpx.Client
) -> None:
with candidate.scenario() as scenario:
user_id: Final = scenario.user(models=["good-model", "anthropic/*"])
key: Final = scenario.key(user_id=user_id, models=[])
wildcard_model: Final = f"claude-{uuid.uuid4().hex}"
assert _chat(candidate, f"anthropic/{wildcard_model}", key).status_code == 200
assert _chat(candidate, "good-model", key).status_code == 200
assert _observed_models(upstream) == [wildcard_model, "good-model-upstream"]
upstream.delete("/__observations").raise_for_status()
denied: Final = tuple(
_chat(candidate, outside, key)
for outside in (f"groq/{wildcard_model}", f"bedrock/anthropic.{wildcard_model}")
)
assert [(response.status_code, response.json()["error"]["type"]) for response in denied] == [
(403, "user_model_access_denied")
] * 2, [response.text for response in denied]
assert _observed_models(upstream) == []
assert _chat(candidate, f"groq/{wildcard_model}", candidate.key).status_code == 200
assert _observed_models(upstream) == [wildcard_model]