fix(openai): route reasoningSummary for gpt-5.4+ chat without tools to Responses API#27618
Conversation
… 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 Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Greptile SummaryThis PR fixes an issue where chat completion requests using
Confidence Score: 5/5The 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 litellm/main.py — the bridge condition now has two additional hardcoded model-name guards
|
| 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
|
|
There was a problem hiding this comment.
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_checkYou can send follow-ups to the cloud agent here.
|
@greptile-apps re review |
|
bugbot run |
There was a problem hiding this comment.
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_checkYou can send follow-ups to the cloud agent here.
|
bugbot run |
|
bugbot run |
There was a problem hiding this comment.
✅ 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.
There was a problem hiding this comment.
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_checkYou can send follow-ups to the cloud agent here.
|
bugbot run |
There was a problem hiding this comment.
✅ 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.
Problem
Chat completions requests with
reasoning_effort+reasoningSummary(AI SDK) but withouttoolsstayed on OpenAI Chat Completions, which rejectsreasoningSummary(often nested underextra_body). The same payload withtoolsworked because LiteLLM already bridged gpt-5.4+ tool+reasoning calls to the Responses API.Fixes LIT-2929
Change
responses_api_bridge_checkto bridge gpt-5.4+ (OpenAI/Azure) whenreasoning_effortis set and eithertoolsor reasoning-summary aliases are present.reasoningSummary/reasoning_summaryfrom top-level andextra_body(utils), merge intoreasoning_effortfor the Responses bridge.OpenAIGPT5Config: static helper delegating tostrip_reasoning_summary_aliases_from_openai_completion_paramsso 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.pyI copied the request body from opencode, and then created a small reproducible req for test.

Before
After

Opencode

Curl inside litellm

Note
Medium Risk
Changes routing criteria for OpenAI/Azure GPT-5 chat completions and mutates
optional_paramsto 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 underextra_body) by detecting these aliases and routing eligible OpenAI/Azure GPT-5 requests to the Responses API even withouttools.Adds helpers to peek and strip reasoning-summary aliases from
optional_params, and when bridged, merges the summary value intoreasoning_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.