diff --git a/litellm/main.py b/litellm/main.py index 9491550380..3fc1f2d4dd 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -991,17 +991,29 @@ def responses_api_bridge_check( mode = "responses" model_info["mode"] = mode - # OpenAI/Azure gpt-5.4+ chat-completions calls that need Responses-only fields - # (e.g. reasoning summary) must be bridged. SDKs send ``reasoningSummary`` / - # ``reasoning_summary`` alongside ``reasoning_effort``; Chat Completions rejects - # those keys, so route when tools+reasoning_effort (original case) or when a - # reasoning summary is requested without tools. + # OpenAI/Azure GPT-5 chat-completions that need Responses-only fields (e.g. + # ``reasoningSummary`` in ``extra_body``) must be bridged; Chat Completions rejects + # those keys. + # + # - gpt-5.4+: tools + reasoning_effort (original) or any reasoning-summary alias. + # - Older GPT-5 names (e.g. ``gpt-5``, ``gpt-5.1``): bridge only when a reasoning + # summary alias is present with ``reasoning_effort`` (tools alone stay on chat). if ( custom_llm_provider in ("openai", "azure") - and OpenAIGPT5Config.is_model_gpt_5_4_plus_model(model) - and reasoning_effort is not None - and (tools or reasoning_summary is not None) and model_info.get("mode") != "responses" + and OpenAIGPT5Config.is_model_gpt_5_model(model) + and not OpenAIGPT5Config.is_model_gpt_5_search_model(model) + and reasoning_effort is not None + and ( + ( + OpenAIGPT5Config.is_model_gpt_5_4_plus_model(model) + and (tools or reasoning_summary is not None) + ) + or ( + not OpenAIGPT5Config.is_model_gpt_5_4_plus_model(model) + and reasoning_summary is not None + ) + ) ): model_info["mode"] = "responses" model = model.replace("responses/", "") diff --git a/tests/test_litellm/test_main.py b/tests/test_litellm/test_main.py index b4c2d0b9df..c37f9fc26b 100644 --- a/tests/test_litellm/test_main.py +++ b/tests/test_litellm/test_main.py @@ -775,6 +775,42 @@ def test_responses_api_bridge_check_gpt_5_4_reasoning_summary_without_tools_rout assert model_info.get("mode") == "responses" +def test_responses_api_bridge_check_gpt_5_reasoning_summary_routes_to_responses(): + """Bare ``gpt-5`` with reasoning_effort + reasoningSummary should bridge (not 5.4+).""" + 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", + custom_llm_provider="openai", + tools=None, + reasoning_effort="medium", + reasoning_summary="auto", + ) + + assert model == "gpt-5" + assert model_info.get("mode") == "responses" + + +def test_responses_api_bridge_check_gpt_5_tools_without_summary_stays_chat(): + """gpt-5 with tools + reasoning_effort but no summary should stay on chat.""" + 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", + custom_llm_provider="openai", + tools=[{"type": "function", "function": {"name": "get_capital"}}], + reasoning_effort="medium", + reasoning_summary=None, + ) + + assert model == "gpt-5" + 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, @@ -839,6 +875,42 @@ def test_gpt_5_4_responses_bridge_merges_reasoning_summary_kwarg_without_tools( assert "reasoning_summary" not in optional_params +@patch("litellm.completion_extras.responses_api_bridge.completion") +def test_gpt_5_responses_bridge_tools_and_reasoning_summary( + mock_responses_completion, +): + """Bare gpt-5 with tools + reasoningSummary should bridge (OpenCode-style).""" + mock_responses_completion.return_value = MagicMock() + + import litellm + + litellm.completion( + model="gpt-5", + messages=[{"role": "user", "content": "ok"}], + tools=[ + { + "type": "function", + "function": { + "name": "apply_patch", + "parameters": {"type": "object", "properties": {}}, + }, + } + ], + tool_choice="auto", + reasoning_effort="medium", + reasoningSummary="auto", + stream=True, + api_key="fake-key", + ) + + assert mock_responses_completion.called is True + optional_params = mock_responses_completion.call_args.kwargs["optional_params"] + assert optional_params.get("reasoning_effort") == { + "effort": "medium", + "summary": "auto", + } + + 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