Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions litellm/llms/openai/chat/gpt_5_transformation.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,18 +102,33 @@ def is_model_gpt_5_4_model(cls, model: str) -> bool:
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.
Expand Down
18 changes: 15 additions & 3 deletions litellm/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -1023,16 +1023,28 @@ 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 BerriAI/litellm#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 (
custom_llm_provider in ("openai", "azure")
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/", "")
Expand Down
38 changes: 38 additions & 0 deletions tests/test_litellm/llms/openai/test_is_model_gpt_5_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,44 @@ def test_pre_5_4_models_are_not_classified_as_5_4_plus(self, model: str):
), 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
# ---------------------------------------------------------------------------
Expand Down
66 changes: 66 additions & 0 deletions tests/test_litellm/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -844,6 +844,72 @@ def test_responses_api_bridge_check_gpt_5_4_tools_without_reasoning_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."""
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."""
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"


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
Expand Down
Loading