From 157d81368f62dbbc95b91da10587d01ce283156e Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Mon, 11 May 2026 10:42:19 +0530 Subject: [PATCH 01/10] fix(openai): route reasoningSummary on gpt-5.4+ chat without tools to Responses API - Extend responses_api_bridge_check when reasoning_effort + summary aliases (including nested extra_body) without tools - Merge summary into reasoning_effort for responses bridge; helpers in utils - Strip summary aliases in GPT-5 chat mapping when not bridged - Tests for bridge + merge behavior Co-authored-by: Cursor --- .../llms/openai/chat/gpt_5_transformation.py | 22 +++++++- litellm/main.py | 41 +++++++++++--- litellm/utils.py | 56 +++++++++++++++++++ tests/test_litellm/test_main.py | 45 +++++++++++++++ 4 files changed, 156 insertions(+), 8 deletions(-) diff --git a/litellm/llms/openai/chat/gpt_5_transformation.py b/litellm/llms/openai/chat/gpt_5_transformation.py index 4e34d10b18..eb7bb384a7 100644 --- a/litellm/llms/openai/chat/gpt_5_transformation.py +++ b/litellm/llms/openai/chat/gpt_5_transformation.py @@ -3,7 +3,11 @@ from typing import Optional, Union import litellm -from litellm.utils import _is_explicitly_disabled_factory, _supports_factory +from litellm.utils import ( + _is_explicitly_disabled_factory, + _supports_factory, + strip_reasoning_summary_aliases_from_openai_completion_params, +) from .gpt_transformation import OpenAIGPTConfig @@ -191,6 +195,16 @@ class OpenAIGPT5Config(OpenAIGPTConfig): if param not in non_supported_params ] + @staticmethod + def _strip_reasoning_summary_aliases_for_chat_completions( + non_default_params: dict, + optional_params: dict, + ) -> None: + """Remove Responses-style reasoning summary keys; invalid on Chat Completions.""" + strip_reasoning_summary_aliases_from_openai_completion_params( + non_default_params, optional_params + ) + def map_openai_params( self, non_default_params: dict, @@ -210,6 +224,12 @@ class OpenAIGPT5Config(OpenAIGPTConfig): drop_params=drop_params, ) + # AI SDK / Responses-style aliases; never valid on Chat Completions when not + # bridged to Responses API (see main.responses_api_bridge_check). + self._strip_reasoning_summary_aliases_for_chat_completions( + non_default_params, optional_params + ) + # Get raw reasoning_effort and effective effort level for all guards. # Use effective_effort (extracted string) for xhigh validation, "none" checks, and # tool/sampling guards — dict inputs like {"effort": "none", "summary": "detailed"} diff --git a/litellm/main.py b/litellm/main.py index 051a82fdd1..6a3e5db205 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -1,3 +1,5 @@ +# LiteLLM main module: public completion, embedding, streaming, and moderation entrypoints. +# # +-----------------------------------------------+ # | | # | Give Feedback / Get Help | @@ -59,7 +61,13 @@ import litellm from litellm import client # Other utils are imported directly to avoid circular imports -from litellm.utils import exception_type, get_litellm_params, get_optional_params +from litellm.utils import ( + exception_type, + get_litellm_params, + get_optional_params, + peek_reasoning_summary_aliases, + strip_reasoning_summary_aliases_from_optional_params, +) # Logging is imported lazily when needed to avoid loading litellm_logging at import time if TYPE_CHECKING: @@ -946,6 +954,7 @@ def responses_api_bridge_check( web_search_options: Optional[OpenAIWebSearchOptions] = None, tools: Optional[List[Any]] = None, reasoning_effort: Optional[Any] = None, + reasoning_summary: Optional[Any] = None, ) -> Tuple[dict, str]: model_info: Dict[str, Any] = {} @@ -982,13 +991,16 @@ def responses_api_bridge_check( mode = "responses" model_info["mode"] = mode - # OpenAI/Azure gpt-5.4+ chat-completions calls with both tools + reasoning_effort - # must be bridged to Responses API. + # 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. if ( custom_llm_provider in ("openai", "azure") and OpenAIGPT5Config.is_model_gpt_5_4_plus_model(model) - and tools and reasoning_effort is not None + and (tools or reasoning_summary is not None) and model_info.get("mode") != "responses" ): model_info["mode"] = "responses" @@ -1634,8 +1646,10 @@ def completion( # type: ignore # noqa: PLR0915 ## RESPONSES API BRIDGE LOGIC ## - check if model has 'mode: responses' in litellm.model_cost map # Only run the second bridge check if the first one didn't already # detect responses mode (e.g. via the "responses/" prefix). The second - # check handles cases like gpt-5.4+ with tools+reasoning_effort that - # the first (early) check doesn't cover. + # check handles cases like gpt-5.4+ with tools+reasoning_effort or + # reasoningSummary/reasoning_summary without tools (AI SDK) that the first + # (early) check doesn't cover. + _reasoning_summary_for_bridge = peek_reasoning_summary_aliases(optional_params) if responses_api_model_info.get("mode") != "responses": responses_api_model_info, model = responses_api_bridge_check( model=model, @@ -1643,14 +1657,27 @@ def completion( # type: ignore # noqa: PLR0915 web_search_options=web_search_options, tools=tools, reasoning_effort=reasoning_effort, + reasoning_summary=_reasoning_summary_for_bridge, ) if responses_api_model_info.get("mode") == "responses": from litellm.completion_extras import responses_api_bridge + optional_params, rs_val = ( + strip_reasoning_summary_aliases_from_optional_params(optional_params) + ) + if isinstance(reasoning_effort, dict) and "summary" in reasoning_effort: - optional_params = dict(optional_params) optional_params["reasoning_effort"] = reasoning_effort + elif rs_val is not None: + eff = optional_params.get("reasoning_effort", reasoning_effort) + if isinstance(eff, dict): + optional_params["reasoning_effort"] = {**eff, "summary": rs_val} + elif eff is not None: + optional_params["reasoning_effort"] = { + "effort": eff, + "summary": rs_val, + } return responses_api_bridge.completion( model=model, diff --git a/litellm/utils.py b/litellm/utils.py index 019fbc2add..d473446b18 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -1,3 +1,5 @@ +"""Utility helpers for LiteLLM core request handling and provider support.""" + # from __future__ import annotations must be the first non-comment statement from __future__ import annotations @@ -9490,6 +9492,60 @@ def get_non_default_completion_params(kwargs: dict) -> dict: return non_default_params +def peek_reasoning_summary_aliases(optional_params: dict) -> Optional[Any]: + """Read AI-SDK-style reasoning summary from optional_params or nested extra_body.""" + rs = optional_params.get("reasoningSummary") or optional_params.get( + "reasoning_summary" + ) + if rs is not None: + return rs + extra_body = optional_params.get("extra_body") + if isinstance(extra_body, dict): + return extra_body.get("reasoningSummary") or extra_body.get("reasoning_summary") + return None + + +def strip_reasoning_summary_aliases_from_optional_params( + optional_params: dict, +) -> Tuple[dict, Optional[Any]]: + """Copy optional_params; remove reasoningSummary aliases from top-level and extra_body.""" + op = dict(optional_params) + rs_val = op.pop("reasoningSummary", None) + if rs_val is None: + rs_val = op.pop("reasoning_summary", None) + eb = op.get("extra_body") + if isinstance(eb, dict): + eb = dict(eb) + if rs_val is None: + rs_val = eb.pop("reasoningSummary", None) or eb.pop( + "reasoning_summary", None + ) + else: + eb.pop("reasoningSummary", None) + eb.pop("reasoning_summary", None) + if eb: + op["extra_body"] = eb + else: + op.pop("extra_body", None) + return op, rs_val + + +def strip_reasoning_summary_aliases_from_openai_completion_params( + non_default_params: dict, + optional_params: dict, +) -> None: + """Drop AI-SDK reasoning summary keys from chat completion param dicts (in-place). + + These aliases are not valid on OpenAI Chat Completions and may appear on + ``non_default_params`` or ``optional_params`` (including nested ``extra_body``). + """ + non_default_params.pop("reasoningSummary", None) + non_default_params.pop("reasoning_summary", None) + stripped, _ = strip_reasoning_summary_aliases_from_optional_params(optional_params) + optional_params.clear() + optional_params.update(stripped) + + def get_non_default_transcription_params(kwargs: dict) -> dict: from litellm.constants import OPENAI_TRANSCRIPTION_PARAMS diff --git a/tests/test_litellm/test_main.py b/tests/test_litellm/test_main.py index 4358d0dc19..b4c2d0b9df 100644 --- a/tests/test_litellm/test_main.py +++ b/tests/test_litellm/test_main.py @@ -757,6 +757,24 @@ def test_responses_api_bridge_check_gpt_5_4_tools_without_reasoning_stays_chat() assert model_info.get("mode") != "responses" +def test_responses_api_bridge_check_gpt_5_4_reasoning_summary_without_tools_routes_to_responses(): + """gpt-5.4+ with reasoning_effort + reasoningSummary but no tools should bridge (AI SDK).""" + 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=None, + reasoning_effort="medium", + reasoning_summary="auto", + ) + + 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, @@ -794,6 +812,33 @@ def test_gpt_5_4_responses_bridge_preserves_reasoning_summary_dict( } +@patch("litellm.completion_extras.responses_api_bridge.completion") +def test_gpt_5_4_responses_bridge_merges_reasoning_summary_kwarg_without_tools( + mock_responses_completion, +): + """reasoningSummary without tools should route and merge into reasoning_effort dict.""" + mock_responses_completion.return_value = MagicMock() + + import litellm + + litellm.completion( + model="gpt-5.4", + messages=[{"role": "user", "content": "ok"}], + reasoning_effort="medium", + reasoningSummary="auto", + 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": "medium", + "summary": "auto", + } + assert "reasoningSummary" not in optional_params + assert "reasoning_summary" not in optional_params + + 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 eed6985cd647bc396b931d2a58ac17abe0e75679 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 11 May 2026 05:25:06 +0000 Subject: [PATCH 02/10] Fix reasoning summary alias stripping --- .../llms/openai/chat/gpt_5_transformation.py | 12 +-- litellm/utils.py | 25 ++++--- .../llms/openai/test_gpt5_transformation.py | 74 ++++++++++++++++++- 3 files changed, 93 insertions(+), 18 deletions(-) diff --git a/litellm/llms/openai/chat/gpt_5_transformation.py b/litellm/llms/openai/chat/gpt_5_transformation.py index eb7bb384a7..a665ef65e5 100644 --- a/litellm/llms/openai/chat/gpt_5_transformation.py +++ b/litellm/llms/openai/chat/gpt_5_transformation.py @@ -212,6 +212,12 @@ class OpenAIGPT5Config(OpenAIGPTConfig): model: str, drop_params: bool, ) -> dict: + # AI SDK / Responses-style aliases; never valid on Chat Completions when not + # bridged to Responses API (see main.responses_api_bridge_check). + self._strip_reasoning_summary_aliases_for_chat_completions( + non_default_params, optional_params + ) + if self.is_model_gpt_5_search_model(model): if "max_tokens" in non_default_params: optional_params["max_completion_tokens"] = non_default_params.pop( @@ -224,12 +230,6 @@ class OpenAIGPT5Config(OpenAIGPTConfig): drop_params=drop_params, ) - # AI SDK / Responses-style aliases; never valid on Chat Completions when not - # bridged to Responses API (see main.responses_api_bridge_check). - self._strip_reasoning_summary_aliases_for_chat_completions( - non_default_params, optional_params - ) - # Get raw reasoning_effort and effective effort level for all guards. # Use effective_effort (extracted string) for xhigh validation, "none" checks, and # tool/sampling guards — dict inputs like {"effort": "none", "summary": "detailed"} diff --git a/litellm/utils.py b/litellm/utils.py index d473446b18..f61a47c401 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -9494,14 +9494,17 @@ def get_non_default_completion_params(kwargs: dict) -> dict: def peek_reasoning_summary_aliases(optional_params: dict) -> Optional[Any]: """Read AI-SDK-style reasoning summary from optional_params or nested extra_body.""" - rs = optional_params.get("reasoningSummary") or optional_params.get( - "reasoning_summary" - ) + rs = optional_params.get("reasoningSummary") + if rs is None: + rs = optional_params.get("reasoning_summary") if rs is not None: return rs extra_body = optional_params.get("extra_body") if isinstance(extra_body, dict): - return extra_body.get("reasoningSummary") or extra_body.get("reasoning_summary") + rs = extra_body.get("reasoningSummary") + if rs is None: + rs = extra_body.get("reasoning_summary") + return rs return None @@ -9511,18 +9514,18 @@ def strip_reasoning_summary_aliases_from_optional_params( """Copy optional_params; remove reasoningSummary aliases from top-level and extra_body.""" op = dict(optional_params) rs_val = op.pop("reasoningSummary", None) + snake_rs_val = op.pop("reasoning_summary", None) if rs_val is None: - rs_val = op.pop("reasoning_summary", None) + rs_val = snake_rs_val eb = op.get("extra_body") if isinstance(eb, dict): eb = dict(eb) + eb_rs_val = eb.pop("reasoningSummary", None) + eb_snake_rs_val = eb.pop("reasoning_summary", None) if rs_val is None: - rs_val = eb.pop("reasoningSummary", None) or eb.pop( - "reasoning_summary", None - ) - else: - eb.pop("reasoningSummary", None) - eb.pop("reasoning_summary", None) + rs_val = eb_rs_val + if rs_val is None: + rs_val = eb_snake_rs_val if eb: op["extra_body"] = eb else: diff --git a/tests/test_litellm/llms/openai/test_gpt5_transformation.py b/tests/test_litellm/llms/openai/test_gpt5_transformation.py index ebf7681f2f..be079ee708 100644 --- a/tests/test_litellm/llms/openai/test_gpt5_transformation.py +++ b/tests/test_litellm/llms/openai/test_gpt5_transformation.py @@ -4,7 +4,11 @@ import litellm from litellm.litellm_core_utils.get_model_cost_map import get_model_cost_map from litellm.llms.openai.chat.gpt_5_transformation import OpenAIGPT5Config from litellm.llms.openai.openai import OpenAIConfig -from litellm.utils import _is_explicitly_disabled_factory +from litellm.utils import ( + _is_explicitly_disabled_factory, + peek_reasoning_summary_aliases, + strip_reasoning_summary_aliases_from_optional_params, +) @pytest.fixture() @@ -1007,6 +1011,74 @@ def test_gpt5_search_drops_unsupported_params(config: OpenAIConfig): assert "tools" not in params +def test_gpt5_search_strips_reasoning_summary_aliases(gpt5_config: OpenAIGPT5Config): + """Search models still strip Responses-only reasoning summary aliases.""" + non_default_params = { + "reasoningSummary": "auto", + "reasoning_summary": "ignored", + } + optional_params = { + "extra_body": { + "reasoningSummary": "auto", + "reasoning_summary": "ignored", + "metadata": "ok", + } + } + + params = gpt5_config.map_openai_params( + non_default_params=non_default_params, + optional_params=optional_params, + model="gpt-5-search-api", + drop_params=False, + ) + + assert "reasoningSummary" not in non_default_params + assert "reasoning_summary" not in non_default_params + assert "reasoningSummary" not in params + assert "reasoning_summary" not in params + assert params["extra_body"] == {"metadata": "ok"} + + +def test_reasoning_summary_alias_helpers_preserve_falsy_and_strip_all_aliases(): + optional_params = {"reasoningSummary": False, "reasoning_summary": "ignored"} + + assert peek_reasoning_summary_aliases(optional_params) is False + stripped, rs_val = strip_reasoning_summary_aliases_from_optional_params( + optional_params + ) + + assert rs_val is False + assert stripped == {} + + optional_params = { + "extra_body": {"reasoningSummary": False, "reasoning_summary": "ignored"} + } + + assert peek_reasoning_summary_aliases(optional_params) is False + stripped, rs_val = strip_reasoning_summary_aliases_from_optional_params( + optional_params + ) + + assert rs_val is False + assert stripped == {} + + optional_params = { + "extra_body": { + "reasoningSummary": "auto", + "reasoning_summary": "ignored", + "metadata": "ok", + } + } + + assert peek_reasoning_summary_aliases(optional_params) == "auto" + stripped, rs_val = strip_reasoning_summary_aliases_from_optional_params( + optional_params + ) + + assert rs_val == "auto" + assert stripped == {"extra_body": {"metadata": "ok"}} + + # GPT-5 unsupported params audit (validated via direct API calls) def test_gpt5_rejects_params_unsupported_by_openai(config: OpenAIConfig): """Params that OpenAI rejects for all GPT-5 reasoning models.""" From 0ac923c6b633e2ba274463ffa42fba84230bda35 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 11 May 2026 05:39:50 +0000 Subject: [PATCH 03/10] Fix GPT-5 reasoning summary alias stripping --- .../llms/openai/chat/gpt_5_transformation.py | 17 ------- litellm/main.py | 10 ++++ litellm/utils.py | 16 ------ .../llms/openai/test_gpt5_transformation.py | 50 ++++++++++--------- 4 files changed, 37 insertions(+), 56 deletions(-) diff --git a/litellm/llms/openai/chat/gpt_5_transformation.py b/litellm/llms/openai/chat/gpt_5_transformation.py index a665ef65e5..9ccb2e1c26 100644 --- a/litellm/llms/openai/chat/gpt_5_transformation.py +++ b/litellm/llms/openai/chat/gpt_5_transformation.py @@ -6,7 +6,6 @@ import litellm from litellm.utils import ( _is_explicitly_disabled_factory, _supports_factory, - strip_reasoning_summary_aliases_from_openai_completion_params, ) from .gpt_transformation import OpenAIGPTConfig @@ -195,16 +194,6 @@ class OpenAIGPT5Config(OpenAIGPTConfig): if param not in non_supported_params ] - @staticmethod - def _strip_reasoning_summary_aliases_for_chat_completions( - non_default_params: dict, - optional_params: dict, - ) -> None: - """Remove Responses-style reasoning summary keys; invalid on Chat Completions.""" - strip_reasoning_summary_aliases_from_openai_completion_params( - non_default_params, optional_params - ) - def map_openai_params( self, non_default_params: dict, @@ -212,12 +201,6 @@ class OpenAIGPT5Config(OpenAIGPTConfig): model: str, drop_params: bool, ) -> dict: - # AI SDK / Responses-style aliases; never valid on Chat Completions when not - # bridged to Responses API (see main.responses_api_bridge_check). - self._strip_reasoning_summary_aliases_for_chat_completions( - non_default_params, optional_params - ) - if self.is_model_gpt_5_search_model(model): if "max_tokens" in non_default_params: optional_params["max_completion_tokens"] = non_default_params.pop( diff --git a/litellm/main.py b/litellm/main.py index 6a3e5db205..9491550380 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -1696,6 +1696,16 @@ def completion( # type: ignore # noqa: PLR0915 encoding=_get_encoding(), stream=stream, ) + elif ( + custom_llm_provider == "openai" + and OpenAIGPT5Config.is_model_gpt_5_model(model) + ) or ( + custom_llm_provider == "azure" + and litellm.AzureOpenAIGPT5Config.is_model_gpt_5_model(model) + ): + optional_params, _ = strip_reasoning_summary_aliases_from_optional_params( + optional_params + ) if custom_llm_provider == "azure": # azure configs diff --git a/litellm/utils.py b/litellm/utils.py index f61a47c401..daac523400 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -9533,22 +9533,6 @@ def strip_reasoning_summary_aliases_from_optional_params( return op, rs_val -def strip_reasoning_summary_aliases_from_openai_completion_params( - non_default_params: dict, - optional_params: dict, -) -> None: - """Drop AI-SDK reasoning summary keys from chat completion param dicts (in-place). - - These aliases are not valid on OpenAI Chat Completions and may appear on - ``non_default_params`` or ``optional_params`` (including nested ``extra_body``). - """ - non_default_params.pop("reasoningSummary", None) - non_default_params.pop("reasoning_summary", None) - stripped, _ = strip_reasoning_summary_aliases_from_optional_params(optional_params) - optional_params.clear() - optional_params.update(stripped) - - def get_non_default_transcription_params(kwargs: dict) -> dict: from litellm.constants import OPENAI_TRANSCRIPTION_PARAMS diff --git a/tests/test_litellm/llms/openai/test_gpt5_transformation.py b/tests/test_litellm/llms/openai/test_gpt5_transformation.py index be079ee708..840f2c75fd 100644 --- a/tests/test_litellm/llms/openai/test_gpt5_transformation.py +++ b/tests/test_litellm/llms/openai/test_gpt5_transformation.py @@ -1,6 +1,7 @@ import pytest import litellm +import litellm.main as litellm_main from litellm.litellm_core_utils.get_model_cost_map import get_model_cost_map from litellm.llms.openai.chat.gpt_5_transformation import OpenAIGPT5Config from litellm.llms.openai.openai import OpenAIConfig @@ -1011,32 +1012,35 @@ def test_gpt5_search_drops_unsupported_params(config: OpenAIConfig): assert "tools" not in params -def test_gpt5_search_strips_reasoning_summary_aliases(gpt5_config: OpenAIGPT5Config): - """Search models still strip Responses-only reasoning summary aliases.""" - non_default_params = { - "reasoningSummary": "auto", - "reasoning_summary": "ignored", - } - optional_params = { - "extra_body": { - "reasoningSummary": "auto", - "reasoning_summary": "ignored", - "metadata": "ok", - } - } +def test_gpt5_chat_strips_reasoning_summary_aliases_after_bridge_check( + monkeypatch: pytest.MonkeyPatch, +): + """Non-bridged GPT-5 chat calls strip Responses-only reasoning summary aliases.""" + captured_kwargs = {} - params = gpt5_config.map_openai_params( - non_default_params=non_default_params, - optional_params=optional_params, - model="gpt-5-search-api", - drop_params=False, + def fake_openai_completion(**kwargs): + captured_kwargs.update(kwargs) + return {} + + monkeypatch.setattr( + litellm_main.openai_chat_completions, + "completion", + fake_openai_completion, ) - assert "reasoningSummary" not in non_default_params - assert "reasoning_summary" not in non_default_params - assert "reasoningSummary" not in params - assert "reasoning_summary" not in params - assert params["extra_body"] == {"metadata": "ok"} + litellm.completion( + model="gpt-5", + messages=[{"role": "user", "content": "ok"}], + reasoning_effort="medium", + reasoningSummary="auto", + extra_body={"reasoning_summary": "ignored", "metadata": "ok"}, + api_key="fake-key", + ) + + optional_params = captured_kwargs["optional_params"] + assert "reasoningSummary" not in optional_params + assert "reasoning_summary" not in optional_params + assert optional_params["extra_body"] == {"metadata": "ok"} def test_reasoning_summary_alias_helpers_preserve_falsy_and_strip_all_aliases(): From 22e9fd12dfd46a7ee3eabf09a98556bc606614af Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Mon, 11 May 2026 11:12:24 +0530 Subject: [PATCH 04/10] Fix reasoningSummary for gpt-5 series as well --- litellm/main.py | 28 +++++++++---- tests/test_litellm/test_main.py | 72 +++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 8 deletions(-) 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 From e74329db303ab13ff19e8335ccbbbe000bf6f0b1 Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Mon, 11 May 2026 11:13:40 +0530 Subject: [PATCH 05/10] Fix greptile issue --- litellm/utils.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/litellm/utils.py b/litellm/utils.py index daac523400..80452e533b 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -9493,18 +9493,20 @@ def get_non_default_completion_params(kwargs: dict) -> dict: def peek_reasoning_summary_aliases(optional_params: dict) -> Optional[Any]: - """Read AI-SDK-style reasoning summary from optional_params or nested extra_body.""" - rs = optional_params.get("reasoningSummary") - if rs is None: - rs = optional_params.get("reasoning_summary") - if rs is not None: - return rs + """Read AI-SDK-style reasoning summary from optional_params or nested extra_body. + + Uses key membership (not ``or`` chains) so falsy values like ``""`` are not skipped. + """ + if "reasoningSummary" in optional_params: + return optional_params["reasoningSummary"] + if "reasoning_summary" in optional_params: + return optional_params["reasoning_summary"] extra_body = optional_params.get("extra_body") if isinstance(extra_body, dict): - rs = extra_body.get("reasoningSummary") - if rs is None: - rs = extra_body.get("reasoning_summary") - return rs + if "reasoningSummary" in extra_body: + return extra_body["reasoningSummary"] + if "reasoning_summary" in extra_body: + return extra_body["reasoning_summary"] return None From 57ed2dad4bdacc982c069b8a83aa1c19ce6c47d5 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 11 May 2026 05:51:51 +0000 Subject: [PATCH 06/10] Simplify GPT-5 responses bridge condition --- litellm/main.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/litellm/main.py b/litellm/main.py index 3fc1f2d4dd..c324f982b8 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -1005,14 +1005,8 @@ def responses_api_bridge_check( 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 - ) + reasoning_summary is not None + or (OpenAIGPT5Config.is_model_gpt_5_4_plus_model(model) and tools) ) ): model_info["mode"] = "responses" From 1628886f4a3584903c2d39beb4d079a6b51bf1e5 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 11 May 2026 06:01:35 +0000 Subject: [PATCH 07/10] Fix GPT-5 reasoning summary strip test path --- tests/test_litellm/llms/openai/test_gpt5_transformation.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_litellm/llms/openai/test_gpt5_transformation.py b/tests/test_litellm/llms/openai/test_gpt5_transformation.py index 840f2c75fd..d279b119ef 100644 --- a/tests/test_litellm/llms/openai/test_gpt5_transformation.py +++ b/tests/test_litellm/llms/openai/test_gpt5_transformation.py @@ -1031,7 +1031,6 @@ def test_gpt5_chat_strips_reasoning_summary_aliases_after_bridge_check( litellm.completion( model="gpt-5", messages=[{"role": "user", "content": "ok"}], - reasoning_effort="medium", reasoningSummary="auto", extra_body={"reasoning_summary": "ignored", "metadata": "ok"}, api_key="fake-key", From 7524c4022e49f79aa0d4440d10e0212da9e74de8 Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Mon, 11 May 2026 18:56:48 +0530 Subject: [PATCH 08/10] dummy change --- litellm/llms/openai/chat/gpt_5_transformation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/litellm/llms/openai/chat/gpt_5_transformation.py b/litellm/llms/openai/chat/gpt_5_transformation.py index 9ccb2e1c26..12a2d64ee6 100644 --- a/litellm/llms/openai/chat/gpt_5_transformation.py +++ b/litellm/llms/openai/chat/gpt_5_transformation.py @@ -7,7 +7,7 @@ from litellm.utils import ( _is_explicitly_disabled_factory, _supports_factory, ) - + from .gpt_transformation import OpenAIGPTConfig From aa1f57fff85c01b541b2fac17210fa579361fb5c Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Mon, 11 May 2026 20:41:10 +0530 Subject: [PATCH 09/10] fix black and github mock test --- .../llms/openai/chat/gpt_5_transformation.py | 2 +- .../test_github_copilot_transformation.py | 33 ++++++++++--------- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/litellm/llms/openai/chat/gpt_5_transformation.py b/litellm/llms/openai/chat/gpt_5_transformation.py index 12a2d64ee6..9ccb2e1c26 100644 --- a/litellm/llms/openai/chat/gpt_5_transformation.py +++ b/litellm/llms/openai/chat/gpt_5_transformation.py @@ -7,7 +7,7 @@ from litellm.utils import ( _is_explicitly_disabled_factory, _supports_factory, ) - + from .gpt_transformation import OpenAIGPTConfig diff --git a/tests/test_litellm/llms/github_copilot/test_github_copilot_transformation.py b/tests/test_litellm/llms/github_copilot/test_github_copilot_transformation.py index 678aa6b56c..45ce5d5840 100644 --- a/tests/test_litellm/llms/github_copilot/test_github_copilot_transformation.py +++ b/tests/test_litellm/llms/github_copilot/test_github_copilot_transformation.py @@ -94,27 +94,33 @@ def test_github_copilot_config_get_openai_compatible_provider_info(): @patch("litellm.llms.github_copilot.authenticator.Authenticator.get_api_key") +@patch("litellm.main.openai_chat_completions.completion") @patch("litellm.llms.openai.openai.OpenAIChatCompletion.completion") -def test_completion_github_copilot_mock_response(mock_completion, mock_get_api_key): +def test_completion_github_copilot_mock_response( + mock_class_completion, mock_instance_completion, mock_get_api_key, monkeypatch +): """Test the completion function with GitHub Copilot provider.""" - # Mock the API key return value + # Force chat path through the patched openai_chat_completions instance even if + # a previous test left EXPERIMENTAL_OPENAI_BASE_LLM_HTTP_HANDLER set in the env. + monkeypatch.delenv("EXPERIMENTAL_OPENAI_BASE_LLM_HTTP_HANDLER", raising=False) + mock_api_key = "gh.test-key-123456789" mock_get_api_key.return_value = mock_api_key - # Mock completion response mock_response = MagicMock() mock_response.choices = [MagicMock()] mock_response.choices[0].message.content = "Hello, I'm GitHub Copilot!" - mock_completion.return_value = mock_response + # Patch both the class method and the live module-level instance to survive + # conftest module reloads that can swap which class object is in use. + mock_class_completion.return_value = mock_response + mock_instance_completion.return_value = mock_response - # Test non-streaming completion messages = [ {"role": "system", "content": "You're GitHub Copilot, an AI assistant."}, {"role": "user", "content": "Hello, who are you?"}, ] - # Create a properly formatted headers dictionary headers = { "editor-version": "Neovim/0.9.0", "Copilot-Integration-Id": "vscode-chat", @@ -128,19 +134,16 @@ def test_completion_github_copilot_mock_response(mock_completion, mock_get_api_k assert response is not None - # Verify the get_api_key call was made (can be called multiple times) assert mock_get_api_key.call_count >= 1 - # Verify the completion call was made with the expected params - mock_completion.assert_called_once() - args, kwargs = mock_completion.call_args + # Exactly one of the two patched targets should have been used. + invoked = [m for m in (mock_class_completion, mock_instance_completion) if m.called] + assert len(invoked) == 1 + invoked[0].assert_called_once() + _, kwargs = invoked[0].call_args - # Check that the proper authorization header is set assert "headers" in kwargs - # Check that the model name is correctly formatted - assert ( - kwargs.get("model") == "gpt-4" - ) # Model name should be without provider prefix + assert kwargs.get("model") == "gpt-4" assert kwargs.get("messages") == messages From b1508161ecb5bd2695fb677101cca8755d489bcd Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 11 May 2026 15:25:40 +0000 Subject: [PATCH 10/10] Preserve reasoning summary without effort --- litellm/main.py | 2 ++ tests/test_litellm/test_main.py | 24 ++++++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/litellm/main.py b/litellm/main.py index c324f982b8..29a9ffc84f 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -1684,6 +1684,8 @@ def completion( # type: ignore # noqa: PLR0915 "effort": eff, "summary": rs_val, } + else: + optional_params["reasoning_effort"] = {"summary": rs_val} return responses_api_bridge.completion( model=model, diff --git a/tests/test_litellm/test_main.py b/tests/test_litellm/test_main.py index c37f9fc26b..76336a91fc 100644 --- a/tests/test_litellm/test_main.py +++ b/tests/test_litellm/test_main.py @@ -875,6 +875,30 @@ 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_responses_bridge_preserves_reasoning_summary_without_effort( + mock_responses_completion, +): + """Reasoning summary should survive responses routing even without effort.""" + mock_responses_completion.return_value = MagicMock() + + import litellm + + with patch.object(litellm, "route_all_chat_openai_to_responses", True): + litellm.completion( + model="gpt-4o", + messages=[{"role": "user", "content": "ok"}], + reasoningSummary="auto", + 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"] == {"summary": "auto"} + assert "reasoningSummary" not in optional_params + 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,