Skip to content

fix(openai): route reasoningSummary for gpt-5.4+ chat without tools to Responses API#27618

Merged
Sameerlite merged 11 commits into
litellm_internal_stagingfrom
litellm_reasoning_summary_chat_bridge
May 11, 2026
Merged

fix(openai): route reasoningSummary for gpt-5.4+ chat without tools to Responses API#27618
Sameerlite merged 11 commits into
litellm_internal_stagingfrom
litellm_reasoning_summary_chat_bridge

Conversation

@Sameerlite

@Sameerlite Sameerlite commented May 11, 2026

Copy link
Copy Markdown
Collaborator

Problem

Chat completions requests with reasoning_effort + reasoningSummary (AI SDK) but without tools stayed on OpenAI Chat Completions, which rejects reasoningSummary (often nested under extra_body). The same payload with tools worked because LiteLLM already bridged gpt-5.4+ tool+reasoning calls to the Responses API.
Fixes LIT-2929

Change

  • Extend responses_api_bridge_check to bridge gpt-5.4+ (OpenAI/Azure) when reasoning_effort is set and either tools or reasoning-summary aliases are present.
  • Peek/strip reasoningSummary / reasoning_summary from top-level and extra_body (utils), merge into reasoning_effort for the Responses bridge.
  • OpenAIGPT5Config: static helper delegating to strip_reasoning_summary_aliases_from_openai_completion_params so non-bridged chat calls do not forward invalid keys.

Tests

  • tests/test_litellm/test_main.py: bridge check + merge without tools; existing bridge tests unchanged.
  • poetry run pytest tests/test_litellm/test_main.py -k 'responses_api_bridge_check or gpt_5_4_responses_bridge'
  • poetry run pytest tests/test_litellm/llms/openai/test_gpt5_transformation.py

I copied the request body from opencode, and then created a small reproducible req for test.
Before
image

After
image

Opencode
image

Curl inside litellm
image


Note

Medium Risk
Changes routing criteria for OpenAI/Azure GPT-5 chat completions and mutates optional_params to merge/strip reasoning-summary fields, which could affect request behavior for GPT-5 users if edge cases are missed.

Overview
Fixes GPT-5 chat completion failures when callers pass AI-SDK-style reasoningSummary/reasoning_summary (often under extra_body) by detecting these aliases and routing eligible OpenAI/Azure GPT-5 requests to the Responses API even without tools.

Adds helpers to peek and strip reasoning-summary aliases from optional_params, and when bridged, merges the summary value into reasoning_effort (creating/augmenting the dict form) while ensuring non-bridged GPT-5 chat calls do not forward Responses-only keys.

Updates/extends tests to cover the new bridge conditions, falsy alias values, alias stripping, and stabilizes a GitHub Copilot completion test against env/module reload differences.

Reviewed by Cursor Bugbot for commit 79618b1. Bugbot is set up for automated code reviews on this repo. Configure here.

… 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 <cursoragent@cursor.com>
@codecov

codecov Bot commented May 11, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@greptile-apps

greptile-apps Bot commented May 11, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes an issue where chat completion requests using reasoning_effort + reasoningSummary (AI SDK convention) without tools were rejected by OpenAI because they were sent to the Chat Completions API instead of the Responses API. The fix extends the bridge-routing logic to cover this case and strips those aliases from non-bridged calls.

  • responses_api_bridge_check now bridges gpt-5.4+ when reasoning_effort is set and either tools or a reasoning-summary alias is present, and also bridges bare gpt-5 / gpt-5.1 / gpt-5.3 when reasoning_effort + a summary alias are both set.
  • peek_reasoning_summary_aliases and strip_reasoning_summary_aliases_from_optional_params are added to utils.py to read and remove reasoningSummary / reasoning_summary aliases from top-level params and extra_body, using explicit in/is None membership checks to safely handle falsy values.
  • New mocked tests cover the no-tools bridge path, the alias-stripping merge into reasoning_effort, and the non-bridged chat stripping; no existing test assertions are changed.

