mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-26 06:03:19 +00:00
fix(openai): bridge gpt-5.6+ tools to /v1/responses without reasoning_effort
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
583ddaf199
commit
5369fe101a
4 changed files with 136 additions and 6 deletions
|
|
@ -102,18 +102,33 @@ class OpenAIGPT5Config(OpenAIGPTConfig):
|
|||
return model_name.startswith("gpt-5.4")
|
||||
|
||||
@classmethod
|
||||
def is_model_gpt_5_4_plus_model(cls, model: str) -> bool:
|
||||
"""Check if the model is gpt-5.4 or newer (5.4, 5.5, 5.6, etc., including pro)."""
|
||||
def _gpt_5_minor_version_at_least(cls, model: str, minimum: int) -> bool:
|
||||
"""Return True when a versioned gpt-5.<minor> name is at or above ``minimum``.
|
||||
|
||||
Non-versioned names (e.g. ``gpt-5``, ``gpt-5-codex``) and unparseable names
|
||||
return False. Named or pro variants such as ``gpt-5.6-sol`` and ``gpt-5.4-pro``
|
||||
are handled by taking the minor version before the first ``-``.
|
||||
"""
|
||||
model_name = model.split("/")[-1]
|
||||
if not model_name.startswith("gpt-5."):
|
||||
return False
|
||||
try:
|
||||
version_str = model_name.replace("gpt-5.", "").split("-")[0]
|
||||
major = version_str.split(".")[0]
|
||||
return int(major) >= 4
|
||||
return int(major) >= minimum
|
||||
except (ValueError, IndexError):
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def is_model_gpt_5_4_plus_model(cls, model: str) -> bool:
|
||||
"""Check if the model is gpt-5.4 or newer (5.4, 5.5, 5.6, etc., including pro)."""
|
||||
return cls._gpt_5_minor_version_at_least(model, 4)
|
||||
|
||||
@classmethod
|
||||
def is_model_gpt_5_6_plus_model(cls, model: str) -> bool:
|
||||
"""Check if the model is gpt-5.6 or newer (5.6, 5.7, etc., including named/pro variants)."""
|
||||
return cls._gpt_5_minor_version_at_least(model, 6)
|
||||
|
||||
@classmethod
|
||||
def _supports_reasoning_effort_level(cls, model: str, level: str) -> bool:
|
||||
"""Check if the model supports a specific reasoning_effort level.
|
||||
|
|
|
|||
|
|
@ -1022,7 +1022,11 @@ def responses_api_bridge_check(
|
|||
# ``reasoningSummary`` in ``extra_body``) must be bridged; Chat Completions rejects
|
||||
# those keys.
|
||||
#
|
||||
# - gpt-5.4+: tools + reasoning_effort (original) or any reasoning-summary alias.
|
||||
# - gpt-5.6+: tools alone bridge, even without reasoning_effort. OpenAI applies a
|
||||
# default reasoning_effort server-side for this family, so tools on Chat
|
||||
# Completions are rejected outright (see issue #33221).
|
||||
# - gpt-5.4/5.5: tools + reasoning_effort, or any reasoning-summary alias. Tools
|
||||
# alone (no reasoning_effort) stay on Chat Completions.
|
||||
# - 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 (
|
||||
|
|
@ -1030,8 +1034,13 @@ def responses_api_bridge_check(
|
|||
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 (reasoning_summary is not None or (OpenAIGPT5Config.is_model_gpt_5_4_plus_model(model) and tools))
|
||||
and (
|
||||
(
|
||||
reasoning_effort is not None
|
||||
and (reasoning_summary is not None or (OpenAIGPT5Config.is_model_gpt_5_4_plus_model(model) and tools))
|
||||
)
|
||||
or (OpenAIGPT5Config.is_model_gpt_5_6_plus_model(model) and tools)
|
||||
)
|
||||
):
|
||||
model_info["mode"] = "responses"
|
||||
model = model.replace("responses/", "")
|
||||
|
|
|
|||
|
|
@ -155,6 +155,44 @@ class TestOpenAIGPT5ConfigIsModelGpt54PlusModel:
|
|||
), f"Expected '{model}' NOT to be classified as gpt-5.4-or-newer"
|
||||
|
||||
|
||||
# Models that are gpt-5.6 or newer. main.py bridges tools-only (no reasoning_effort)
|
||||
# requests to /v1/responses for exactly this set, so gpt-5.4/5.5 must land on the
|
||||
# False side while the gpt-5.6 family (including named variants) lands on True.
|
||||
GPT5_6_PLUS_MODELS = [
|
||||
"gpt-5.6",
|
||||
"gpt-5.6-sol",
|
||||
"gpt-5.6-terra",
|
||||
"gpt-5.6-luna",
|
||||
"openai/gpt-5.6-sol",
|
||||
]
|
||||
|
||||
GPT5_PRE_5_6_MODELS = [
|
||||
"gpt-5",
|
||||
"gpt-5.1",
|
||||
"gpt-5.3",
|
||||
"gpt-5.4",
|
||||
"gpt-5.4-pro",
|
||||
"gpt-5.5",
|
||||
"gpt-5.5-pro",
|
||||
"gpt-4o",
|
||||
]
|
||||
|
||||
|
||||
class TestOpenAIGPT5ConfigIsModelGpt56PlusModel:
|
||||
|
||||
@pytest.mark.parametrize("model", GPT5_6_PLUS_MODELS)
|
||||
def test_gpt5_6_plus_models_are_classified_as_5_6_plus(self, model: str):
|
||||
assert OpenAIGPT5Config.is_model_gpt_5_6_plus_model(
|
||||
model
|
||||
), f"Expected '{model}' to be classified as gpt-5.6-or-newer"
|
||||
|
||||
@pytest.mark.parametrize("model", GPT5_PRE_5_6_MODELS)
|
||||
def test_pre_5_6_models_are_not_classified_as_5_6_plus(self, model: str):
|
||||
assert not OpenAIGPT5Config.is_model_gpt_5_6_plus_model(
|
||||
model
|
||||
), f"Expected '{model}' NOT to be classified as gpt-5.6-or-newer"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# AzureOpenAIGPT5Config
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -898,6 +898,74 @@ def test_responses_api_bridge_check_gpt_5_tools_without_summary_stays_chat():
|
|||
assert model_info.get("mode") != "responses"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"model",
|
||||
["gpt-5.6", "gpt-5.6-sol", "gpt-5.6-luna", "gpt-5.6-terra"],
|
||||
)
|
||||
@pytest.mark.parametrize("custom_llm_provider", ["openai", "azure"])
|
||||
def test_responses_api_bridge_check_gpt_5_6_tools_without_reasoning_routes_to_responses(
|
||||
model, custom_llm_provider
|
||||
):
|
||||
"""gpt-5.6+ with tools but no reasoning_effort must bridge to Responses API.
|
||||
|
||||
Regression test for https://github.com/BerriAI/litellm/issues/33221
|
||||
OpenAI applies a default reasoning_effort server-side for the gpt-5.6 family,
|
||||
so function tools on /v1/chat/completions are rejected with a 400.
|
||||
"""
|
||||
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, returned_model = responses_api_bridge_check(
|
||||
model=model,
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
tools=[{"type": "function", "function": {"name": "get_capital"}}],
|
||||
reasoning_effort=None,
|
||||
)
|
||||
|
||||
assert returned_model == model
|
||||
assert model_info.get("mode") == "responses"
|
||||
|
||||
|
||||
def test_responses_api_bridge_check_gpt_5_6_tools_plus_reasoning_routes_to_responses():
|
||||
"""gpt-5.6 with tools and explicit reasoning_effort still bridges (explicit-effort path)."""
|
||||
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.6-sol",
|
||||
custom_llm_provider="openai",
|
||||
tools=[{"type": "function", "function": {"name": "get_capital"}}],
|
||||
reasoning_effort="high",
|
||||
)
|
||||
|
||||
assert model == "gpt-5.6-sol"
|
||||
assert model_info.get("mode") == "responses"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model", ["gpt-5.4", "gpt-5.5", "gpt-5.5-pro"])
|
||||
def test_responses_api_bridge_check_gpt_5_4_and_5_5_tools_without_reasoning_stay_chat(model):
|
||||
"""gpt-5.4/5.5 tools-only (no reasoning_effort) must NOT bridge; only 5.6+ does.
|
||||
|
||||
Guards against widening the gpt-5.6 tools-only bridge back onto older families,
|
||||
which is the behavior the maintainer regression tests above pin down.
|
||||
"""
|
||||
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, returned_model = responses_api_bridge_check(
|
||||
model=model,
|
||||
custom_llm_provider="openai",
|
||||
tools=[{"type": "function", "function": {"name": "get_capital"}}],
|
||||
reasoning_effort=None,
|
||||
)
|
||||
|
||||
assert returned_model == model
|
||||
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,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue