diff --git a/litellm/proxy/response_api_endpoints/endpoints.py b/litellm/proxy/response_api_endpoints/endpoints.py index 3b6afc34063..a680e445a2c 100644 --- a/litellm/proxy/response_api_endpoints/endpoints.py +++ b/litellm/proxy/response_api_endpoints/endpoints.py @@ -34,6 +34,7 @@ from litellm.proxy.common_utils.http_parsing_utils import ( _read_request_body, _safe_set_request_parsed_body, ) +from litellm.proxy.route_llm_request import raise_if_required_body_param_missing from litellm.types.llms.openai import ( REASONING_EFFORT, ResponsesAPIOptionalRequestParams, @@ -280,6 +281,7 @@ async def responses_api( # instead of a polling ID that immediately fails in the background task. processor = ProxyBaseLLMRequestProcessing(data=data) try: + raise_if_required_body_param_missing(route_type="aresponses", data=data) data, _logging_obj = await processor.common_processing_pre_call_logic( request=request, general_settings=general_settings, diff --git a/tests/test_litellm/proxy/response_api_endpoints/test_endpoints.py b/tests/test_litellm/proxy/response_api_endpoints/test_endpoints.py index f53fbde6b51..751d4753608 100644 --- a/tests/test_litellm/proxy/response_api_endpoints/test_endpoints.py +++ b/tests/test_litellm/proxy/response_api_endpoints/test_endpoints.py @@ -121,6 +121,71 @@ async def test_streaming_upstream_errors_keep_the_client_protocol( assert "error" in events[-1] +@pytest.mark.asyncio +async def test_responses_api_background_polling_rejects_missing_input(): + from fastapi import Response as FastAPIResponse + from starlette.requests import Request + + from litellm.proxy._types import ProxyException, UserAPIKeyAuth + from litellm.proxy.response_api_endpoints.endpoints import responses_api + + processor = MagicMock() + + async def return_exception(*, e: Exception, **kwargs: object) -> Exception: + return e + + processor._handle_llm_api_exception = AsyncMock(side_effect=return_exception) + processor.common_processing_pre_call_logic = AsyncMock(return_value=({"model": "gpt-4o"}, MagicMock())) + + async def receive(): + return { + "type": "http.request", + "body": b'{"model":"gpt-4o","background":true}', + "more_body": False, + } + + request = Request( + { + "type": "http", + "method": "POST", + "path": "/v1/responses", + "headers": [(b"content-type", b"application/json")], + }, + receive, + ) + + with ( + patch( # test-quality-ok: endpoint constructs the processor directly + "litellm.proxy.response_api_endpoints.endpoints.ProxyBaseLLMRequestProcessing", + return_value=processor, + ), + patch( # test-quality-ok: polling decision is imported inside the endpoint + "litellm.proxy.response_polling.polling_handler.should_use_polling_for_request", + return_value=True, + ), + patch( # test-quality-ok: background task is imported inside the endpoint + "litellm.proxy.response_polling.background_streaming.background_streaming_task", + new_callable=AsyncMock, + ) as mock_background_streaming_task, + patch( # test-quality-ok: polling handler is imported inside the endpoint + "litellm.proxy.response_polling.polling_handler.ResponsePollingHandler.create_initial_state", + new_callable=AsyncMock, + ) as mock_create_initial_state, + ): + with pytest.raises(ProxyException) as exc_info: + await responses_api( + request=request, + fastapi_response=FastAPIResponse(), + user_api_key_dict=UserAPIKeyAuth(api_key="sk-test"), + ) + + assert exc_info.value.code == "400" + assert exc_info.value.param == "input" + processor.common_processing_pre_call_logic.assert_not_awaited() + mock_background_streaming_task.assert_not_called() + mock_create_initial_state.assert_not_awaited() + + class TestResponsesAPIEndpoints(unittest.TestCase): @pytest.mark.asyncio @patch("litellm.proxy.proxy_server.llm_router")