Confidence Score: 5/5

The change is safe to merge; the new bridge paths are well-tested with mocks and the stripping logic correctly handles falsy aliases.

The core logic — peeking at aliases before the bridge check, stripping them before forwarding, and merging into the reasoning_effort dict — is covered by focused mocked tests. No existing tests are weakened, and the only concern is that the newly added is_model_gpt_5_model / is_model_gpt_5_search_model string checks in the bridge condition follow the same hardcoded-name pattern as pre-existing code rather than using the model cost map.

litellm/main.py — the bridge condition now has two additional hardcoded model-name guards

Important Files Changed

Filename Overview
litellm/main.py Extends responses_api_bridge_check to bridge gpt-5.4+ or bare gpt-5 when reasoning_summary aliases are present, and strips those aliases from non-bridged chat calls; new hardcoded is_model_gpt_5_model / is_model_gpt_5_search_model guards were added to the bridge condition.
litellm/utils.py Adds peek_reasoning_summary_aliases and strip_reasoning_summary_aliases_from_optional_params; both use explicit in/is None guards to correctly handle falsy values.
litellm/llms/openai/chat/gpt_5_transformation.py Trivial import formatting change only; no logic changes.
tests/test_litellm/llms/openai/test_gpt5_transformation.py Adds mocked unit tests for alias stripping and non-bridged chat stripping; no real network calls, no existing test assertions weakened.
tests/test_litellm/test_main.py Adds mocked tests for the new bridge paths (gpt-5.4+ without tools, bare gpt-5 with summary, gpt-5 with tools+summary); no pre-existing tests are modified.

Reviews (2): Last reviewed commit: "Fix greptile issue" | Re-trigger Greptile

Comment thread litellm/utils.py Outdated
Comment thread litellm/utils.py Outdated
Comment thread litellm/utils.py Outdated
Comment thread litellm/utils.py Outdated
Comment thread litellm/llms/openai/chat/gpt_5_transformation.py Outdated
@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
1 out of 2 committers have signed the CLA.

✅ Sameerlite
❌ cursoragent
You have signed the CLA already but the status is still pending? Let us recheck it.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Stripping reasoning summary aliases in map_openai_params is dead code
    • Moved non-bridged GPT-5 chat alias stripping to after the Responses bridge decision and removed the dead map_openai_params stripping helper.
Preview (0ac923c6b6)
diff --git a/litellm/llms/openai/chat/gpt_5_transformation.py b/litellm/llms/openai/chat/gpt_5_transformation.py
--- a/litellm/llms/openai/chat/gpt_5_transformation.py
+++ b/litellm/llms/openai/chat/gpt_5_transformation.py
@@ -3,7 +3,10 @@
 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,
+)
 
 from .gpt_transformation import OpenAIGPTConfig
 

diff --git a/litellm/main.py b/litellm/main.py
--- 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 @@
 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 @@
     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 @@
             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 @@
         ## 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 @@
                 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,
@@ -1669,6 +1696,16 @@
                 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
--- 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,47 @@
     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")
+    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):
+        rs = extra_body.get("reasoningSummary")
+        if rs is None:
+            rs = extra_body.get("reasoning_summary")
+        return rs
+    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)
+    snake_rs_val = op.pop("reasoning_summary", None)
+    if rs_val is 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_rs_val
+            if rs_val is None:
+                rs_val = eb_snake_rs_val
+        if eb:
+            op["extra_body"] = eb
+        else:
+            op.pop("extra_body", None)
+    return op, rs_val
+
+
 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
--- a/tests/test_litellm/llms/openai/test_gpt5_transformation.py
+++ b/tests/test_litellm/llms/openai/test_gpt5_transformation.py
@@ -1,10 +1,15 @@
 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
-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 +1012,77 @@
     assert "tools" not in params
 
 
