From 1fec48499f2ed484dc580beb4309e719e2238ea3 Mon Sep 17 00:00:00 2001 From: Cesar Garcia <128240629+Chesars@users.noreply.github.com> Date: Thu, 6 Nov 2025 19:54:40 -0300 Subject: [PATCH] fix: Pass extra_body to provider in Responses API requests (#16320) ## Problem The `extra_body` parameter in `litellm.responses()` and `litellm.aresponses()` was being accepted but never passed to the HTTP request sent to the LLM provider. This prevented users from sending custom/experimental parameters to provider APIs. ## Changes - Added `data.update(extra_body)` in `async_response_api_handler` (line 2138) - Added `data.update(extra_body)` in `response_api_handler` (line 2012) - Added tests to `test_openai_responses_api.py` for extra_body functionality ## Testing - Tests verify extra_body params are passed in both sync and async modes - Existing Responses API tests continue to pass - Manually verified with OpenAI API that custom params are sent correctly ## Impact Users can now pass custom/experimental parameters via extra_body: ```python litellm.aresponses( model="gpt-4o", input="hello", extra_body={"custom_param": "value"} # Now works! ) ``` This aligns with the OpenAI SDK pattern and matches behavior in other LiteLLM endpoints (completion, embedding, etc.) that already support extra_body. --- litellm/llms/custom_httpx/llm_http_handler.py | 6 + .../test_openai_responses_api.py | 130 ++++++++++++++++++ 2 files changed, 136 insertions(+) diff --git a/litellm/llms/custom_httpx/llm_http_handler.py b/litellm/llms/custom_httpx/llm_http_handler.py index dd39c29203..3a290e7ce8 100644 --- a/litellm/llms/custom_httpx/llm_http_handler.py +++ b/litellm/llms/custom_httpx/llm_http_handler.py @@ -2009,6 +2009,9 @@ class BaseLLMHTTPHandler: headers=headers, ) + if extra_body: + data.update(extra_body) + ## LOGGING logging_obj.pre_call( input=input, @@ -2135,6 +2138,9 @@ class BaseLLMHTTPHandler: headers=headers, ) + if extra_body: + data.update(extra_body) + ## LOGGING logging_obj.pre_call( input=input, diff --git a/tests/llm_responses_api_testing/test_openai_responses_api.py b/tests/llm_responses_api_testing/test_openai_responses_api.py index f93720620b..173860a0aa 100644 --- a/tests/llm_responses_api_testing/test_openai_responses_api.py +++ b/tests/llm_responses_api_testing/test_openai_responses_api.py @@ -1676,3 +1676,133 @@ async def test_openai_streaming_logging(): await asyncio.sleep(2) assert tcl.validate_usage, "Usage should be validated" + + +# Tests for extra_body parameter passing +class MockResponse: + def __init__(self, json_data, status_code): + self._json_data = json_data + self.status_code = status_code + self.text = str(json_data) + self.headers = httpx.Headers({}) + + def json(self): + return self._json_data + + +@pytest.fixture +def extra_body_mock_response_data(): + return { + "id": "resp_test123", + "object": "response", + "created_at": 1234567890, + "status": "completed", + "model": "gpt-4o", + "output": [ + { + "type": "message", + "id": "msg_123", + "status": "completed", + "role": "assistant", + "content": [ + {"type": "output_text", "text": "Hello!", "annotations": []} + ], + } + ], + "usage": {"input_tokens": 10, "output_tokens": 5, "total_tokens": 15}, + "parallel_tool_calls": True, + "text": {"format": {"type": "text"}}, + "error": None, + "metadata": {}, + "temperature": 1.0, + "reasoning": {"effort": None, "summary": None}, + } + + +@pytest.mark.asyncio +async def test_aresponses_extra_body_params_passed(extra_body_mock_response_data): + """Test that extra_body parameters are passed in async mode.""" + with patch( + "litellm.llms.custom_httpx.http_handler.AsyncHTTPHandler.post", + new_callable=AsyncMock, + ) as mock_post: + mock_post.return_value = MockResponse(extra_body_mock_response_data, 200) + + response = await litellm.aresponses( + model="gpt-4o", + input="Test input", + max_output_tokens=20, + extra_body={ + "custom_param_1": "value1", + "custom_param_2": {"nested": "value2"}, + "experimental_feature": True, + }, + ) + + assert response is not None + assert response.id is not None + + request_body = mock_post.call_args.kwargs["json"] + + assert "custom_param_1" in request_body + assert request_body["custom_param_1"] == "value1" + assert "custom_param_2" in request_body + assert request_body["custom_param_2"]["nested"] == "value2" + assert "experimental_feature" in request_body + assert request_body["experimental_feature"] is True + assert request_body["model"] == "gpt-4o" + assert request_body["input"] == "Test input" + + +def test_responses_extra_body_params_passed_sync(extra_body_mock_response_data): + """Test that extra_body parameters are passed in sync mode.""" + with patch( + "litellm.llms.custom_httpx.http_handler.HTTPHandler.post", + return_value=MockResponse(extra_body_mock_response_data, 200), + ) as mock_post: + response = litellm.responses( + model="gpt-4o", + input="Sync test", + max_output_tokens=20, + extra_body={ + "sync_custom_param": "sync_value", + "another_param": 42, + }, + ) + + assert response is not None + assert response.id is not None + + request_body = mock_post.call_args.kwargs["json"] + + assert "sync_custom_param" in request_body + assert request_body["sync_custom_param"] == "sync_value" + assert "another_param" in request_body + assert request_body["another_param"] == 42 + assert request_body["model"] == "gpt-4o" + + +@pytest.mark.asyncio +async def test_extra_body_merges_with_request_data(extra_body_mock_response_data): + """Test that extra_body is merged into the request data.""" + with patch( + "litellm.llms.custom_httpx.http_handler.AsyncHTTPHandler.post", + new_callable=AsyncMock, + ) as mock_post: + mock_post.return_value = MockResponse(extra_body_mock_response_data, 200) + + await litellm.aresponses( + model="gpt-4o", + input="Test", + temperature=0.7, + max_output_tokens=20, + extra_body={ + "custom_field": "custom_value", + }, + ) + + request_body = mock_post.call_args.kwargs["json"] + + assert "temperature" in request_body + assert "custom_field" in request_body + assert request_body["custom_field"] == "custom_value"