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.
This commit is contained in:
Cesar Garcia
2025-11-06 14:54:40 -08:00
committed by GitHub
parent c98b125851
commit 1fec48499f
2 changed files with 136 additions and 0 deletions
@@ -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,
@@ -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"