mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-14 23:21:35 +00:00
* test(e2e): cover passthrough headers, batch assume-role, gemini, vllm, bedrock guardrails, batch rate-limit mapping Add parent-package e2e suites for the six feature gaps: pass-through header forwarding via /config/pass_through_endpoint, Bedrock batch STS assume-role, Gemini chat + files, hosted_vllm batch/files, Bedrock guardrail pre_call blocks (plus restored content-filter team opt-out), and OpenAI batch RPM 429 body mapping. Registry cells and LiteLLMParamsBody/TeamMetadata fields updated so markers collect cleanly. * test(e2e): cover LIT-4587 gaps for redis, responses, tpm cache, apply_guardrail, langfuse Adds customer-shaped live e2e for apply_guardrail, responses store+metadata TTL, TPM excluding cached tokens, redis-backed RPM, redis circuit-breaker path, Langfuse spend, Cohere chat, virtual-key auth, file content download, hosted_vllm chat, and Nova Sonic realtime. Registry cells updated for the new markers. * test(e2e): drive LIT-4587 gap suites on Anthropic to avoid Gemini quota flakes Redis RPM, circuit-breaker path, virtual-key auth, responses metadata, and Langfuse driver models now use Anthropic haiku so local runs stay green when Gemini daily quota is exhausted. * test(e2e): drop Langfuse spend suite; feature is being deprecated Remove test_langfuse_e2e.py, logging.langfuse registry cells, and the langfuse-only conftest driver/credentials fixtures. * test(e2e): fold provider/batch feature tests into their endpoint suites Keep the e2e layout endpoint- and suite-scoped instead of one file per provider or feature Move the virtual-key auth case into access_control/test_access_control_e2e.py as TestVirtualKeyAuth (replacing an incomplete stub) and drop the standalone test_virtual_key_auth_e2e.py Fold the five per-file batch suites (file content, RPM 429 mapping, Bedrock assume-role, Gemini files, hosted_vllm batch) into batches/test_batches_e2e.py. The hosted_vllm batch case is skipped for now since it needs a live vLLM server (HOSTED_VLLM_API_BASE) the e2e environment does not provision; it and the gemini-files and RPM-mapping cases reference LIT-3382 / LIT-3266 where relevant Merge the cohere, gemini and hosted_vllm chat cases into llm_translation/test_chat_completions_regression_e2e.py so /chat/completions coverage lives in one endpoint file, and repoint the coverage_registry source fields to the new homes Move the shared CacheControl / TextBlock / RichMessage request blocks into the root models.py (re-exported from endpoints_client) so quota_management can use them without a cross-suite import, which also clears the basedpyright errors in test_tpm_excludes_cached_tokens_e2e.py; type the httpbin echo body in test_passthrough_headers_e2e.py with a pydantic model to drop the Any-typed json.loads path * test(e2e): address review feedback and re-home virtual-key coverage Replace the tautological Bedrock assume-role batch id assertion (`startswith(...) or batch.id`, always true) with a managed-id shape check, since the unified target_model_names path re-encodes the id rather than returning a raw ARN Raise the batch RPM-mapping test's rpm_limit above one so the file upload can no longer consume the key's sole request unit before batch create runs; the batch create then clears the generic per-request limiter and the batch limiter is what returns the "Batch rate limit exceeded" body the assertions check Set exercised_on to [] on the pass-through header test; it drives a pass-through endpoint, not /chat/completions Move the virtual-key valid_allows / invalid_denied cells from other.yaml to mgmt.yaml as mgmt.virtual_key.* so TestVirtualKeyAuth rolls up under Management, and point its covers marker at the new ids
144 lines
5.1 KiB
Python
144 lines
5.1 KiB
Python
"""Live e2e: the gateway's authorization and error-shape contract.
|
|
|
|
A virtual key may only call models in its allow-list and route groups in its
|
|
allowed_routes; both denials are a 403 raised before any provider is touched. A
|
|
syntactically valid request naming a non-existent model is a 400 with a JSON body,
|
|
never forwarded and never a 5xx. Migrated from
|
|
litellm-regression-tests/tests/test_access_control.py: the source asserted 401 for
|
|
the disallowed-model case against an older proxy, but the current contract
|
|
(auth_checks.py) is a 403 key_model_access_denied, and the unknown-route check is
|
|
replaced by a stronger route-permission check (an llm-only key rejected from a
|
|
management route).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
|
|
import pytest
|
|
|
|
from access_control_client import (
|
|
AccessControlClient,
|
|
MODEL_ACCESS_DENIED_MARKER,
|
|
ROUTE_NOT_ALLOWED_MARKER,
|
|
)
|
|
from e2e_config import unique_marker
|
|
from e2e_http import Success, UnauthorizedError, UnknownApiError, unwrap
|
|
from lifecycle import ResourceManager
|
|
from models import ChatBody, ChatMessage, LiteLLMParamsBody
|
|
from proxy_client import ProxyClient
|
|
|
|
pytestmark = pytest.mark.e2e
|
|
|
|
ALLOWED_MODEL = "gemini-2.5-flash"
|
|
DISALLOWED_MODEL = "gpt-5.5"
|
|
VIRTUAL_KEY_BACKEND = "anthropic/claude-haiku-4-5-20251001"
|
|
|
|
|
|
def _is_json(body: str) -> bool:
|
|
try:
|
|
json.loads(body)
|
|
return True
|
|
except ValueError:
|
|
return False
|
|
|
|
|
|
|
|
class TestAccessControl:
|
|
def test_disallowed_model_is_denied_403(
|
|
self, client: AccessControlClient, resources: ResourceManager
|
|
) -> None:
|
|
key = resources.key(models=[ALLOWED_MODEL])
|
|
result = client.chat_status(
|
|
key, DISALLOWED_MODEL, f"capital of France? {unique_marker()}"
|
|
)
|
|
assert result.status_code == 403, (
|
|
f"key limited to {ALLOWED_MODEL!r} calling {DISALLOWED_MODEL!r} must be "
|
|
f"denied 403, got {result.status_code}: {result.body[:300]}"
|
|
)
|
|
assert MODEL_ACCESS_DENIED_MARKER in result.body, (
|
|
f"403 body must be a model-access denial, got: {result.body[:300]}"
|
|
)
|
|
|
|
def test_llm_only_key_forbidden_from_management_route_403(
|
|
self, client: AccessControlClient, resources: ResourceManager
|
|
) -> None:
|
|
key = client.llm_only_key()
|
|
resources.defer(lambda: client.delete_key(key))
|
|
result = client.create_model_status(key, f"e2e-forbidden-{unique_marker()}")
|
|
assert result.status_code == 403, (
|
|
f"llm-only key calling a management route must be denied 403, got "
|
|
f"{result.status_code}: {result.body[:300]}"
|
|
)
|
|
assert ROUTE_NOT_ALLOWED_MARKER in result.body, (
|
|
f"403 body must be a route-permission denial, got: {result.body[:300]}"
|
|
)
|
|
|
|
def test_unknown_model_returns_400(
|
|
self, client: AccessControlClient, resources: ResourceManager
|
|
) -> None:
|
|
key = resources.key()
|
|
result = client.chat_status(
|
|
key, f"nonexistent-model-{unique_marker()}", "hi this is a test"
|
|
)
|
|
assert result.status_code == 400, (
|
|
f"unknown model must be rejected 400 before forwarding, got "
|
|
f"{result.status_code}: {result.body[:300]}"
|
|
)
|
|
assert _is_json(result.body), f"400 body must be valid JSON: {result.body[:300]}"
|
|
|
|
|
|
class TestVirtualKeyAuth:
|
|
"""Virtual-key auth the way OpenAI-compatible clients send it: a real key
|
|
must reach chat, a forged bearer must be rejected before the provider."""
|
|
|
|
@pytest.mark.covers(
|
|
"mgmt.virtual_key.valid_allows",
|
|
"mgmt.virtual_key.invalid_denied",
|
|
exercised_on=[],
|
|
)
|
|
def test_valid_key_allows_and_invalid_key_denied(
|
|
self, proxy: ProxyClient, resources: ResourceManager
|
|
) -> None:
|
|
model = f"e2e-auth-chat-{unique_marker()}"
|
|
model_id = proxy.create_model(
|
|
model,
|
|
LiteLLMParamsBody(model=VIRTUAL_KEY_BACKEND, api_key="os.environ/ANTHROPIC_API_KEY"),
|
|
)
|
|
resources.defer(lambda: proxy.delete_model(model_id))
|
|
key = resources.key()
|
|
|
|
ok = unwrap(
|
|
proxy.chat(
|
|
key,
|
|
ChatBody(
|
|
model=model,
|
|
messages=[
|
|
ChatMessage(
|
|
role="user",
|
|
content=f"Reply with one word. {unique_marker()}",
|
|
)
|
|
],
|
|
max_tokens=16,
|
|
),
|
|
)
|
|
)
|
|
assert ok.choices, f"valid key must complete chat: {ok}"
|
|
|
|
bad = proxy.chat(
|
|
"sk-e2e-forged-not-a-real-key",
|
|
ChatBody(
|
|
model=model,
|
|
messages=[ChatMessage(role="user", content="should not run")],
|
|
max_tokens=8,
|
|
),
|
|
)
|
|
match bad:
|
|
case UnauthorizedError():
|
|
return
|
|
case UnknownApiError(status_code=status) if status in (401, 403):
|
|
return
|
|
case Success():
|
|
pytest.fail("forged bearer must not reach a successful completion")
|
|
case _:
|
|
pytest.fail(f"forged bearer must be auth-denied, got {bad}")
|