mirror of
https://github.com/tiennm99/litellm.git
synced 2026-07-21 00:21:38 +00:00
* test(proxy/proxy_server): pin forwarding routes (PR2) (#28887) * test(proxy): pin proxy_server.py forwarding-route behavior PR2 of the proxy_server.py behavior-pinning project: fills the 12 forwarding-route test files added by the harness PR with happy + error pins for all 52 LLM-facing routes (models, chat/completions, completions, embeddings, moderations, audio, assistants, threads, utils, model-info, model-metrics, queue). Every happy-path test asserts the full response dict via normalize() so the gate enforces real shape pinning rather than status codes. * test(proxy): drop task-plumbing comments from PR2 test files * test(proxy): tighten PR2 error-path status-code pins Apply the same review feedback Greptile gave on PR1 (#28856) and PR3 (#28850) to PR2's forwarding-route tests: - Replace permissive `>= 400` / `in (X, Y)` status assertions with the exact 500/405 the handler actually returns, so a regression that silently shifts the code now fails the pin. - Add a body-presence check alongside each tightened status assertion to satisfy _pin_check.py's no-status-only rule. --------- Co-authored-by: Claude <noreply@anthropic.com> * test(proxy): pin proxy_server.py non-route surface behavior (PR1) (#28856) * test(proxy): pin proxy_server.py non-route surface behavior (PR1) Fills the 7 PR1 placeholder files under tests/test_litellm/proxy/proxy_server/ with behavior pins for the non-route surface of proxy_server.py: lifecycle/init/shutdown, ProxyConfig class methods, DB-overlay config scrubbers, spend counters, background-health helpers, OpenAPI customization, exception handlers, and streaming-generator helpers. 233 tests cover 101 pin-list symbols (1+ happy + 1+ error each). New-tests-only coverage on litellm/proxy/proxy_server.py: 32.80% line / 20.91% branch (PR1 gate: 25% line / 18% branch). Full directory runs in ~22s with -n 4. Plan: https://www.notion.so/Plan-Pin-proxy_server-py-behavior-2026-05-25-36c43b8acdab81ee845fd5365128a2fc * test(proxy): address Greptile review comments on test_lifecycle.py - test_initialize_signature_is_async_with_expected_params: hard-code expected_param_count so a signature change actually trips the gate (previously both sides of the comparison were len(sig.parameters)). - test_check_request_disconnection_invalid_when_connected_times_out: patch asyncio.sleep so the test no longer spins for ~1.2 s of real wall-clock; timeout lowered to 0.05 s. --------- Co-authored-by: Claude <noreply@anthropic.com> * test(proxy/proxy_server): pin control-plane routes (PR3) (#28850) * test(proxy/proxy_server): pin misc routes (PR3, partial) Adds happy + error tests for the misc control-plane routes: GET /, /routes, /adaptive_router/state, /get_logo_url, /get_image, /get_favicon. Also gitignores .pin_list.txt (used by the pin gate). * test(proxy/proxy_server): pin login/SSO routes (PR3, partial) Adds happy + error tests for the 5 login/SSO control-plane routes: GET /fallback/login, POST /login, POST /v2/login, POST /v3/login, POST /v3/login/exchange. Mocks authenticate_user and create_ui_token_object at their imported location. * test(proxy/proxy_server): pin onboarding routes (PR3, partial) Adds happy + error tests for the 2 onboarding control-plane routes: GET /onboarding/get_token, POST /onboarding/claim_token. Wires a MagicMock async context manager for prisma_client.db.tx() and signs the onboarding JWT with the patched master_key. * test(proxy/proxy_server): pin model_cost_map reload routes (PR3, partial) Adds happy + error tests for the 5 model-cost-map control-plane routes: POST /reload/model_cost_map, POST|DELETE|GET /schedule/model_cost_map_reload(/status), GET /model/cost_map/source. Attaches litellm_config to mock_prisma per-test (the table is not in the default _PRISMA_TABLES fixture). * test(proxy/proxy_server): pin anthropic_beta_headers reload routes (PR3, partial) Adds happy + error tests for the 4 anthropic-beta-headers control-plane routes: POST /reload/anthropic_beta_headers, POST|DELETE|GET /schedule/anthropic_beta_headers_reload(/status). Stubs db.litellm_config (not in default _PRISMA_TABLES) and monkeypatches reload_beta_headers_config so no network calls fire. * test(proxy/proxy_server): pin invitation routes (PR3, partial) Adds happy + error tests for the 4 invitation control-plane routes: POST /invitation/new, GET /invitation/info, POST /invitation/update, POST /invitation/delete. Patches _user_has_admin_privileges / _user_has_admin_view to avoid extensive get_user_object mocking. * test(proxy/proxy_server): pin config CRUD routes (PR3, partial) Adds happy + error tests for the 8 config-CRUD control-plane routes: POST /config/update, POST|GET /config/field/update|info, GET /config/list, POST /config/field/delete, POST /config/callback/delete, GET /get/config/callbacks, GET /config/yaml. Attaches litellm_config to mock_prisma per-test. * test(proxy/proxy_server): tighten pin assertions per review - test_routes_misc.py: `b"" in response.content` is trivially true; replace with `len(response.content) > 0` so an empty 405 body trips the gate. - test_routes_login_sso.py: `len(response.content) >= 0` is trivially true; tighten to `> 0`. - test_routes_anthropic_beta.py: replace brittle string-literal checks on the serialized JSON (`'"interval_hours": 12' in payload`) with `json.loads` + dict access so the assertion survives any serializer spacing. - test_routes_config.py: `assert status_code in (404, 500)` was too permissive; the handler re-raises HTTPException(404) verbatim, so pin 404 strictly. --------- Co-authored-by: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
194 lines
6.1 KiB
Python
194 lines
6.1 KiB
Python
"""Behavior pins for ``proxy_server.py`` audio routes.
|
|
|
|
Pins (PR2):
|
|
- POST /v1/audio/speech
|
|
- POST /audio/speech
|
|
- POST /v1/audio/transcriptions
|
|
- POST /audio/transcriptions
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import io
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
|
|
import pytest
|
|
|
|
from litellm.proxy import proxy_server
|
|
|
|
|
|
@pytest.fixture
|
|
def patched_speech(monkeypatch):
|
|
monkeypatch.setattr(proxy_server, "llm_router", MagicMock())
|
|
monkeypatch.setattr(
|
|
proxy_server,
|
|
"proxy_logging_obj",
|
|
MagicMock(
|
|
pre_call_hook=AsyncMock(side_effect=lambda **kw: kw["data"]),
|
|
post_call_failure_hook=AsyncMock(),
|
|
update_request_status=AsyncMock(),
|
|
),
|
|
)
|
|
|
|
async def _add_data(data, **kwargs):
|
|
return data
|
|
|
|
monkeypatch.setattr(proxy_server, "add_litellm_data_to_request", _add_data)
|
|
|
|
class _FakeBinaryResp:
|
|
async def aiter_bytes(self, chunk_size: int = 8192):
|
|
async def _gen():
|
|
yield b"\x00\x01\x02"
|
|
|
|
return _gen()
|
|
|
|
async def _llm_call():
|
|
return _FakeBinaryResp()
|
|
|
|
async def _fake_route_request(*args, **kwargs):
|
|
return _llm_call()
|
|
|
|
monkeypatch.setattr(proxy_server, "route_request", _fake_route_request)
|
|
yield
|
|
|
|
|
|
@pytest.fixture
|
|
def patched_speech_error(monkeypatch):
|
|
monkeypatch.setattr(proxy_server, "llm_router", MagicMock())
|
|
monkeypatch.setattr(
|
|
proxy_server,
|
|
"proxy_logging_obj",
|
|
MagicMock(
|
|
pre_call_hook=AsyncMock(side_effect=lambda **kw: kw["data"]),
|
|
post_call_failure_hook=AsyncMock(),
|
|
update_request_status=AsyncMock(),
|
|
),
|
|
)
|
|
|
|
async def _add_data(data, **kwargs):
|
|
return data
|
|
|
|
monkeypatch.setattr(proxy_server, "add_litellm_data_to_request", _add_data)
|
|
|
|
async def _raise(*args, **kwargs):
|
|
raise ValueError("speech boom")
|
|
|
|
monkeypatch.setattr(proxy_server, "route_request", _raise)
|
|
yield
|
|
|
|
|
|
@pytest.fixture
|
|
def patched_transcription(monkeypatch):
|
|
router = MagicMock()
|
|
router.model_names = ["whisper-1"]
|
|
monkeypatch.setattr(proxy_server, "llm_router", router)
|
|
monkeypatch.setattr(
|
|
proxy_server,
|
|
"proxy_logging_obj",
|
|
MagicMock(
|
|
pre_call_hook=AsyncMock(side_effect=lambda **kw: kw["data"]),
|
|
post_call_failure_hook=AsyncMock(),
|
|
post_call_response_headers_hook=AsyncMock(return_value={}),
|
|
update_request_status=AsyncMock(),
|
|
),
|
|
)
|
|
|
|
async def _add_data(data, **kwargs):
|
|
return data
|
|
|
|
monkeypatch.setattr(proxy_server, "add_litellm_data_to_request", _add_data)
|
|
monkeypatch.setattr(
|
|
proxy_server, "check_file_size_under_limit", lambda **kwargs: True
|
|
)
|
|
|
|
async def _form_data(request):
|
|
from starlette.datastructures import FormData, UploadFile
|
|
|
|
upload = UploadFile(
|
|
filename="audio.mp3",
|
|
file=io.BytesIO(b"\x00\x01\x02"),
|
|
)
|
|
return FormData([("file", upload), ("model", "whisper-1")])
|
|
|
|
monkeypatch.setattr(proxy_server, "get_form_data", _form_data)
|
|
|
|
async def _llm_call():
|
|
return {"text": "hello world"}
|
|
|
|
async def _fake_route_request(*args, **kwargs):
|
|
return _llm_call()
|
|
|
|
monkeypatch.setattr(proxy_server, "route_request", _fake_route_request)
|
|
yield
|
|
|
|
|
|
@pytest.fixture
|
|
def patched_transcription_error(monkeypatch, patched_transcription):
|
|
async def _raise(*args, **kwargs):
|
|
raise ValueError("transcription boom")
|
|
|
|
monkeypatch.setattr(proxy_server, "route_request", _raise)
|
|
yield
|
|
|
|
|
|
@pytest.mark.parametrize("path", ["/v1/audio/speech", "/audio/speech"])
|
|
def test_audio_speech_happy_path(client, auth_as, patched_speech, path):
|
|
"""Pins ``POST /v1/audio/speech`` and ``POST /audio/speech`` (happy)."""
|
|
payload = {"model": "tts-1", "input": "Hi", "voice": "alloy"}
|
|
with auth_as():
|
|
response = client.post(path, json=payload)
|
|
assert response.status_code == 200
|
|
response_summary = {
|
|
"status_code": response.status_code,
|
|
"content_type": response.headers.get("content-type", ""),
|
|
"body_bytes": response.content,
|
|
}
|
|
assert response_summary == {
|
|
"status_code": 200,
|
|
"content_type": "audio/mpeg",
|
|
"body_bytes": b"\x00\x01\x02",
|
|
}
|
|
|
|
|
|
@pytest.mark.parametrize("path", ["/v1/audio/speech", "/audio/speech"])
|
|
def test_audio_speech_error(client, auth_as, patched_speech_error, path):
|
|
"""Pins ``POST /v1/audio/speech`` and ``POST /audio/speech`` (error)."""
|
|
payload = {"model": "tts-1", "input": "Hi", "voice": "alloy"}
|
|
with auth_as():
|
|
response = client.post(path, json=payload)
|
|
assert response.status_code == 500
|
|
assert len(response.content) > 0
|
|
|
|
|
|
@pytest.mark.parametrize("path", ["/v1/audio/transcriptions", "/audio/transcriptions"])
|
|
def test_audio_transcription_happy_path(client, auth_as, patched_transcription, path):
|
|
"""Pins ``POST /v1/audio/transcriptions`` / ``POST /audio/transcriptions`` (happy)."""
|
|
files = {"file": ("audio.mp3", b"\x00\x01\x02", "audio/mpeg")}
|
|
data = {"model": "whisper-1"}
|
|
with auth_as():
|
|
response = client.post(path, files=files, data=data)
|
|
assert response.status_code == 200
|
|
body = response.json()
|
|
assert body == {"text": "hello world"}
|
|
response_summary = {
|
|
"status_code": response.status_code,
|
|
"text_field": body["text"],
|
|
"media_type_hint": response.headers.get("content-type", "").split(";")[0],
|
|
}
|
|
assert response_summary == {
|
|
"status_code": 200,
|
|
"text_field": "hello world",
|
|
"media_type_hint": "application/json",
|
|
}
|
|
|
|
|
|
@pytest.mark.parametrize("path", ["/v1/audio/transcriptions", "/audio/transcriptions"])
|
|
def test_audio_transcription_error(client, auth_as, patched_transcription_error, path):
|
|
"""Pins ``POST /v1/audio/transcriptions`` / ``POST /audio/transcriptions`` (error)."""
|
|
files = {"file": ("audio.mp3", b"\x00\x01\x02", "audio/mpeg")}
|
|
data = {"model": "whisper-1"}
|
|
with auth_as():
|
|
response = client.post(path, files=files, data=data)
|
|
assert response.status_code == 500
|
|
assert len(response.content) > 0
|