diff --git a/litellm/responses/litellm_completion_transformation/handler.py b/litellm/responses/litellm_completion_transformation/handler.py index 74ad6675e1d..e676a7a45b4 100644 --- a/litellm/responses/litellm_completion_transformation/handler.py +++ b/litellm/responses/litellm_completion_transformation/handler.py @@ -31,6 +31,7 @@ class LiteLLMCompletionTransformationHandler: _is_async: bool = False, stream: Optional[bool] = None, extra_headers: Optional[Dict[str, Any]] = None, + extra_body: Optional[Dict[str, Any]] = None, **kwargs, ) -> Union[ ResponsesAPIResponse, @@ -47,6 +48,7 @@ class LiteLLMCompletionTransformationHandler: custom_llm_provider=custom_llm_provider, stream=stream, extra_headers=extra_headers, + extra_body=extra_body, **kwargs, ) ) diff --git a/litellm/responses/litellm_completion_transformation/transformation.py b/litellm/responses/litellm_completion_transformation/transformation.py index 2bff4e23c78..89d87a3eb48 100644 --- a/litellm/responses/litellm_completion_transformation/transformation.py +++ b/litellm/responses/litellm_completion_transformation/transformation.py @@ -161,6 +161,7 @@ class LiteLLMCompletionResponsesConfig: custom_llm_provider: Optional[str] = None, stream: Optional[bool] = None, extra_headers: Optional[Dict[str, Any]] = None, + extra_body: Optional[Dict[str, Any]] = None, **kwargs, ) -> dict: """ @@ -215,6 +216,7 @@ class LiteLLMCompletionResponsesConfig: # litellm specific params "custom_llm_provider": custom_llm_provider, "extra_headers": extra_headers, + "extra_body": extra_body, } # Responses API `Completed` events require usage, we pass `stream_options` to litellm.completion to include usage diff --git a/litellm/responses/main.py b/litellm/responses/main.py index 8f524690be1..214c98ed815 100644 --- a/litellm/responses/main.py +++ b/litellm/responses/main.py @@ -681,6 +681,7 @@ def responses( _is_async=_is_async, stream=stream, extra_headers=extra_headers, + extra_body=extra_body, **kwargs, ) diff --git a/tests/test_litellm/test_extra_body_responses_bridge.py b/tests/test_litellm/test_extra_body_responses_bridge.py new file mode 100644 index 00000000000..20e594b3167 --- /dev/null +++ b/tests/test_litellm/test_extra_body_responses_bridge.py @@ -0,0 +1,100 @@ +""" +Regression tests for #20982 — ``extra_body`` is not passed through the +responses-to-completion bridge. + +When calling ``litellm.responses()`` with ``extra_body`` for a model +that uses the completion bridge, the ``extra_body`` parameter was silently +dropped because: +1. It's an explicit parameter, so it's not in ``**kwargs`` +2. The handler and transformation functions didn't accept/forward it +""" + +import os +import sys + +sys.path.insert( + 0, os.path.abspath("../..") +) # Adds the parent directory to the system path + +from litellm.responses.litellm_completion_transformation.transformation import ( + LiteLLMCompletionResponsesConfig, +) + + +class TestExtraBodyResponsesBridge: + """Verify ``extra_body`` flows through the responses-to-completion + transformation.""" + + def test_extra_body_included_in_completion_request(self): + """extra_body should appear in the transformed completion request.""" + extra_body = {"provider": {"order": ["Together"], "allow_fallbacks": False}} + result = LiteLLMCompletionResponsesConfig.transform_responses_api_request_to_chat_completion_request( + model="openrouter/test-model", + input="Hello", + responses_api_request={}, + custom_llm_provider="openrouter", + stream=False, + extra_headers=None, + extra_body=extra_body, + ) + assert result.get("extra_body") == extra_body + + def test_extra_body_none_by_default(self): + """When not provided, extra_body should be None in the request.""" + result = LiteLLMCompletionResponsesConfig.transform_responses_api_request_to_chat_completion_request( + model="openrouter/test-model", + input="Hello", + responses_api_request={}, + custom_llm_provider="openrouter", + stream=False, + extra_headers=None, + ) + assert result.get("extra_body") is None + + def test_extra_body_preserved_with_other_params(self): + """extra_body should coexist with other parameters like + extra_headers, temperature, etc.""" + extra_body = {"custom_key": "custom_value"} + extra_headers = {"X-Custom": "header"} + result = LiteLLMCompletionResponsesConfig.transform_responses_api_request_to_chat_completion_request( + model="test-model", + input="Hello", + responses_api_request={"temperature": 0.7, "max_output_tokens": 100}, + custom_llm_provider=None, + stream=True, + extra_headers=extra_headers, + extra_body=extra_body, + ) + assert result["extra_body"] == extra_body + assert result["extra_headers"] == extra_headers + assert result["temperature"] == 0.7 + assert result["max_tokens"] == 100 + + def test_extra_body_empty_dict(self): + """An empty extra_body dict should still be passed through.""" + result = LiteLLMCompletionResponsesConfig.transform_responses_api_request_to_chat_completion_request( + model="test-model", + input="Hello", + responses_api_request={}, + extra_body={}, + ) + assert result.get("extra_body") == {} + + def test_extra_body_nested_dict(self): + """Deeply nested extra_body should be preserved as-is.""" + extra_body = { + "provider": { + "order": ["Together", "Fireworks"], + "allow_fallbacks": False, + "quantizations": ["fp16"], + }, + "transforms": ["middle-out"], + } + result = LiteLLMCompletionResponsesConfig.transform_responses_api_request_to_chat_completion_request( + model="openrouter/model", + input="Test", + responses_api_request={}, + extra_body=extra_body, + ) + assert result["extra_body"] == extra_body + assert result["extra_body"]["provider"]["order"] == ["Together", "Fireworks"]