diff --git a/litellm/llms/vertex_ai/gemini/transformation.py b/litellm/llms/vertex_ai/gemini/transformation.py index 00b461dcda0..6a5ac92816b 100644 --- a/litellm/llms/vertex_ai/gemini/transformation.py +++ b/litellm/llms/vertex_ai/gemini/transformation.py @@ -529,6 +529,17 @@ def _gemini_convert_messages_with_history( # noqa: PLR0915 raise e +def _pop_and_merge_extra_body(data: RequestBody, optional_params: dict) -> None: + """Pop extra_body from optional_params and shallow-merge into data, deep-merging dict values.""" + extra_body: Optional[dict] = optional_params.pop("extra_body", None) + if extra_body is not None: + for k, v in extra_body.items(): + if k in data and isinstance(data[k], dict) and isinstance(v, dict): + data[k].update(v) + else: + data[k] = v + + def _transform_request_body( messages: List[AllMessageValues], model: str, @@ -619,6 +630,7 @@ def _transform_request_body( # Only add labels for Vertex AI endpoints (not Google GenAI/AI Studio) and only if non-empty if labels and custom_llm_provider != LlmProviders.GEMINI: data["labels"] = labels + _pop_and_merge_extra_body(data, optional_params) except Exception as e: raise e diff --git a/litellm/responses/main.py b/litellm/responses/main.py index 7105f1ae6fb..e943789a1cd 100644 --- a/litellm/responses/main.py +++ b/litellm/responses/main.py @@ -682,6 +682,7 @@ def responses( _is_async=_is_async, stream=stream, extra_headers=extra_headers, + extra_body=extra_body, **kwargs, ) diff --git a/tests/test_litellm/responses/test_responses_utils.py b/tests/test_litellm/responses/test_responses_utils.py index 8f7acb6c120..7feab9c6035 100644 --- a/tests/test_litellm/responses/test_responses_utils.py +++ b/tests/test_litellm/responses/test_responses_utils.py @@ -2,6 +2,7 @@ import base64 import json import os import sys +from unittest.mock import MagicMock, patch import pytest from fastapi.testclient import TestClient @@ -352,3 +353,34 @@ class TestResponsesAPIProviderSpecificParams: # Should not raise any exception result = ResponsesAPIRequestUtils.get_requested_response_api_optional_param(params) assert "temperature" in result + + +def test_responses_extra_body_forwarded_to_completion_transformation_handler(): + """ + Regression test: extra_body must be forwarded to response_api_handler + when responses_api_provider_config is None (completion transformation path). + + Before the fix, extra_body was a named parameter of responses() but was + not passed to litellm_completion_transformation_handler.response_api_handler(), + so it was silently dropped. + """ + with patch( + "litellm.responses.main.ProviderConfigManager.get_provider_responses_api_config", + return_value=None, + ), patch( + "litellm.responses.main.litellm_completion_transformation_handler.response_api_handler", + ) as mock_handler: + mock_handler.return_value = MagicMock() + + litellm.responses( + model="openai/gpt-4o", + input="Hello", + extra_body={"custom_key": "custom_value"}, + ) + + mock_handler.assert_called_once() + call_kwargs = mock_handler.call_args + # extra_body can be a positional or keyword arg; check both + assert call_kwargs.kwargs.get("extra_body") == { + "custom_key": "custom_value" + }