+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 = {}
+
+    def fake_openai_completion(**kwargs):
+        captured_kwargs.update(kwargs)
+        return {}
+
+    monkeypatch.setattr(
+        litellm_main.openai_chat_completions,
+        "completion",
+        fake_openai_completion,
+    )
+
+    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():
+    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."""

diff --git a/tests/test_litellm/test_main.py b/tests/test_litellm/test_main.py
--- a/tests/test_litellm/test_main.py
+++ b/tests/test_litellm/test_main.py
@@ -757,6 +757,24 @@
     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 @@
     }
 
 
+@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

You can send follow-ups to the cloud agent here.

Comment thread litellm/llms/openai/chat/gpt_5_transformation.py Outdated
@Sameerlite

Copy link
Copy Markdown
Collaborator Author

@greptile-apps re review

@Sameerlite

Copy link
Copy Markdown
Collaborator Author

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Redundant inner condition can be simplified
    • Simplified the GPT-5 responses bridge condition to check reasoning summaries directly and call the 5.4+ helper only for tool-only bridging.
Preview (57ed2dad4b)
diff --git a/litellm/llms/openai/chat/gpt_5_transformation.py b/litellm/llms/openai/chat/gpt_5_transformation.py
--- a/litellm/llms/openai/chat/gpt_5_transformation.py
+++ b/litellm/llms/openai/chat/gpt_5_transformation.py
@@ -3,7 +3,10 @@
 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,
+)
 
 from .gpt_transformation import OpenAIGPTConfig
 

diff --git a/litellm/main.py b/litellm/main.py
--- 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 @@
 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 @@
     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,14 +991,23 @@
             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 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 tools
+        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 model_info.get("mode") != "responses"
+        and (
+            reasoning_summary is not None
+            or (OpenAIGPT5Config.is_model_gpt_5_4_plus_model(model) and tools)
+        )
     ):
         model_info["mode"] = "responses"
         model = model.replace("responses/", "")
@@ -1634,8 +1652,10 @@
         ## 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 +1663,27 @@
                 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,
@@ -1669,6 +1702,16 @@
                 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
--- 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,49 @@
     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.
+
+    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):
+        if "reasoningSummary" in extra_body:
+            return extra_body["reasoningSummary"]
+        if "reasoning_summary" in extra_body:
+            return extra_body["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)
+    snake_rs_val = op.pop("reasoning_summary", None)
+    if rs_val is 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_rs_val
+            if rs_val is None:
+                rs_val = eb_snake_rs_val
+        if eb:
+            op["extra_body"] = eb
+        else:
+            op.pop("extra_body", None)
+    return op, rs_val
+
+
 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
--- a/tests/test_litellm/llms/openai/test_gpt5_transformation.py
+++ b/tests/test_litellm/llms/openai/test_gpt5_transformation.py
@@ -1,10 +1,15 @@
 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
-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 +1012,77 @@
     assert "tools" not in params
 
 
+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 = {}
+
+    def fake_openai_completion(**kwargs):
+        captured_kwargs.update(kwargs)
+        return {}
+
+    monkeypatch.setattr(
+        litellm_main.openai_chat_completions,
+        "completion",
+        fake_openai_completion,
+    )
+
+    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():
+    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."""

diff --git a/tests/test_litellm/test_main.py b/tests/test_litellm/test_main.py
--- a/tests/test_litellm/test_main.py
+++ b/tests/test_litellm/test_main.py
@@ -757,6 +757,60 @@
     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"
+
+
+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,
@@ -794,6 +848,69 @@
     }
 
 
+@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
+
+
+@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

You can send follow-ups to the cloud agent here.

Comment thread litellm/main.py
@Sameerlite

Copy link
Copy Markdown
Collaborator Author

bugbot run

Comment thread tests/test_litellm/llms/openai/test_gpt5_transformation.py
@Sameerlite

