fix(proxy): label a 408 invalid_request_error again and pin the in-route status on the files and realtime tails

This commit is contained in:
mateo-berri 2026-09-08 13:11:24 -07:00
parent 8b89c909a9
commit 720f2ca775
5 changed files with 95 additions and 2 deletions

View file

@ -12,7 +12,6 @@ _OPENAI_ERROR_TYPE_BY_STATUS: Final[Mapping[int, str]] = MappingProxyType(
{
status.HTTP_401_UNAUTHORIZED: "authentication_error",
status.HTTP_403_FORBIDDEN: "permission_error",
status.HTTP_408_REQUEST_TIMEOUT: "timeout_error",
status.HTTP_429_TOO_MANY_REQUESTS: "rate_limit_error",
}
)

View file

@ -18,7 +18,7 @@ from litellm.proxy.common_utils.openai_error_payload import (
(401, "authentication_error"),
(403, "permission_error"),
(404, "invalid_request_error"),
(408, "timeout_error"),
(408, "invalid_request_error"),
(422, "invalid_request_error"),
(429, "rate_limit_error"),
(499, "invalid_request_error"),

View file

@ -4773,3 +4773,49 @@ def test_get_file_content_reports_a_missing_managed_file_as_a_404(
assert response.status_code == 404, response.text
assert response.json() == _missing_managed_file_error(file_id)
def _setup_managed_file_stored_in_an_unknown_storage_backend(
mocker: MockerFixture, monkeypatch: pytest.MonkeyPatch, llm_router: Router
) -> None:
"""Wire the content route to a managed file whose row names a storage backend the
factory does not know, which is the one in-route ProxyException on these routes."""
from types import SimpleNamespace
import litellm.proxy.proxy_server as ps
from litellm.llms.base_llm.files.transformation import BaseFileEndpoints
from litellm.proxy._types import LitellmUserRoles
proxy_logging_obj = setup_proxy_logging_object(monkeypatch, llm_router)
managed_files = mocker.MagicMock(spec=BaseFileEndpoints)
managed_files.prisma_client = mocker.MagicMock()
proxy_logging_obj.proxy_hook_mapping["managed_files"] = managed_files
repository = mocker.MagicMock()
repository.table.find_first = mocker.AsyncMock(
return_value=SimpleNamespace(storage_backend="ftp", storage_url="ftp://bucket/file")
)
monkeypatch.setattr(
"litellm.proxy.openai_files_endpoints.files_endpoints.ManagedFileRepository", lambda _prisma: repository
)
monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", llm_router)
app.dependency_overrides[ps.user_api_key_auth] = lambda: UserAPIKeyAuth(
api_key="test-key",
user_role=LitellmUserRoles.INTERNAL_USER,
user_id="test-user",
)
def test_get_file_content_keeps_the_status_of_a_rejection_raised_inside_the_route(
mocker: MockerFixture, monkeypatch: pytest.MonkeyPatch, llm_router: Router
):
"""A ProxyException raised inside the route carries its status as the string ``code``,
and the tail used to rebuild it as a 500 because it only read ``status_code``."""
_setup_managed_file_stored_in_an_unknown_storage_backend(mocker, monkeypatch, llm_router)
response = _call_managed_file_route("GET", f"/v1/files/{_unified_managed_file_id()}/content")
assert response.status_code == 400, response.text
error = response.json()["error"]
assert error["message"].startswith("Storage backend error")
assert (error["type"], error["param"], error["code"]) == ("invalid_request_error", "file_id", "400")

View file

@ -1241,3 +1241,38 @@ def test_realtime_calls_upstream_rejection_answers_an_openai_typed_error(
assert response.status_code == 404
assert (response.json()["error"]["type"], response.json()["error"]["param"]) == ("invalid_request_error", None)
def test_transcription_sessions_rejection_answers_an_openai_typed_error(
proxy_app: FastAPI,
mock_add_litellm_data: Callable[..., Awaitable[object]],
mock_pre_call_hook: Callable[..., Awaitable[object]],
monkeypatch: pytest.MonkeyPatch,
):
"""A model the router cannot serve surfaces as a bare HTTPException, which this tail
used to relabel with the literal string "None" for both type and param."""
async def failing_route_request(*args: object, **kwargs: object) -> None:
raise HTTPException(
status_code=400,
detail={"error": "realtime: Invalid model name passed in model=no-such-transcribe"},
)
proxy_logging = MagicMock()
proxy_logging.pre_call_hook = AsyncMock(side_effect=mock_pre_call_hook)
proxy_logging.post_call_failure_hook = AsyncMock()
monkeypatch.setattr("litellm.proxy.proxy_server.route_request", failing_route_request)
monkeypatch.setattr("litellm.proxy.proxy_server.add_litellm_data_to_request", mock_add_litellm_data)
monkeypatch.setattr("litellm.proxy.proxy_server.proxy_logging_obj", proxy_logging)
proxy_app.dependency_overrides[user_api_key_auth] = lambda: UserAPIKeyAuth(user_id="test-user")
try:
response = TestClient(proxy_app, raise_server_exceptions=False).post(
"/v1/realtime/transcription_sessions",
headers={"Authorization": "Bearer sk-test-master-key"},
json={"input_audio_transcription": {"model": "no-such-transcribe"}},
)
finally:
proxy_app.dependency_overrides.pop(user_api_key_auth, None)
assert response.status_code == 400
assert (response.json()["error"]["type"], response.json()["error"]["param"]) == ("invalid_request_error", None)

View file

@ -2202,6 +2202,19 @@ class TestGuardrailBlockErrorPayloadNeverStringifiesNone:
assert frame["error"]["param"] is None
assert frame["error"]["code"] == "400"
def test_a_streaming_frame_keeps_the_status_a_proxy_exception_was_raised_with(self):
"""ProxyException stores its status as the string ``code``, so a 429 raised before the
first chunk used to reach the SSE frame as a 500."""
from litellm.proxy._types import ProxyException
from litellm.proxy.common_request_processing import sse_error_payload
error_status, error_obj = sse_error_payload(
ProxyException(message="Rate limit reached", type="rate_limit_error", param=None, code=429)
)
assert error_status == 429
assert (error_obj["type"], error_obj["code"]) == ("rate_limit_error", "429")
@pytest.mark.parametrize(
"status_code, expected_type",
[