From 3596464d11ada2cd4da1b8a097243b1e9502a627 Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Fri, 13 Mar 2026 13:40:29 +0530 Subject: [PATCH 1/4] Revert "feat(openai): drop reasoning_effort for gpt-5.4 when tools present" This reverts commit 14b52b131883f87f01fcacd1c6553c149e701da3. --- .../llms/openai/chat/gpt_5_transformation.py | 10 -------- .../chat/test_openai_gpt_transformation.py | 1 + .../llms/openai/test_gpt5_transformation.py | 23 +++++++++---------- 3 files changed, 12 insertions(+), 22 deletions(-) diff --git a/litellm/llms/openai/chat/gpt_5_transformation.py b/litellm/llms/openai/chat/gpt_5_transformation.py index 3f33d6183f3..e3b38050236 100644 --- a/litellm/llms/openai/chat/gpt_5_transformation.py +++ b/litellm/llms/openai/chat/gpt_5_transformation.py @@ -223,16 +223,6 @@ 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/tests/test_litellm/llms/openai/chat/test_openai_gpt_transformation.py b/tests/test_litellm/llms/openai/chat/test_openai_gpt_transformation.py index 0f743b1a93b..81ffa1aa428 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,6 +9,7 @@ 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, diff --git a/tests/test_litellm/llms/openai/test_gpt5_transformation.py b/tests/test_litellm/llms/openai/test_gpt5_transformation.py index 7c731e4e00a..d9547980067 100644 --- a/tests/test_litellm/llms/openai/test_gpt5_transformation.py +++ b/tests/test_litellm/llms/openai/test_gpt5_transformation.py @@ -324,11 +324,10 @@ def test_gpt5_4_pro_allows_reasoning_effort_xhigh(config: OpenAIConfig): assert params["reasoning_effort"] == "xhigh" -def test_gpt5_preserves_reasoning_effort_dict_with_summary(config: OpenAIConfig): - """Dict with summary/generate_summary is preserved for Responses API. +def test_gpt5_normalizes_reasoning_effort_dict_to_string(config: OpenAIConfig): + """Chat completion API expects reasoning_effort as a string, not a dict. 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"}}, @@ -366,10 +365,10 @@ def test_gpt5_xhigh_dict_accepted_for_supported_model(config: OpenAIConfig): def test_gpt5_none_dict_with_tools_no_tool_drop(config: OpenAIConfig): - """Dict with effort='none' and tools: reasoning_effort dropped for gpt-5.4. + """Dict with effort='none' and tools: no tool-drop, reasoning_effort preserved. - gpt-5.4 drops all reasoning_effort when tools are present, - since that combination is only supported in the Responses API. + Regression: effective_effort='none' must be used for tool-drop guard so + {"effort": "none", "summary": "detailed"} is not incorrectly treated as non-none. """ tools = [{"type": "function", "function": {"name": "test", "description": "test"}}] params = config.map_openai_params( @@ -378,7 +377,7 @@ def test_gpt5_none_dict_with_tools_no_tool_drop(config: OpenAIConfig): model="gpt-5.4", drop_params=False, ) - assert "reasoning_effort" not in params + assert params["reasoning_effort"] == {"effort": "none", "summary": "detailed"} assert params["tools"] == tools @@ -411,10 +410,10 @@ def test_gpt5_preserves_reasoning_effort_dict_with_summary_from_optional_params( model="gpt-5.4", drop_params=False, ) - assert params["reasoning_effort"] == {"effort": "medium", "summary": "detailed"} + assert params["reasoning_effort"] == "medium" -def test_gpt5_4_drops_reasoning_effort_when_user_sends_reasoning_and_tools(config: OpenAIConfig): +def test_gpt5_4_drops_reasoning_effort_when_tools_present(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( @@ -438,8 +437,8 @@ def test_gpt5_4_keeps_reasoning_effort_when_no_tools(config: OpenAIConfig): 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.""" +def test_gpt5_4_keeps_reasoning_effort_none_with_tools(config: OpenAIConfig): + """reasoning_effort='none' is kept when tools are present.""" tools = [{"type": "function", "function": {"name": "test", "description": "test"}}] params = config.map_openai_params( non_default_params={"reasoning_effort": "none", "tools": tools}, @@ -447,7 +446,7 @@ def test_gpt5_4_drops_reasoning_effort_none_with_tools(config: OpenAIConfig): model="gpt-5.4", drop_params=False, ) - assert "reasoning_effort" not in params + assert params["reasoning_effort"] == "none" assert params["tools"] == tools From 7abbe2dc064bec0717ca1d3c7d981131eb02db43 Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Fri, 13 Mar 2026 14:19:04 +0530 Subject: [PATCH 2/4] Fix routing of tool call + reasoning effor for gpt-5.4 --- litellm/main.py | 16 +++++++++++ tests/test_litellm/test_main.py | 51 +++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+) diff --git a/litellm/main.py b/litellm/main.py index 569a9133d2f..b0de4e8a207 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -99,6 +99,7 @@ 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, @@ -934,6 +935,8 @@ 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: @@ -951,6 +954,17 @@ 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)) @@ -1596,6 +1610,8 @@ 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": diff --git a/tests/test_litellm/test_main.py b/tests/test_litellm/test_main.py index 3a43b1229de..ab0a18490ea 100644 --- a/tests/test_litellm/test_main.py +++ b/tests/test_litellm/test_main.py @@ -627,6 +627,57 @@ 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" + + 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 From 30645d683fad8972675c1f2f881f8f9456427d63 Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Fri, 13 Mar 2026 14:19:40 +0530 Subject: [PATCH 3/4] Reserve reasoning for responses via chat completion --- .../llms/openai/chat/gpt_5_transformation.py | 8 ++-- litellm/main.py | 6 ++- .../llms/openai/test_gpt5_transformation.py | 19 ++++------ tests/test_litellm/test_main.py | 37 +++++++++++++++++++ 4 files changed, 53 insertions(+), 17 deletions(-) diff --git a/litellm/llms/openai/chat/gpt_5_transformation.py b/litellm/llms/openai/chat/gpt_5_transformation.py index e3b38050236..bb5783011a3 100644 --- a/litellm/llms/openai/chat/gpt_5_transformation.py +++ b/litellm/llms/openai/chat/gpt_5_transformation.py @@ -188,11 +188,9 @@ class OpenAIGPT5Config(OpenAIGPTConfig): ) or optional_params.get("reasoning_effort") effective_effort = _get_effort_level(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"}: + # 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: normalized = _normalize_reasoning_effort_for_chat_completion( raw_reasoning_effort ) diff --git a/litellm/main.py b/litellm/main.py index b0de4e8a207..722b4a7aaec 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -955,7 +955,7 @@ def responses_api_bridge_check( model_info["mode"] = "responses" model = model.replace("responses/", "") - # OpenAI gpt-5.4 chat-completions calls with both tools + reasoning_effort + # OpenAI gpt-5.4+ chat-completions calls with both tools + reasoning_effort # must be bridged to Responses API. if ( custom_llm_provider == "openai" @@ -1617,6 +1617,10 @@ def completion( # type: ignore # noqa: PLR0915 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/test_litellm/llms/openai/test_gpt5_transformation.py b/tests/test_litellm/llms/openai/test_gpt5_transformation.py index d9547980067..8eeca8c1d7f 100644 --- a/tests/test_litellm/llms/openai/test_gpt5_transformation.py +++ b/tests/test_litellm/llms/openai/test_gpt5_transformation.py @@ -324,18 +324,15 @@ def test_gpt5_4_pro_allows_reasoning_effort_xhigh(config: OpenAIConfig): assert params["reasoning_effort"] == "xhigh" -def test_gpt5_normalizes_reasoning_effort_dict_to_string(config: OpenAIConfig): - """Chat completion API expects reasoning_effort as a string, not a dict. - - Config/deployments may pass Responses API format: {'effort': 'high', 'summary': 'detailed'}. - """ +def test_gpt5_normalizes_reasoning_effort_dict_with_summary(config: OpenAIConfig): + """Dict with summary/generate_summary is normalized for chat completions.""" 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"] == {"effort": "high", "summary": "detailed"} + assert params["reasoning_effort"] == "high" def test_gpt5_xhigh_dict_triggers_validation(config: OpenAIConfig): @@ -361,7 +358,7 @@ def test_gpt5_xhigh_dict_accepted_for_supported_model(config: OpenAIConfig): model="gpt-5.4", drop_params=False, ) - assert params["reasoning_effort"] == {"effort": "xhigh", "summary": "detailed"} + assert params["reasoning_effort"] == "xhigh" def test_gpt5_none_dict_with_tools_no_tool_drop(config: OpenAIConfig): @@ -377,7 +374,7 @@ def test_gpt5_none_dict_with_tools_no_tool_drop(config: OpenAIConfig): model="gpt-5.4", drop_params=False, ) - assert params["reasoning_effort"] == {"effort": "none", "summary": "detailed"} + assert params["reasoning_effort"] == "none" assert params["tools"] == tools @@ -397,13 +394,13 @@ def test_gpt5_none_dict_with_sampling_params_allowed(config: OpenAIConfig): model="gpt-5.1", drop_params=False, ) - assert params["reasoning_effort"] == {"effort": "none", "summary": "detailed"} + assert params["reasoning_effort"] == "none" assert params["logprobs"] is True assert params["top_p"] == 0.9 -def test_gpt5_preserves_reasoning_effort_dict_with_summary_from_optional_params(config: OpenAIConfig): - """reasoning_effort dict with summary in optional_params is preserved.""" +def test_gpt5_normalizes_reasoning_effort_dict_with_summary_from_optional_params(config: OpenAIConfig): + """reasoning_effort dict with summary in optional_params is normalized.""" params = config.map_openai_params( non_default_params={}, optional_params={"reasoning_effort": {"effort": "medium", "summary": "detailed"}}, diff --git a/tests/test_litellm/test_main.py b/tests/test_litellm/test_main.py index ab0a18490ea..6ac988b2c21 100644 --- a/tests/test_litellm/test_main.py +++ b/tests/test_litellm/test_main.py @@ -678,6 +678,43 @@ def test_responses_api_bridge_check_gpt_5_4_tools_without_reasoning_stays_chat() 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 From 408b717cb9fee5551cf27a2ba45f17d308cb845a Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Fri, 13 Mar 2026 23:43:03 +0530 Subject: [PATCH 4/4] Fix gpt 5 transformation tests --- .../llms/openai/test_gpt5_transformation.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/test_litellm/llms/openai/test_gpt5_transformation.py b/tests/test_litellm/llms/openai/test_gpt5_transformation.py index 8eeca8c1d7f..47ae3c44c9e 100644 --- a/tests/test_litellm/llms/openai/test_gpt5_transformation.py +++ b/tests/test_litellm/llms/openai/test_gpt5_transformation.py @@ -410,8 +410,12 @@ def test_gpt5_normalizes_reasoning_effort_dict_with_summary_from_optional_params assert params["reasoning_effort"] == "medium" -def test_gpt5_4_drops_reasoning_effort_when_tools_present(config: OpenAIConfig): - """gpt-5.4: function calls not supported with reasoning_effort != 'none'. Drop reasoning_effort.""" +def test_gpt5_4_passes_through_reasoning_effort_with_tools(config: OpenAIConfig): + """gpt-5.4 with tools + reasoning_effort: map_openai_params passes through both. + + Routing to Responses API (which supports tools + reasoning) happens at completion() + level (responses_api_bridge_check). See test_responses_api_bridge_check_gpt_5_4_tools_plus_reasoning_routes_to_responses. + """ tools = [{"type": "function", "function": {"name": "test", "description": "test"}}] params = config.map_openai_params( non_default_params={"reasoning_effort": "high", "tools": tools}, @@ -419,7 +423,7 @@ def test_gpt5_4_drops_reasoning_effort_when_tools_present(config: OpenAIConfig): model="gpt-5.4", drop_params=False, ) - assert "reasoning_effort" not in params + assert params["reasoning_effort"] == "high" assert params["tools"] == tools