Copy link
Copy Markdown
Collaborator Author

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 1628886. Configure here.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Reasoning summary silently dropped when effort is missing
    • Added a fallback that preserves a stripped reasoning summary as reasoning_effort summary when no effort value is provided, with regression coverage.
Preview (b1508161ec)
diff --git a/litellm/llms/openai/chat/gpt_5_transformation.py b/litellm/llms/openai/chat/gpt_5_transformation.py
--- a/litellm/llms/openai/chat/gpt_5_transformation.py
+++ b/litellm/llms/openai/chat/gpt_5_transformation.py
@@ -3,7 +3,10 @@
 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,
+)
 
 from .gpt_transformation import OpenAIGPTConfig
 

diff --git a/litellm/main.py b/litellm/main.py
--- 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 @@
 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 @@
     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,14 +991,23 @@
             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 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 tools
+        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 model_info.get("mode") != "responses"
+        and (
+            reasoning_summary is not None
+            or (OpenAIGPT5Config.is_model_gpt_5_4_plus_model(model) and tools)
+        )
     ):
         model_info["mode"] = "responses"
         model = model.replace("responses/", "")
@@ -1634,8 +1652,10 @@
         ## 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 +1663,29 @@
                 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,
+                    }
+                else:
+                    optional_params["reasoning_effort"] = {"summary": rs_val}
 
             return responses_api_bridge.completion(
                 model=model,
@@ -1669,6 +1704,16 @@
                 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
--- 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,49 @@
     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.
+
+    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):
+        if "reasoningSummary" in extra_body:
+            return extra_body["reasoningSummary"]
+        if "reasoning_summary" in extra_body:
+            return extra_body["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)
+    snake_rs_val = op.pop("reasoning_summary", None)
+    if rs_val is 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_rs_val
+            if rs_val is None:
+                rs_val = eb_snake_rs_val
+        if eb:
+            op["extra_body"] = eb
+        else:
+            op.pop("extra_body", None)
+    return op, rs_val
+
+
 def get_non_default_transcription_params(kwargs: dict) -> dict:
     from litellm.constants import OPENAI_TRANSCRIPTION_PARAMS
 

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
--- 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 @@
 
 
 @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 @@
 
     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
 
 

diff --git a/tests/test_litellm/llms/openai/test_gpt5_transformation.py b/tests/test_litellm/llms/openai/test_gpt5_transformation.py
--- a/tests/test_litellm/llms/openai/test_gpt5_transformation.py
+++ b/tests/test_litellm/llms/openai/test_gpt5_transformation.py
@@ -1,10 +1,15 @@
 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
-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 +1012,76 @@
     assert "tools" not in params
 
 
+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 = {}
+
+    def fake_openai_completion(**kwargs):
+        captured_kwargs.update(kwargs)
+        return {}
+
+    monkeypatch.setattr(
+        litellm_main.openai_chat_completions,
+        "completion",
+        fake_openai_completion,
+    )
+
+    litellm.completion(
+        model="gpt-5",
+        messages=[{"role": "user", "content": "ok"}],
+        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():
+    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."""

diff --git a/tests/test_litellm/test_main.py b/tests/test_litellm/test_main.py
--- a/tests/test_litellm/test_main.py
+++ b/tests/test_litellm/test_main.py
@@ -757,6 +757,60 @@
     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"
+
+
+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,
@@ -794,6 +848,93 @@
     }
 
 
+@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
+
+
+@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,
+):
+    """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

You can send follow-ups to the cloud agent here.

Comment thread litellm/main.py
@Sameerlite

Copy link
Copy Markdown
Collaborator Author

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit b150816. Configure here.

@mateo-berri mateo-berri left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM; thanks!

@Sameerlite Sameerlite merged commit 5833d3e into litellm_internal_staging May 11, 2026
117 checks passed
@Sameerlite Sameerlite deleted the litellm_reasoning_summary_chat_bridge branch May 11, 2026 18:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants