diff --git a/docs/my-website/docs/providers/openai.md b/docs/my-website/docs/providers/openai.md index 9d557303ef..80931ad821 100644 --- a/docs/my-website/docs/providers/openai.md +++ b/docs/my-website/docs/providers/openai.md @@ -638,7 +638,9 @@ This is useful when you want to use [Responses API](https://platform.openai.com/ :::tip gpt-5.4 + reasoning_effort + function tools -OpenAI does not support `reasoning_effort` with function tools for `gpt-5.4` in `/v1/chat/completions`. Use the responses bridge instead: +LiteLLM drops `reasoning_effort` from `gpt-5.4` requests to `litellm.completion()` that include tools, since that combination is supported in the Responses API. + +If you need reasoning **and** tools together, use the responses bridge instead: ```python response = litellm.completion( diff --git a/docs/my-website/docs/reasoning_content.md b/docs/my-website/docs/reasoning_content.md index 5dd40122c7..8bf59f66a3 100644 --- a/docs/my-website/docs/reasoning_content.md +++ b/docs/my-website/docs/reasoning_content.md @@ -594,7 +594,9 @@ Expected Response :::tip gpt-5.4: reasoning_effort + function tools -OpenAI does not support `reasoning_effort` with function tools for `gpt-5.4` in `/v1/chat/completions`. Use `openai/responses/gpt-5.4` to route through the Responses API instead. See [Responses API Bridge](/docs/providers/openai#openai-chat-completion-to-responses-api-bridge) for details. +LiteLLM drops `reasoning_effort` from `gpt-5.4` requests to `litellm.completion()` that include tools, since that combination is supported in the Responses API. + +If you need reasoning **and** tools together, use `openai/responses/gpt-5.4` to route through the Responses API instead. See [Responses API Bridge](/docs/providers/openai#openai-chat-completion-to-responses-api-bridge) for details. ::: diff --git a/litellm/google_genai/main.py b/litellm/google_genai/main.py index 7c97975a54..a937a35da2 100644 --- a/litellm/google_genai/main.py +++ b/litellm/google_genai/main.py @@ -39,15 +39,6 @@ base_llm_http_handler = BaseLLMHTTPHandler() ################################################# -def _get_tool_config_from_kwargs(kwargs: Dict[str, Any]) -> Optional[Dict[str, Any]]: - """Read toolConfig/tool_config without dropping intentionally empty dicts.""" - if "toolConfig" in kwargs: - return kwargs["toolConfig"] - if "tool_config" in kwargs: - return kwargs["tool_config"] - return None - - class GenerateContentSetupResult(BaseModel): """Internal Type - Result of setting up a generate content call""" @@ -180,14 +171,12 @@ class GenerateContentHelper: system_instruction = kwargs.get("systemInstruction") or kwargs.get( "system_instruction" ) - tool_config = _get_tool_config_from_kwargs(kwargs) request_body = ( generate_content_provider_config.transform_generate_content_request( model=model, contents=contents, tools=tools, generate_content_config_dict=generate_content_config_dict, - tool_config=tool_config, system_instruction=system_instruction, ) ) @@ -334,7 +323,6 @@ def generate_content( system_instruction = kwargs.get("systemInstruction") or kwargs.get( "system_instruction" ) - tool_config = _get_tool_config_from_kwargs(kwargs) # Check if we should use the adapter (when provider config is None) if setup_result.generate_content_provider_config is None: @@ -366,7 +354,6 @@ def generate_content( _is_async=_is_async, client=kwargs.get("client"), litellm_metadata=kwargs.get("litellm_metadata", {}), - tool_config=tool_config, system_instruction=system_instruction, ) @@ -427,7 +414,6 @@ async def agenerate_content_stream( system_instruction = kwargs.get("systemInstruction") or kwargs.get( "system_instruction" ) - tool_config = _get_tool_config_from_kwargs(kwargs) # Check if we should use the adapter (when provider config is None) if setup_result.generate_content_provider_config is None: @@ -466,7 +452,6 @@ async def agenerate_content_stream( client=kwargs.get("client"), stream=True, litellm_metadata=kwargs.get("litellm_metadata", {}), - tool_config=tool_config, system_instruction=system_instruction, ) @@ -535,10 +520,6 @@ def generate_content_stream( ) # Call the handler with streaming enabled (sync version) - system_instruction = kwargs.get("systemInstruction") or kwargs.get( - "system_instruction" - ) - tool_config = _get_tool_config_from_kwargs(kwargs) return base_llm_http_handler.generate_content_handler( model=setup_result.model, contents=contents, @@ -555,8 +536,6 @@ def generate_content_stream( client=kwargs.get("client"), stream=True, litellm_metadata=kwargs.get("litellm_metadata", {}), - tool_config=tool_config, - system_instruction=system_instruction, ) except Exception as e: diff --git a/litellm/llms/base_llm/google_genai/transformation.py b/litellm/llms/base_llm/google_genai/transformation.py index 7952e2b0e1..e8b3bf1a57 100644 --- a/litellm/llms/base_llm/google_genai/transformation.py +++ b/litellm/llms/base_llm/google_genai/transformation.py @@ -152,7 +152,6 @@ class BaseGoogleGenAIGenerateContentConfig(ABC): contents: GenerateContentContentListUnionDict, tools: Optional[ToolConfigDict], generate_content_config_dict: Dict, - tool_config: Optional[Dict[str, Any]] = None, system_instruction: Optional[Any] = None, ) -> dict: """ @@ -162,7 +161,6 @@ class BaseGoogleGenAIGenerateContentConfig(ABC): model: The model name contents: Input contents tools: Tools - tool_config: Tool configuration generate_content_config_dict: Generation config parameters system_instruction: Optional system instruction diff --git a/litellm/llms/custom_httpx/llm_http_handler.py b/litellm/llms/custom_httpx/llm_http_handler.py index 9706a9f6e1..a8d649064a 100644 --- a/litellm/llms/custom_httpx/llm_http_handler.py +++ b/litellm/llms/custom_httpx/llm_http_handler.py @@ -9334,7 +9334,6 @@ class BaseLLMHTTPHandler: client: Optional[Union[HTTPHandler, AsyncHTTPHandler]] = None, stream: bool = False, litellm_metadata: Optional[Dict[str, Any]] = None, - tool_config: Optional[Dict[str, Any]] = None, system_instruction: Optional[Any] = None, ) -> Any: """ @@ -9352,7 +9351,6 @@ class BaseLLMHTTPHandler: generate_content_provider_config=generate_content_provider_config, generate_content_config_dict=generate_content_config_dict, tools=tools, - tool_config=tool_config, custom_llm_provider=custom_llm_provider, litellm_params=litellm_params, logging_obj=logging_obj, @@ -9391,7 +9389,6 @@ class BaseLLMHTTPHandler: model=model, contents=contents, tools=tools, - tool_config=tool_config, generate_content_config_dict=generate_content_config_dict, system_instruction=system_instruction, ) @@ -9464,7 +9461,6 @@ class BaseLLMHTTPHandler: client: Optional[AsyncHTTPHandler] = None, stream: bool = False, litellm_metadata: Optional[Dict[str, Any]] = None, - tool_config: Optional[Dict[str, Any]] = None, system_instruction: Optional[Any] = None, ) -> Any: """ @@ -9502,7 +9498,6 @@ class BaseLLMHTTPHandler: model=model, contents=contents, tools=tools, - tool_config=tool_config, generate_content_config_dict=generate_content_config_dict, system_instruction=system_instruction, ) diff --git a/litellm/llms/gemini/google_genai/transformation.py b/litellm/llms/gemini/google_genai/transformation.py index 24f59b8072..7c4c7dba62 100644 --- a/litellm/llms/gemini/google_genai/transformation.py +++ b/litellm/llms/gemini/google_genai/transformation.py @@ -308,7 +308,6 @@ class GoogleGenAIConfig(BaseGoogleGenAIGenerateContentConfig, VertexLLM): contents: GenerateContentContentListUnionDict, tools: Optional[ToolConfigDict], generate_content_config_dict: Dict, - tool_config: Optional[Dict[str, Any]] = None, system_instruction: Optional[Any] = None, ) -> dict: from litellm.types.google_genai.main import ( @@ -327,8 +326,6 @@ class GoogleGenAIConfig(BaseGoogleGenAIGenerateContentConfig, VertexLLM): if system_instruction is not None: request_dict["systemInstruction"] = system_instruction - if tool_config is not None: - request_dict["toolConfig"] = tool_config return request_dict def transform_generate_content_response( diff --git a/litellm/llms/openai/chat/gpt_5_transformation.py b/litellm/llms/openai/chat/gpt_5_transformation.py index bb5783011a..3f33d6183f 100644 --- a/litellm/llms/openai/chat/gpt_5_transformation.py +++ b/litellm/llms/openai/chat/gpt_5_transformation.py @@ -188,9 +188,11 @@ class OpenAIGPT5Config(OpenAIGPTConfig): ) or optional_params.get("reasoning_effort") effective_effort = _get_effort_level(raw_reasoning_effort) - # Normalize dict reasoning_effort to string for Chat Completions API. - # Example: {"effort": "high", "summary": "detailed"} -> "high" - if isinstance(raw_reasoning_effort, dict) and "effort" in raw_reasoning_effort: + # Normalize to string for Chat Completions API when dict has only "effort". + # Preserve full dict (e.g. {"effort": "high", "summary": "detailed"}) for Responses API. + if isinstance(raw_reasoning_effort, dict) and set( + raw_reasoning_effort.keys() + ) <= {"effort"}: normalized = _normalize_reasoning_effort_for_chat_completion( raw_reasoning_effort ) @@ -221,6 +223,16 @@ class OpenAIGPT5Config(OpenAIGPTConfig): "max_tokens" ) + # gpt-5.4: reasoning_effort + tools is only supported in the Responses API + # Drop reasoning_effort when tools are present in chat completions + if self.is_model_gpt_5_4_model(model): + has_tools = bool( + non_default_params.get("tools") or optional_params.get("tools") + ) + if has_tools and effective_effort is not None: + non_default_params.pop("reasoning_effort", None) + optional_params.pop("reasoning_effort", None) + # gpt-5.1/5.2 support logprobs, top_p, top_logprobs only when reasoning_effort="none" supports_none = self._supports_reasoning_effort_level(model, "none") if supports_none: diff --git a/litellm/llms/vertex_ai/google_genai/transformation.py b/litellm/llms/vertex_ai/google_genai/transformation.py index 18836b164d..d7a4ceeb3e 100644 --- a/litellm/llms/vertex_ai/google_genai/transformation.py +++ b/litellm/llms/vertex_ai/google_genai/transformation.py @@ -73,7 +73,6 @@ class VertexAIGoogleGenAIConfig(GoogleGenAIConfig): contents: Any, tools: Optional[Any], generate_content_config_dict: Dict, - tool_config: Optional[Dict[str, Any]] = None, system_instruction: Optional[Any] = None, ) -> dict: """ @@ -90,11 +89,8 @@ class VertexAIGoogleGenAIConfig(GoogleGenAIConfig): if tools: result["tools"] = tools - if tool_config is not None: - result["toolConfig"] = tool_config - # Add systemInstruction if provided - if system_instruction is not None: + if system_instruction: result["systemInstruction"] = system_instruction # Handle generationConfig - Vertex AI expects it in the same format diff --git a/litellm/main.py b/litellm/main.py index 722b4a7aae..569a9133d2 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -99,7 +99,6 @@ from litellm.llms.base_llm.base_model_iterator import ( from litellm.llms.bedrock.common_utils import BedrockModelInfo from litellm.llms.cohere.common_utils import CohereModelInfo from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler, HTTPHandler -from litellm.llms.openai.chat.gpt_5_transformation import OpenAIGPT5Config from litellm.llms.openai_like.json_loader import JSONProviderRegistry from litellm.llms.vertex_ai.common_utils import ( VertexAIModelRoute, @@ -935,8 +934,6 @@ def responses_api_bridge_check( model: str, custom_llm_provider: str, web_search_options: Optional[OpenAIWebSearchOptions] = None, - tools: Optional[List[Any]] = None, - reasoning_effort: Optional[Any] = None, ) -> Tuple[dict, str]: model_info: Dict[str, Any] = {} try: @@ -954,17 +951,6 @@ def responses_api_bridge_check( if web_search_options is not None and custom_llm_provider == "xai": model_info["mode"] = "responses" model = model.replace("responses/", "") - - # OpenAI gpt-5.4+ chat-completions calls with both tools + reasoning_effort - # must be bridged to Responses API. - if ( - custom_llm_provider == "openai" - and OpenAIGPT5Config.is_model_gpt_5_4_plus_model(model) - and tools - and reasoning_effort is not None - ): - model_info["mode"] = "responses" - model = model.replace("responses/", "") except Exception as e: verbose_logger.debug("Error getting model info: {}".format(e)) @@ -1610,17 +1596,11 @@ def completion( # type: ignore # noqa: PLR0915 model=model, custom_llm_provider=custom_llm_provider, web_search_options=web_search_options, - tools=tools, - reasoning_effort=reasoning_effort, ) if model_info.get("mode") == "responses": from litellm.completion_extras import responses_api_bridge - if isinstance(reasoning_effort, dict) and "summary" in reasoning_effort: - optional_params = dict(optional_params) - optional_params["reasoning_effort"] = reasoning_effort - return responses_api_bridge.completion( model=model, messages=messages, diff --git a/tests/proxy_unit_tests/test_google_gemini_proxy_request.py b/tests/proxy_unit_tests/test_google_gemini_proxy_request.py index 92b4b9af49..90c2cac18d 100644 --- a/tests/proxy_unit_tests/test_google_gemini_proxy_request.py +++ b/tests/proxy_unit_tests/test_google_gemini_proxy_request.py @@ -174,7 +174,6 @@ async def test_google_gemini_httpx_request_direct(): ], "role": "user" }, - "toolConfig": {"functionCallingConfig": {"mode": "ANY"}}, "config": { # Note: already transformed from generationConfig "temperature": 0, "topP": 1, @@ -241,7 +240,6 @@ async def test_google_gemini_httpx_request_direct(): generate_content_provider_config=provider_config, generate_content_config_dict=sample_payload["config"], tools=None, - tool_config=sample_payload["toolConfig"], custom_llm_provider="gemini", litellm_params=litellm_params, logging_obj=logging_obj, @@ -267,7 +265,6 @@ async def test_google_gemini_httpx_request_direct(): request_data = call_kwargs.get('json') if request_data: assert 'contents' in request_data, "Expected 'contents' in request data" - assert request_data["toolConfig"] == sample_payload["toolConfig"] # The config should be included in the request as generationConfig if 'generationConfig' in request_data: diff --git a/tests/test_litellm/google_genai/test_google_genai_main.py b/tests/test_litellm/google_genai/test_google_genai_main.py index 5eb5c6a117..5854e4b55a 100644 --- a/tests/test_litellm/google_genai/test_google_genai_main.py +++ b/tests/test_litellm/google_genai/test_google_genai_main.py @@ -1,13 +1,24 @@ #!/usr/bin/env python3 -"""Tests for Google GenAI main entrypoints.""" - +""" +Test to verify the Google GenAI generate_content adapter functionality +""" +import json import os import sys -from unittest.mock import AsyncMock, MagicMock, patch import pytest -sys.path.insert(0, os.path.abspath("../../..")) +sys.path.insert( + 0, os.path.abspath("../../..") +) # Adds the parent directory to the system path + +import json +import os +import sys + +import pytest + +import litellm @pytest.mark.asyncio @@ -15,6 +26,8 @@ async def test_agenerate_content_stream(): """ Test that the agenerate_content_stream function works """ + from unittest.mock import AsyncMock, patch + from litellm.google_genai.main import ( agenerate_content_stream, base_llm_http_handler, @@ -23,40 +36,10 @@ async def test_agenerate_content_stream(): with patch.object( base_llm_http_handler, "generate_content_handler", new=AsyncMock() ) as mock_post: - await agenerate_content_stream( + result = await agenerate_content_stream( model="gemini/gemini-2.0-flash-001", contents="Hello, world!", stream=True, ) mock_post.assert_called_once() - assert mock_post.call_args.kwargs["stream"] is True - - -def test_generate_content_stream_forwards_system_instruction(): - """Test that generate_content_stream forwards systemInstruction and toolConfig.""" - from litellm.google_genai.main import ( - base_llm_http_handler, - generate_content_stream, - ) - - mock_response = MagicMock() - tool_config = {"functionCallingConfig": {"mode": "ANY"}} - - with patch.object( - base_llm_http_handler, "generate_content_handler", return_value=mock_response - ) as mock_post: - result = generate_content_stream( - model="gemini/gemini-2.0-flash-001", - contents="Hello, world!", - stream=True, - systemInstruction={"parts": [{"text": "You are helpful"}]}, - toolConfig=tool_config, - ) - - assert result is mock_response - mock_post.assert_called_once() - assert mock_post.call_args.kwargs["stream"] is True - assert mock_post.call_args.kwargs["tool_config"] == tool_config - assert mock_post.call_args.kwargs["system_instruction"] == { - "parts": [{"text": "You are helpful"}] - } + mock_post.call_args.kwargs["stream"] == True diff --git a/tests/test_litellm/google_genai/test_google_genai_transformation.py b/tests/test_litellm/google_genai/test_google_genai_transformation.py index f5f63db819..8943d198dc 100644 --- a/tests/test_litellm/google_genai/test_google_genai_transformation.py +++ b/tests/test_litellm/google_genai/test_google_genai_transformation.py @@ -12,9 +12,6 @@ sys.path.insert( import pytest from litellm.llms.gemini.google_genai.transformation import GoogleGenAIConfig -from litellm.llms.vertex_ai.google_genai.transformation import ( - VertexAIGoogleGenAIConfig, -) from litellm.responses.litellm_completion_transformation.transformation import ( LiteLLMCompletionResponsesConfig, ) @@ -176,26 +173,6 @@ def test_map_generate_content_optional_params_response_mime_type(): assert "responseJsonSchema" in result -@pytest.mark.parametrize( - "config_cls", - [GoogleGenAIConfig, VertexAIGoogleGenAIConfig], -) -def test_transform_generate_content_request_preserves_tool_config(config_cls): - config = config_cls() - tool_config = {"functionCallingConfig": {"mode": "ANY"}} - - result = config.transform_generate_content_request( - model="gemini-3-flash-preview", - contents=[{"role": "user", "parts": [{"text": "hello"}]}], - tools=[{"functionDeclarations": [{"name": "execute_command"}]}], - tool_config=tool_config, - generate_content_config_dict={"temperature": 1}, - system_instruction={"parts": [{"text": "system"}]}, - ) - - assert result["toolConfig"] == tool_config - - def test_responses_api_reasoning_dict_format(): """Test that reasoning parameter with dict format is mapped to reasoning_effort""" from litellm.types.llms.openai import ResponsesAPIOptionalRequestParams @@ -297,7 +274,6 @@ def test_transform_generate_content_request_with_system_instruction(): model="gemini-3-flash-preview", contents=contents, tools=None, - tool_config=None, generate_content_config_dict=generate_content_config_dict, system_instruction=system_instruction, ) @@ -329,7 +305,6 @@ def test_transform_generate_content_request_without_system_instruction(): model="gemini-3-flash-preview", contents=contents, tools=None, - tool_config=None, generate_content_config_dict=generate_content_config_dict, system_instruction=None, ) @@ -381,7 +356,6 @@ def test_transform_generate_content_request_system_instruction_with_tools(): model="gemini-3-flash-preview", contents=contents, tools=tools, - tool_config=None, generate_content_config_dict=generate_content_config_dict, system_instruction=system_instruction, ) diff --git a/tests/test_litellm/llms/openai/chat/test_openai_gpt_transformation.py b/tests/test_litellm/llms/openai/chat/test_openai_gpt_transformation.py index c66b67cf4c..0f743b1a93 100644 --- a/tests/test_litellm/llms/openai/chat/test_openai_gpt_transformation.py +++ b/tests/test_litellm/llms/openai/chat/test_openai_gpt_transformation.py @@ -9,11 +9,11 @@ import pytest sys.path.insert(0, os.path.abspath("../../../../..")) -from litellm.llms.openai.chat.gpt_5_transformation import OpenAIGPT5Config from litellm.llms.openai.chat.gpt_transformation import ( OpenAIChatCompletionStreamingHandler, OpenAIGPTConfig, ) +from litellm.llms.openai.chat.gpt_5_transformation import OpenAIGPT5Config class TestOpenAIGPTConfig: @@ -460,11 +460,8 @@ class TestGPT5ReasoningEffortPreservation: assert "reasoning_effort" not in non_default_params - def test_reasoning_effort_dict_none_treated_as_none_for_tools(self): - """none-dict: {"effort": "none", "summary": "detailed"} is treated as effort=none. - - Tool-drop guard should NOT fire; reasoning_effort should be kept. - """ + def test_reasoning_effort_dict_none_dropped_for_gpt5_4_with_tools(self): + """none-dict with tools on gpt-5.4: reasoning_effort is dropped.""" tools = [{"type": "function", "function": {"name": "test", "description": "test"}}] non_default_params = {"reasoning_effort": {"effort": "none", "summary": "detailed"}, "tools": tools} optional_params = {} @@ -476,7 +473,7 @@ class TestGPT5ReasoningEffortPreservation: drop_params=False, ) - assert non_default_params.get("reasoning_effort") == {"effort": "none", "summary": "detailed"} + assert "reasoning_effort" not in non_default_params assert non_default_params.get("tools") == tools def test_reasoning_effort_dict_none_treated_as_none_for_sampling(self): diff --git a/tests/test_litellm/llms/openai/test_gpt5_transformation.py b/tests/test_litellm/llms/openai/test_gpt5_transformation.py index 09e8954c13..7c731e4e00 100644 --- a/tests/test_litellm/llms/openai/test_gpt5_transformation.py +++ b/tests/test_litellm/llms/openai/test_gpt5_transformation.py @@ -324,15 +324,19 @@ def test_gpt5_4_pro_allows_reasoning_effort_xhigh(config: OpenAIConfig): assert params["reasoning_effort"] == "xhigh" -def test_gpt5_normalizes_reasoning_effort_dict_with_summary(config: OpenAIConfig): - """Dict with summary/generate_summary is normalized for chat completions.""" +def test_gpt5_preserves_reasoning_effort_dict_with_summary(config: OpenAIConfig): + """Dict with summary/generate_summary is preserved for Responses API. + + Config/deployments may pass Responses API format: {'effort': 'high', 'summary': 'detailed'}. + We preserve the full dict so it reaches the Responses API transformation. + """ params = config.map_openai_params( non_default_params={"reasoning_effort": {"effort": "high", "summary": "detailed"}}, optional_params={}, model="gpt-5.4", drop_params=False, ) - assert params["reasoning_effort"] == "high" + assert params["reasoning_effort"] == {"effort": "high", "summary": "detailed"} def test_gpt5_xhigh_dict_triggers_validation(config: OpenAIConfig): @@ -358,14 +362,14 @@ def test_gpt5_xhigh_dict_accepted_for_supported_model(config: OpenAIConfig): model="gpt-5.4", drop_params=False, ) - assert params["reasoning_effort"] == "xhigh" + assert params["reasoning_effort"] == {"effort": "xhigh", "summary": "detailed"} def test_gpt5_none_dict_with_tools_no_tool_drop(config: OpenAIConfig): - """Dict with effort='none' and tools: no tool-drop, reasoning_effort preserved. + """Dict with effort='none' and tools: reasoning_effort dropped for gpt-5.4. - Regression: effective_effort='none' must be used for tool-drop guard so - {"effort": "none", "summary": "detailed"} is not incorrectly treated as non-none. + gpt-5.4 drops all reasoning_effort when tools are present, + since that combination is only supported in the Responses API. """ tools = [{"type": "function", "function": {"name": "test", "description": "test"}}] params = config.map_openai_params( @@ -374,7 +378,7 @@ def test_gpt5_none_dict_with_tools_no_tool_drop(config: OpenAIConfig): model="gpt-5.4", drop_params=False, ) - assert params["reasoning_effort"] == "none" + assert "reasoning_effort" not in params assert params["tools"] == tools @@ -394,20 +398,70 @@ def test_gpt5_none_dict_with_sampling_params_allowed(config: OpenAIConfig): model="gpt-5.1", drop_params=False, ) - assert params["reasoning_effort"] == "none" + assert params["reasoning_effort"] == {"effort": "none", "summary": "detailed"} assert params["logprobs"] is True assert params["top_p"] == 0.9 -def test_gpt5_normalizes_reasoning_effort_dict_with_summary_from_optional_params(config: OpenAIConfig): - """reasoning_effort dict with summary in optional_params is normalized.""" +def test_gpt5_preserves_reasoning_effort_dict_with_summary_from_optional_params(config: OpenAIConfig): + """reasoning_effort dict with summary in optional_params is preserved.""" params = config.map_openai_params( non_default_params={}, optional_params={"reasoning_effort": {"effort": "medium", "summary": "detailed"}}, model="gpt-5.4", drop_params=False, ) - assert params["reasoning_effort"] == "medium" + assert params["reasoning_effort"] == {"effort": "medium", "summary": "detailed"} + + +def test_gpt5_4_drops_reasoning_effort_when_user_sends_reasoning_and_tools(config: OpenAIConfig): + """gpt-5.4: function calls not supported with reasoning_effort != 'none'. Drop reasoning_effort.""" + tools = [{"type": "function", "function": {"name": "test", "description": "test"}}] + params = config.map_openai_params( + non_default_params={"reasoning_effort": "high", "tools": tools}, + optional_params={}, + model="gpt-5.4", + drop_params=False, + ) + assert "reasoning_effort" not in params + assert params["tools"] == tools + + +def test_gpt5_4_keeps_reasoning_effort_when_no_tools(config: OpenAIConfig): + """reasoning_effort is kept when tools are not present.""" + params = config.map_openai_params( + non_default_params={"reasoning_effort": "high"}, + optional_params={}, + model="gpt-5.4", + drop_params=False, + ) + assert params["reasoning_effort"] == "high" + + +def test_gpt5_4_drops_reasoning_effort_none_with_tools(config: OpenAIConfig): + """reasoning_effort='none' is also dropped when tools are present for gpt-5.4.""" + tools = [{"type": "function", "function": {"name": "test", "description": "test"}}] + params = config.map_openai_params( + non_default_params={"reasoning_effort": "none", "tools": tools}, + optional_params={}, + model="gpt-5.4", + drop_params=False, + ) + assert "reasoning_effort" not in params + assert params["tools"] == tools + + +def test_gpt5_2_keeps_reasoning_effort_with_tools(config: OpenAIConfig): + """gpt-5.2: reasoning_effort drop only applies to gpt-5.4, not gpt-5.2.""" + tools = [{"type": "function", "function": {"name": "test", "description": "test"}}] + params = config.map_openai_params( + non_default_params={"reasoning_effort": "high", "tools": tools}, + optional_params={}, + model="gpt-5.2", + drop_params=False, + ) + assert params["reasoning_effort"] == "high" + assert params["tools"] == tools def test_gpt5_4_pro_rejects_non_default_temperature(config: OpenAIConfig): diff --git a/tests/test_litellm/test_main.py b/tests/test_litellm/test_main.py index 6ac988b2c2..3a43b1229d 100644 --- a/tests/test_litellm/test_main.py +++ b/tests/test_litellm/test_main.py @@ -627,94 +627,6 @@ def test_responses_api_bridge_check_gpt_5_4_pro(): ) -def test_responses_api_bridge_check_gpt_5_4_tools_plus_reasoning_routes_to_responses(): - """gpt-5.4 with both tools and reasoning_effort should route to Responses API.""" - from litellm.main import responses_api_bridge_check - - with patch("litellm.main._get_model_info_helper") as mock_get_model_info: - mock_get_model_info.return_value = {"max_tokens": 128000} - model_info, model = responses_api_bridge_check( - model="gpt-5.4", - custom_llm_provider="openai", - tools=[{"type": "function", "function": {"name": "get_capital"}}], - reasoning_effort="xhigh", - ) - - assert model == "gpt-5.4" - assert model_info.get("mode") == "responses" - - -def test_responses_api_bridge_check_gpt_5_5_tools_plus_reasoning_routes_to_responses(): - """gpt-5.5+ with both tools and reasoning_effort should route to Responses API.""" - from litellm.main import responses_api_bridge_check - - with patch("litellm.main._get_model_info_helper") as mock_get_model_info: - mock_get_model_info.return_value = {"max_tokens": 128000} - model_info, model = responses_api_bridge_check( - model="gpt-5.5-pro", - custom_llm_provider="openai", - tools=[{"type": "function", "function": {"name": "get_capital"}}], - reasoning_effort="xhigh", - ) - - assert model == "gpt-5.5-pro" - assert model_info.get("mode") == "responses" - - -def test_responses_api_bridge_check_gpt_5_4_tools_without_reasoning_stays_chat(): - """gpt-5.4 with tools only should not be force-routed to Responses API.""" - from litellm.main import responses_api_bridge_check - - with patch("litellm.main._get_model_info_helper") as mock_get_model_info: - mock_get_model_info.return_value = {"max_tokens": 128000} - model_info, model = responses_api_bridge_check( - model="gpt-5.4", - custom_llm_provider="openai", - tools=[{"type": "function", "function": {"name": "get_capital"}}], - reasoning_effort=None, - ) - - assert model == "gpt-5.4" - assert model_info.get("mode") != "responses" - - -@patch("litellm.completion_extras.responses_api_bridge.completion") -def test_gpt_5_4_responses_bridge_preserves_reasoning_summary_dict( - mock_responses_completion, -): - """When routed to Responses, preserve reasoning_effort summary dict.""" - mock_responses_completion.return_value = MagicMock() - - import litellm - - litellm.completion( - model="gpt-5.4", - messages=[{"role": "user", "content": "What is the capital of France?"}], - tools=[ - { - "type": "function", - "function": { - "name": "get_capital", - "description": "Get the capital of a country", - "parameters": { - "type": "object", - "properties": {"country": {"type": "string"}}, - }, - }, - } - ], - reasoning_effort={"effort": "xhigh", "summary": "detailed"}, - api_key="fake-key", - ) - - assert mock_responses_completion.called is True - optional_params = mock_responses_completion.call_args.kwargs["optional_params"] - assert optional_params["reasoning_effort"] == { - "effort": "xhigh", - "summary": "detailed", - } - - def test_responses_api_bridge_check_handles_exception(): """Test that responses_api_bridge_check handles exceptions and still processes responses/ models.""" from litellm.main import responses_api_bridge_check