mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-07 02:59:05 +00:00
test(e2e): rename suites, drop vendor_contract, fix greptile gaps
Move shared status helpers into e2e_http, rename chat auth headers and chat security suites, remove vendor_contract and dev_config files_settings, and tighten transcription validation plus vector-store search assertions
This commit is contained in:
parent
c8caf61ee2
commit
3c7d66989e
19 changed files with 126 additions and 143 deletions
|
|
@ -204,11 +204,6 @@ general_settings:
|
|||
# configured for the batched model. Defaults to false.
|
||||
# track_unmanaged_batch_cost: true
|
||||
|
||||
# for /v1/files (vector store upload, batches, etc.)
|
||||
files_settings:
|
||||
- custom_llm_provider: openai
|
||||
api_key: os.environ/OPENAI_API_KEY
|
||||
|
||||
sandbox_tools:
|
||||
- sandbox_tool_name: e2b_sandbox
|
||||
litellm_params:
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
"""Vendor §11.1 auth security on LLM routes (LIT-4778).
|
||||
"""Chat Authorization header matrix on LLM routes (LIT-4778).
|
||||
|
||||
Virtual-key chat must reject missing and malformed Authorization headers before
|
||||
any provider call. These cases sit next to the existing valid/invalid key check
|
||||
and pin the full bearer-token failure matrix from the vendor brief.
|
||||
and pin the bearer-token failure matrix.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
@ -27,7 +27,7 @@ class RawAuthorizationHeaders(BaseModel):
|
|||
|
||||
|
||||
def _register_model(proxy: ProxyClient, resources: ResourceManager) -> str:
|
||||
model = f"e2e-auth-sec-{unique_marker()}"
|
||||
model = f"e2e-auth-headers-{unique_marker()}"
|
||||
model_id = proxy.create_model(
|
||||
model,
|
||||
LiteLLMParamsBody(model=OPENAI_BACKEND, api_key="os.environ/OPENAI_API_KEY"),
|
||||
|
|
@ -56,7 +56,7 @@ def _assert_auth_denied(result: StreamingResponse, context: str) -> None:
|
|||
)
|
||||
|
||||
|
||||
class TestChatAuthSecurity:
|
||||
class TestChatAuthHeaders:
|
||||
@pytest.mark.covers("other.auth.llm_chat.missing_header_denied")
|
||||
def test_missing_authorization_header_is_denied(
|
||||
self, proxy: ProxyClient, resources: ResourceManager
|
||||
|
|
@ -220,6 +220,70 @@ def require_successful_call(result: StreamingResponse) -> None:
|
|||
)
|
||||
|
||||
|
||||
def is_client_error(status: int) -> bool:
|
||||
return 400 <= status < 500
|
||||
|
||||
|
||||
def is_auth_denied(status: int) -> bool:
|
||||
return status in (401, 403)
|
||||
|
||||
|
||||
def assert_not_server_error(result: StreamingResponse, context: str) -> None:
|
||||
assert result.status_code not in (500, 502, 503), (
|
||||
f"{context}: proxy must not 5xx, got {result.status_code}: {result.body[:300]}"
|
||||
)
|
||||
|
||||
|
||||
def assert_client_error(result: StreamingResponse, context: str) -> None:
|
||||
assert is_client_error(result.status_code), (
|
||||
f"{context}: expected 4xx, got {result.status_code}: {result.body[:300]}"
|
||||
)
|
||||
|
||||
|
||||
def assert_error_or_server_known(result: StreamingResponse, context: str) -> None:
|
||||
"""Missing required fields may be 4xx or 5xx per known acceptable proxy behavior."""
|
||||
assert result.status_code in range(400, 600), (
|
||||
f"{context}: expected error status, got {result.status_code}: {result.body[:300]}"
|
||||
)
|
||||
assert result.status_code != 200
|
||||
|
||||
|
||||
def assert_auth_denied(result: StreamingResponse, context: str) -> None:
|
||||
assert is_auth_denied(result.status_code), (
|
||||
f"{context}: expected 401/403, got {result.status_code}: {result.body[:300]}"
|
||||
)
|
||||
|
||||
|
||||
def is_provider_account_denied(result: StreamingResponse) -> bool:
|
||||
"""True when the gateway reached the provider and the account/model is disabled."""
|
||||
if result.status_code not in (400, 403, 404):
|
||||
return False
|
||||
body = result.body.lower()
|
||||
needles = (
|
||||
"operation not allowed",
|
||||
"end of its life",
|
||||
"accessdenied",
|
||||
"not authorized",
|
||||
"model use case details have not been submitted",
|
||||
"you don't have access",
|
||||
"do not have access",
|
||||
)
|
||||
return any(n in body for n in needles)
|
||||
|
||||
|
||||
def require_success_or_provider_denied(result: StreamingResponse, context: str) -> bool:
|
||||
"""Return True on success; return False when the provider denied the account.
|
||||
|
||||
Raises on unexpected failures so real product regressions still fail hard.
|
||||
"""
|
||||
if result.ok and not result.stream_error:
|
||||
return True
|
||||
if is_provider_account_denied(result):
|
||||
return False
|
||||
require_successful_call(result)
|
||||
return True
|
||||
|
||||
|
||||
def _headers(headers: BaseModel) -> dict[str, str]:
|
||||
dumped: dict[str, object] = headers.model_dump(by_alias=True, exclude_none=True)
|
||||
return {key: str(value) for key, value in dumped.items()}
|
||||
|
|
|
|||
|
|
@ -12,11 +12,10 @@ import pytest
|
|||
from pydantic import BaseModel
|
||||
|
||||
from e2e_config import unique_marker
|
||||
from e2e_http import require_successful_call
|
||||
from e2e_http import require_successful_call, assert_error_or_server_known
|
||||
from endpoints_client import EndpointsClient
|
||||
from lifecycle import ResourceManager
|
||||
from models import LiteLLMParamsBody
|
||||
from vendor_contract import assert_error_or_server_known
|
||||
|
||||
pytestmark = pytest.mark.e2e
|
||||
|
||||
|
|
|
|||
|
|
@ -79,10 +79,12 @@ class TestAudioTranscriptions:
|
|||
match result:
|
||||
case Success():
|
||||
pytest.fail("empty audio file must not succeed as a transcript")
|
||||
case UnknownApiError(status_code=status):
|
||||
assert status in range(400, 600), f"unexpected {status}"
|
||||
case _:
|
||||
case UnknownApiError(status_code=status) if 400 <= status < 500:
|
||||
return
|
||||
case UnknownApiError(status_code=status):
|
||||
pytest.fail(f"empty audio expected 4xx, got {status}: {result}")
|
||||
case _:
|
||||
pytest.fail(f"empty audio unexpected result: {result}")
|
||||
|
||||
@pytest.mark.covers("llm.audio_transcriptions.openai.input_validation.nonstream.works")
|
||||
def test_missing_model_returns_error(
|
||||
|
|
@ -101,7 +103,9 @@ class TestAudioTranscriptions:
|
|||
match result:
|
||||
case Success():
|
||||
pytest.fail("transcription without model must not succeed")
|
||||
case UnknownApiError(status_code=status):
|
||||
assert status in range(400, 600), f"unexpected {status}"
|
||||
case _:
|
||||
case UnknownApiError(status_code=status) if 400 <= status < 500:
|
||||
return
|
||||
case UnknownApiError(status_code=status):
|
||||
pytest.fail(f"missing model expected 4xx, got {status}: {result}")
|
||||
case _:
|
||||
pytest.fail(f"missing model unexpected result: {result}")
|
||||
|
|
|
|||
|
|
@ -7,19 +7,18 @@ missing messages and invalid model handling without crashing the proxy.
|
|||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
from pydantic import BaseModel
|
||||
|
||||
from e2e_config import unique_marker
|
||||
import requests
|
||||
|
||||
from lifecycle import ResourceManager
|
||||
from models import LiteLLMParamsBody
|
||||
from proxy_client import ProxyClient
|
||||
from vendor_contract import (
|
||||
from e2e_http import (
|
||||
assert_client_error,
|
||||
assert_error_or_server_known,
|
||||
require_success_or_provider_denied,
|
||||
)
|
||||
from lifecycle import ResourceManager
|
||||
from models import LiteLLMParamsBody
|
||||
from proxy_client import ProxyClient
|
||||
|
||||
pytestmark = pytest.mark.e2e
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
"""Vendor API strategy coverage for /chat/completions (LIT-4778).
|
||||
"""Chat completions security and input-sanitization e2e (LIT-4778).
|
||||
|
||||
Positive multi-turn history, input validation, boundary handling, contract shape,
|
||||
and input sanitization against a live proxy and a real OpenAI-compatible model.
|
||||
Multi-turn history, input validation, boundary handling, response shape, and
|
||||
SQL/XSS payload sanitization against a live proxy and a real OpenAI-compatible model.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
@ -51,7 +51,7 @@ class ChatErrorEnvelope(BaseModel):
|
|||
|
||||
|
||||
def _register_chat_model(proxy: ProxyClient, resources: ResourceManager) -> tuple[str, str]:
|
||||
model = f"e2e-vendor-chat-{unique_marker()}"
|
||||
model = f"e2e-chat-sec-{unique_marker()}"
|
||||
model_id = proxy.create_model(
|
||||
model,
|
||||
LiteLLMParamsBody(model=OPENAI_BACKEND, api_key="os.environ/OPENAI_API_KEY"),
|
||||
|
|
@ -80,7 +80,7 @@ def _assert_not_server_error(result: StreamingResponse, context: str) -> None:
|
|||
)
|
||||
|
||||
|
||||
class TestChatCompletionsVendorContract:
|
||||
class TestChatCompletionsSecVulnerability:
|
||||
@pytest.mark.covers("llm.chat_completions.openai.multi_turn.nonstream.works")
|
||||
def test_multi_turn_history_is_honored(
|
||||
self, proxy: ProxyClient, resources: ResourceManager
|
||||
|
|
@ -12,15 +12,15 @@ import pytest
|
|||
from pydantic import BaseModel
|
||||
|
||||
from e2e_config import unique_marker
|
||||
from e2e_http import require_successful_call
|
||||
from endpoints_client import EmbeddingsResult, EndpointsClient
|
||||
from lifecycle import ResourceManager
|
||||
from models import LiteLLMParamsBody
|
||||
from vendor_contract import (
|
||||
from e2e_http import (
|
||||
assert_client_error,
|
||||
assert_error_or_server_known,
|
||||
require_success_or_provider_denied,
|
||||
require_successful_call,
|
||||
)
|
||||
from endpoints_client import EmbeddingsResult, EndpointsClient
|
||||
from lifecycle import ResourceManager
|
||||
from models import LiteLLMParamsBody
|
||||
|
||||
pytestmark = pytest.mark.e2e
|
||||
|
||||
|
|
|
|||
|
|
@ -10,11 +10,10 @@ import pytest
|
|||
from pydantic import BaseModel
|
||||
|
||||
from e2e_config import unique_marker
|
||||
from e2e_http import NoBody, Success, UnknownApiError
|
||||
from e2e_http import NoBody, Success, UnknownApiError, assert_error_or_server_known
|
||||
from lifecycle import ResourceManager
|
||||
from models import LiteLLMParamsBody
|
||||
from proxy_client import ProxyClient
|
||||
from vendor_contract import assert_error_or_server_known
|
||||
|
||||
pytestmark = pytest.mark.e2e
|
||||
|
||||
|
|
|
|||
|
|
@ -11,15 +11,15 @@ import pytest
|
|||
from pydantic import BaseModel
|
||||
|
||||
from e2e_config import unique_marker
|
||||
from e2e_http import require_successful_call
|
||||
from endpoints_client import EndpointsClient, ImagesResult
|
||||
from lifecycle import ResourceManager
|
||||
from models import LiteLLMParamsBody
|
||||
from vendor_contract import (
|
||||
from e2e_http import (
|
||||
assert_client_error,
|
||||
assert_error_or_server_known,
|
||||
require_success_or_provider_denied,
|
||||
require_successful_call,
|
||||
)
|
||||
from endpoints_client import EndpointsClient, ImagesResult
|
||||
from lifecycle import ResourceManager
|
||||
from models import LiteLLMParamsBody
|
||||
|
||||
pytestmark = pytest.mark.e2e
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import pytest
|
|||
from pydantic import BaseModel
|
||||
|
||||
from e2e_config import unique_marker
|
||||
from e2e_http import require_successful_call, unwrap
|
||||
from e2e_http import require_successful_call, unwrap, assert_error_or_server_known
|
||||
from endpoints_client import EndpointsClient, MessagesResult
|
||||
from lifecycle import ResourceManager
|
||||
from models import (
|
||||
|
|
@ -24,7 +24,6 @@ from models import (
|
|||
SpendLogRow,
|
||||
ToolInputSchema,
|
||||
)
|
||||
from vendor_contract import assert_error_or_server_known
|
||||
|
||||
pytestmark = pytest.mark.e2e
|
||||
|
||||
|
|
|
|||
|
|
@ -11,11 +11,10 @@ from dataclasses import dataclass
|
|||
import pytest
|
||||
|
||||
from e2e_config import unique_marker
|
||||
from e2e_http import StreamingResponse, UnknownApiError, unwrap
|
||||
from e2e_http import StreamingResponse, UnknownApiError, unwrap, is_provider_account_denied
|
||||
from lifecycle import ResourceManager
|
||||
from models import ChatBody, ChatMessage, LiteLLMParamsBody
|
||||
from proxy_client import ProxyClient
|
||||
from vendor_contract import is_provider_account_denied
|
||||
|
||||
pytestmark = pytest.mark.e2e
|
||||
|
||||
|
|
|
|||
|
|
@ -11,11 +11,10 @@ import pytest
|
|||
from pydantic import BaseModel
|
||||
|
||||
from e2e_config import unique_marker
|
||||
from e2e_http import unwrap
|
||||
from e2e_http import unwrap, assert_error_or_server_known
|
||||
from endpoints_client import EndpointsClient
|
||||
from lifecycle import ResourceManager
|
||||
from models import LiteLLMParamsBody
|
||||
from vendor_contract import assert_error_or_server_known
|
||||
|
||||
pytestmark = pytest.mark.e2e
|
||||
|
||||
|
|
|
|||
|
|
@ -23,11 +23,10 @@ import pytest
|
|||
from pydantic import BaseModel
|
||||
|
||||
from e2e_config import unique_marker
|
||||
from e2e_http import unwrap
|
||||
from e2e_http import unwrap, assert_error_or_server_known
|
||||
from endpoints_client import EndpointsClient
|
||||
from lifecycle import ResourceManager
|
||||
from models import LiteLLMParamsBody, OcrBody, OcrDocument, OcrResponse
|
||||
from vendor_contract import assert_error_or_server_known
|
||||
|
||||
pytestmark = pytest.mark.e2e
|
||||
|
||||
|
|
|
|||
|
|
@ -10,11 +10,10 @@ import pytest
|
|||
from pydantic import BaseModel
|
||||
|
||||
from e2e_config import unique_marker
|
||||
from e2e_http import NoBody, unwrap
|
||||
from e2e_http import NoBody, unwrap, assert_auth_denied
|
||||
from lifecycle import ResourceManager
|
||||
from models import LiteLLMParamsBody
|
||||
from proxy_client import ProxyClient
|
||||
from vendor_contract import assert_auth_denied
|
||||
|
||||
pytestmark = pytest.mark.e2e
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,14 @@ import pytest
|
|||
from pydantic import BaseModel, ValidationError
|
||||
|
||||
from e2e_config import unique_marker
|
||||
from e2e_http import require_successful_call
|
||||
from e2e_http import (
|
||||
assert_client_error,
|
||||
assert_error_or_server_known,
|
||||
assert_not_server_error,
|
||||
is_client_error,
|
||||
require_success_or_provider_denied,
|
||||
require_successful_call,
|
||||
)
|
||||
from endpoints_client import (
|
||||
EndpointsClient,
|
||||
FunctionParameterProperty,
|
||||
|
|
@ -26,13 +33,6 @@ from endpoints_client import (
|
|||
)
|
||||
from lifecycle import ResourceManager
|
||||
from models import LiteLLMParamsBody
|
||||
from vendor_contract import (
|
||||
assert_client_error,
|
||||
assert_error_or_server_known,
|
||||
assert_not_server_error,
|
||||
is_client_error,
|
||||
require_success_or_provider_denied,
|
||||
)
|
||||
|
||||
pytestmark = pytest.mark.e2e
|
||||
|
||||
|
|
|
|||
|
|
@ -12,10 +12,9 @@ import pytest
|
|||
from pydantic import BaseModel
|
||||
|
||||
from e2e_config import unique_marker
|
||||
from e2e_http import NoBody, unwrap
|
||||
from e2e_http import NoBody, unwrap, assert_client_error, assert_error_or_server_known
|
||||
from lifecycle import ResourceManager
|
||||
from proxy_client import ProxyClient
|
||||
from vendor_contract import assert_client_error, assert_error_or_server_known
|
||||
|
||||
pytestmark = pytest.mark.e2e
|
||||
|
||||
|
|
|
|||
|
|
@ -13,11 +13,10 @@ import pytest
|
|||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
from e2e_config import POLL_INTERVAL, POLL_TIMEOUT, unique_marker
|
||||
from e2e_http import FileUploadForm, NoBody, unwrap
|
||||
from e2e_http import FileUploadForm, NoBody, unwrap, assert_error_or_server_known
|
||||
from lifecycle import ResourceManager
|
||||
from models import LiteLLMParamsBody
|
||||
from proxy_client import ProxyClient
|
||||
from vendor_contract import assert_error_or_server_known
|
||||
|
||||
pytestmark = pytest.mark.e2e
|
||||
|
||||
|
|
@ -221,7 +220,7 @@ class TestVectorStores:
|
|||
proxy.transport.upload(
|
||||
"/v1/files",
|
||||
headers=proxy.transport.bearer(key),
|
||||
form=FileUploadForm(purpose="assistants"),
|
||||
form=FileUploadForm(purpose="assistants", custom_llm_provider="openai"),
|
||||
filename="vs_doc.txt",
|
||||
content=content,
|
||||
file_content_type="text/plain",
|
||||
|
|
@ -275,7 +274,16 @@ class TestVectorStores:
|
|||
response_type=VectorStoreSearchResponse,
|
||||
)
|
||||
)
|
||||
assert search.data is not None, f"search returned no data field: {search}"
|
||||
assert search.data, f"search returned no hits for marker {marker!r}: {search}"
|
||||
hit_blob = " ".join(
|
||||
" ".join(part.get("text", "") for part in (hit.content or []))
|
||||
+ " "
|
||||
+ (hit.filename or "")
|
||||
for hit in search.data
|
||||
)
|
||||
assert marker in hit_blob or any(
|
||||
(hit.file_id or "") == uploaded.id for hit in search.data
|
||||
), f"search hits must reference marker or uploaded file; marker={marker!r} hits={search.data}"
|
||||
|
||||
deleted_file = unwrap(
|
||||
proxy.transport.delete(
|
||||
|
|
|
|||
|
|
@ -1,79 +0,0 @@
|
|||
"""Shared helpers for vendor API contract e2e tests (LIT-4778).
|
||||
|
||||
Status-centric assertions used across endpoint negatives, sanitization, and
|
||||
auth matrix cases so each test file stays thin.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from e2e_http import StreamingResponse
|
||||
|
||||
|
||||
def is_client_error(status: int) -> bool:
|
||||
return 400 <= status < 500
|
||||
|
||||
|
||||
def is_auth_denied(status: int) -> bool:
|
||||
return status in (401, 403)
|
||||
|
||||
|
||||
def assert_not_server_error(result: StreamingResponse, context: str) -> None:
|
||||
assert result.status_code not in (500, 502, 503), (
|
||||
f"{context}: proxy must not 5xx, got {result.status_code}: {result.body[:300]}"
|
||||
)
|
||||
|
||||
|
||||
def assert_client_error(result: StreamingResponse, context: str) -> None:
|
||||
assert is_client_error(result.status_code), (
|
||||
f"{context}: expected 4xx, got {result.status_code}: {result.body[:300]}"
|
||||
)
|
||||
|
||||
|
||||
def assert_error_or_server_known(result: StreamingResponse, context: str) -> None:
|
||||
"""Missing required fields may be 4xx or 5xx per known acceptable proxy behavior."""
|
||||
assert result.status_code in range(400, 600), (
|
||||
f"{context}: expected error status, got {result.status_code}: {result.body[:300]}"
|
||||
)
|
||||
assert result.status_code != 200
|
||||
|
||||
|
||||
def assert_auth_denied(result: StreamingResponse, context: str) -> None:
|
||||
assert is_auth_denied(result.status_code), (
|
||||
f"{context}: expected 401/403, got {result.status_code}: {result.body[:300]}"
|
||||
)
|
||||
|
||||
|
||||
def is_provider_account_denied(result: StreamingResponse) -> bool:
|
||||
"""True when the gateway reached the provider and the account/model is disabled.
|
||||
|
||||
Common on local AWS accounts that list Bedrock models but cannot InvokeModel
|
||||
("Operation not allowed") or when a model version is EOL.
|
||||
"""
|
||||
if result.status_code not in (400, 403, 404):
|
||||
return False
|
||||
body = result.body.lower()
|
||||
needles = (
|
||||
"operation not allowed",
|
||||
"end of its life",
|
||||
"accessdenied",
|
||||
"not authorized",
|
||||
"model use case details have not been submitted",
|
||||
"you don't have access",
|
||||
"do not have access",
|
||||
)
|
||||
return any(n in body for n in needles)
|
||||
|
||||
|
||||
def require_success_or_provider_denied(result: StreamingResponse, context: str) -> bool:
|
||||
"""Return True on success; return False when the provider denied the account.
|
||||
|
||||
Raises on unexpected failures so real product regressions still fail hard.
|
||||
"""
|
||||
if result.ok and not result.stream_error:
|
||||
return True
|
||||
if is_provider_account_denied(result):
|
||||
return False
|
||||
from e2e_http import require_successful_call
|
||||
|
||||
require_successful_call(result)
|
||||
return True
|
||||
Loading…
Add table
Reference in a new issue