diff --git a/litellm/proxy/common_utils/openai_error_payload.py b/litellm/proxy/common_utils/openai_error_payload.py index 2d589871fea..89f735ee8b6 100644 --- a/litellm/proxy/common_utils/openai_error_payload.py +++ b/litellm/proxy/common_utils/openai_error_payload.py @@ -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", } ) diff --git a/tests/test_litellm/proxy/common_utils/test_openai_error_payload.py b/tests/test_litellm/proxy/common_utils/test_openai_error_payload.py index 3145be2d522..8b653ddfb71 100644 --- a/tests/test_litellm/proxy/common_utils/test_openai_error_payload.py +++ b/tests/test_litellm/proxy/common_utils/test_openai_error_payload.py @@ -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"), diff --git a/tests/test_litellm/proxy/openai_files_endpoint/test_files_endpoint.py b/tests/test_litellm/proxy/openai_files_endpoint/test_files_endpoint.py index 132b53792b0..5f1e7e1fe0c 100644 --- a/tests/test_litellm/proxy/openai_files_endpoint/test_files_endpoint.py +++ b/tests/test_litellm/proxy/openai_files_endpoint/test_files_endpoint.py @@ -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") diff --git a/tests/test_litellm/proxy/realtime_endpoints/test_realtime_webrtc_endpoints.py b/tests/test_litellm/proxy/realtime_endpoints/test_realtime_webrtc_endpoints.py index 8cc5994dc81..7436cf84fec 100644 --- a/tests/test_litellm/proxy/realtime_endpoints/test_realtime_webrtc_endpoints.py +++ b/tests/test_litellm/proxy/realtime_endpoints/test_realtime_webrtc_endpoints.py @@ -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) diff --git a/tests/test_litellm/proxy/test_common_request_processing.py b/tests/test_litellm/proxy/test_common_request_processing.py index bfae42f64f1..be666607823 100644 --- a/tests/test_litellm/proxy/test_common_request_processing.py +++ b/tests/test_litellm/proxy/test_common_request_processing.py @@ -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", [