mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-07 08:26:10 +00:00
Merge pull request #23576 from Sameerlite/litellm_gpt54-tools-reasoning-routing
Litellm gpt54 tools reasoning routing
This commit is contained in:
commit
57397e0d26
5 changed files with 134 additions and 37 deletions
|
|
@ -188,11 +188,9 @@ class OpenAIGPT5Config(OpenAIGPTConfig):
|
|||
) or optional_params.get("reasoning_effort")
|
||||
effective_effort = _get_effort_level(raw_reasoning_effort)
|
||||
|
||||
# Normalize to string for Chat Completions API when dict has only "effort".
|
||||
# Preserve full dict (e.g. {"effort": "high", "summary": "detailed"}) for Responses API.
|
||||
if isinstance(raw_reasoning_effort, dict) and set(
|
||||
raw_reasoning_effort.keys()
|
||||
) <= {"effort"}:
|
||||
# Normalize dict reasoning_effort to string for Chat Completions API.
|
||||
# Example: {"effort": "high", "summary": "detailed"} -> "high"
|
||||
if isinstance(raw_reasoning_effort, dict) and "effort" in raw_reasoning_effort:
|
||||
normalized = _normalize_reasoning_effort_for_chat_completion(
|
||||
raw_reasoning_effort
|
||||
)
|
||||
|
|
@ -223,16 +221,6 @@ class OpenAIGPT5Config(OpenAIGPTConfig):
|
|||
"max_tokens"
|
||||
)
|
||||
|
||||
# gpt-5.4: reasoning_effort + tools is only supported in the Responses API
|
||||
# Drop reasoning_effort when tools are present in chat completions
|
||||
if self.is_model_gpt_5_4_model(model):
|
||||
has_tools = bool(
|
||||
non_default_params.get("tools") or optional_params.get("tools")
|
||||
)
|
||||
if has_tools and effective_effort is not None:
|
||||
non_default_params.pop("reasoning_effort", None)
|
||||
optional_params.pop("reasoning_effort", None)
|
||||
|
||||
# gpt-5.1/5.2 support logprobs, top_p, top_logprobs only when reasoning_effort="none"
|
||||
supports_none = self._supports_reasoning_effort_level(model, "none")
|
||||
if supports_none:
|
||||
|
|
|
|||
|
|
@ -99,6 +99,7 @@ from litellm.llms.base_llm.base_model_iterator import (
|
|||
from litellm.llms.bedrock.common_utils import BedrockModelInfo
|
||||
from litellm.llms.cohere.common_utils import CohereModelInfo
|
||||
from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler, HTTPHandler
|
||||
from litellm.llms.openai.chat.gpt_5_transformation import OpenAIGPT5Config
|
||||
from litellm.llms.openai_like.json_loader import JSONProviderRegistry
|
||||
from litellm.llms.vertex_ai.common_utils import (
|
||||
VertexAIModelRoute,
|
||||
|
|
@ -934,6 +935,8 @@ def responses_api_bridge_check(
|
|||
model: str,
|
||||
custom_llm_provider: str,
|
||||
web_search_options: Optional[OpenAIWebSearchOptions] = None,
|
||||
tools: Optional[List[Any]] = None,
|
||||
reasoning_effort: Optional[Any] = None,
|
||||
) -> Tuple[dict, str]:
|
||||
model_info: Dict[str, Any] = {}
|
||||
try:
|
||||
|
|
@ -951,6 +954,17 @@ def responses_api_bridge_check(
|
|||
if web_search_options is not None and custom_llm_provider == "xai":
|
||||
model_info["mode"] = "responses"
|
||||
model = model.replace("responses/", "")
|
||||
|
||||
# OpenAI gpt-5.4+ chat-completions calls with both tools + reasoning_effort
|
||||
# must be bridged to Responses API.
|
||||
if (
|
||||
custom_llm_provider == "openai"
|
||||
and OpenAIGPT5Config.is_model_gpt_5_4_plus_model(model)
|
||||
and tools
|
||||
and reasoning_effort is not None
|
||||
):
|
||||
model_info["mode"] = "responses"
|
||||
model = model.replace("responses/", "")
|
||||
except Exception as e:
|
||||
verbose_logger.debug("Error getting model info: {}".format(e))
|
||||
|
||||
|
|
@ -1596,11 +1610,17 @@ def completion( # type: ignore # noqa: PLR0915
|
|||
model=model,
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
web_search_options=web_search_options,
|
||||
tools=tools,
|
||||
reasoning_effort=reasoning_effort,
|
||||
)
|
||||
|
||||
if model_info.get("mode") == "responses":
|
||||
from litellm.completion_extras import responses_api_bridge
|
||||
|
||||
if isinstance(reasoning_effort, dict) and "summary" in reasoning_effort:
|
||||
optional_params = dict(optional_params)
|
||||
optional_params["reasoning_effort"] = reasoning_effort
|
||||
|
||||
return responses_api_bridge.completion(
|
||||
model=model,
|
||||
messages=messages,
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import pytest
|
|||
|
||||
sys.path.insert(0, os.path.abspath("../../../../.."))
|
||||
|
||||
from litellm.llms.openai.chat.gpt_5_transformation import OpenAIGPT5Config
|
||||
from litellm.llms.openai.chat.gpt_transformation import (
|
||||
OpenAIChatCompletionStreamingHandler,
|
||||
OpenAIGPTConfig,
|
||||
|
|
|
|||
|
|
@ -324,19 +324,15 @@ def test_gpt5_4_pro_allows_reasoning_effort_xhigh(config: OpenAIConfig):
|
|||
assert params["reasoning_effort"] == "xhigh"
|
||||
|
||||
|
||||
def test_gpt5_preserves_reasoning_effort_dict_with_summary(config: OpenAIConfig):
|
||||
"""Dict with summary/generate_summary is preserved for Responses API.
|
||||
|
||||
Config/deployments may pass Responses API format: {'effort': 'high', 'summary': 'detailed'}.
|
||||
We preserve the full dict so it reaches the Responses API transformation.
|
||||
"""
|
||||
def test_gpt5_normalizes_reasoning_effort_dict_with_summary(config: OpenAIConfig):
|
||||
"""Dict with summary/generate_summary is normalized for chat completions."""
|
||||
params = config.map_openai_params(
|
||||
non_default_params={"reasoning_effort": {"effort": "high", "summary": "detailed"}},
|
||||
optional_params={},
|
||||
model="gpt-5.4",
|
||||
drop_params=False,
|
||||
)
|
||||
assert params["reasoning_effort"] == {"effort": "high", "summary": "detailed"}
|
||||
assert params["reasoning_effort"] == "high"
|
||||
|
||||
|
||||
def test_gpt5_xhigh_dict_triggers_validation(config: OpenAIConfig):
|
||||
|
|
@ -362,14 +358,14 @@ def test_gpt5_xhigh_dict_accepted_for_supported_model(config: OpenAIConfig):
|
|||
model="gpt-5.4",
|
||||
drop_params=False,
|
||||
)
|
||||
assert params["reasoning_effort"] == {"effort": "xhigh", "summary": "detailed"}
|
||||
assert params["reasoning_effort"] == "xhigh"
|
||||
|
||||
|
||||
def test_gpt5_none_dict_with_tools_no_tool_drop(config: OpenAIConfig):
|
||||
"""Dict with effort='none' and tools: reasoning_effort dropped for gpt-5.4.
|
||||
"""Dict with effort='none' and tools: no tool-drop, reasoning_effort preserved.
|
||||
|
||||
gpt-5.4 drops all reasoning_effort when tools are present,
|
||||
since that combination is only supported in the Responses API.
|
||||
Regression: effective_effort='none' must be used for tool-drop guard so
|
||||
{"effort": "none", "summary": "detailed"} is not incorrectly treated as non-none.
|
||||
"""
|
||||
tools = [{"type": "function", "function": {"name": "test", "description": "test"}}]
|
||||
params = config.map_openai_params(
|
||||
|
|
@ -378,7 +374,7 @@ def test_gpt5_none_dict_with_tools_no_tool_drop(config: OpenAIConfig):
|
|||
model="gpt-5.4",
|
||||
drop_params=False,
|
||||
)
|
||||
assert "reasoning_effort" not in params
|
||||
assert params["reasoning_effort"] == "none"
|
||||
assert params["tools"] == tools
|
||||
|
||||
|
||||
|
|
@ -398,24 +394,28 @@ def test_gpt5_none_dict_with_sampling_params_allowed(config: OpenAIConfig):
|
|||
model="gpt-5.1",
|
||||
drop_params=False,
|
||||
)
|
||||
assert params["reasoning_effort"] == {"effort": "none", "summary": "detailed"}
|
||||
assert params["reasoning_effort"] == "none"
|
||||
assert params["logprobs"] is True
|
||||
assert params["top_p"] == 0.9
|
||||
|
||||
|
||||
def test_gpt5_preserves_reasoning_effort_dict_with_summary_from_optional_params(config: OpenAIConfig):
|
||||
"""reasoning_effort dict with summary in optional_params is preserved."""
|
||||
def test_gpt5_normalizes_reasoning_effort_dict_with_summary_from_optional_params(config: OpenAIConfig):
|
||||
"""reasoning_effort dict with summary in optional_params is normalized."""
|
||||
params = config.map_openai_params(
|
||||
non_default_params={},
|
||||
optional_params={"reasoning_effort": {"effort": "medium", "summary": "detailed"}},
|
||||
model="gpt-5.4",
|
||||
drop_params=False,
|
||||
)
|
||||
assert params["reasoning_effort"] == {"effort": "medium", "summary": "detailed"}
|
||||
assert params["reasoning_effort"] == "medium"
|
||||
|
||||
|
||||
def test_gpt5_4_drops_reasoning_effort_when_user_sends_reasoning_and_tools(config: OpenAIConfig):
|
||||
"""gpt-5.4: function calls not supported with reasoning_effort != 'none'. Drop reasoning_effort."""
|
||||
def test_gpt5_4_passes_through_reasoning_effort_with_tools(config: OpenAIConfig):
|
||||
"""gpt-5.4 with tools + reasoning_effort: map_openai_params passes through both.
|
||||
|
||||
Routing to Responses API (which supports tools + reasoning) happens at completion()
|
||||
level (responses_api_bridge_check). See test_responses_api_bridge_check_gpt_5_4_tools_plus_reasoning_routes_to_responses.
|
||||
"""
|
||||
tools = [{"type": "function", "function": {"name": "test", "description": "test"}}]
|
||||
params = config.map_openai_params(
|
||||
non_default_params={"reasoning_effort": "high", "tools": tools},
|
||||
|
|
@ -423,7 +423,7 @@ def test_gpt5_4_drops_reasoning_effort_when_user_sends_reasoning_and_tools(confi
|
|||
model="gpt-5.4",
|
||||
drop_params=False,
|
||||
)
|
||||
assert "reasoning_effort" not in params
|
||||
assert params["reasoning_effort"] == "high"
|
||||
assert params["tools"] == tools
|
||||
|
||||
|
||||
|
|
@ -438,8 +438,8 @@ def test_gpt5_4_keeps_reasoning_effort_when_no_tools(config: OpenAIConfig):
|
|||
assert params["reasoning_effort"] == "high"
|
||||
|
||||
|
||||
def test_gpt5_4_drops_reasoning_effort_none_with_tools(config: OpenAIConfig):
|
||||
"""reasoning_effort='none' is also dropped when tools are present for gpt-5.4."""
|
||||
def test_gpt5_4_keeps_reasoning_effort_none_with_tools(config: OpenAIConfig):
|
||||
"""reasoning_effort='none' is kept when tools are present."""
|
||||
tools = [{"type": "function", "function": {"name": "test", "description": "test"}}]
|
||||
params = config.map_openai_params(
|
||||
non_default_params={"reasoning_effort": "none", "tools": tools},
|
||||
|
|
@ -447,7 +447,7 @@ def test_gpt5_4_drops_reasoning_effort_none_with_tools(config: OpenAIConfig):
|
|||
model="gpt-5.4",
|
||||
drop_params=False,
|
||||
)
|
||||
assert "reasoning_effort" not in params
|
||||
assert params["reasoning_effort"] == "none"
|
||||
assert params["tools"] == tools
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -627,6 +627,94 @@ def test_responses_api_bridge_check_gpt_5_4_pro():
|
|||
)
|
||||
|
||||
|
||||
def test_responses_api_bridge_check_gpt_5_4_tools_plus_reasoning_routes_to_responses():
|
||||
"""gpt-5.4 with both tools and reasoning_effort should route to Responses API."""
|
||||
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=[{"type": "function", "function": {"name": "get_capital"}}],
|
||||
reasoning_effort="xhigh",
|
||||
)
|
||||
|
||||
assert model == "gpt-5.4"
|
||||
assert model_info.get("mode") == "responses"
|
||||
|
||||
|
||||
def test_responses_api_bridge_check_gpt_5_5_tools_plus_reasoning_routes_to_responses():
|
||||
"""gpt-5.5+ with both tools and reasoning_effort should route to Responses API."""
|
||||
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.5-pro",
|
||||
custom_llm_provider="openai",
|
||||
tools=[{"type": "function", "function": {"name": "get_capital"}}],
|
||||
reasoning_effort="xhigh",
|
||||
)
|
||||
|
||||
assert model == "gpt-5.5-pro"
|
||||
assert model_info.get("mode") == "responses"
|
||||
|
||||
|
||||
def test_responses_api_bridge_check_gpt_5_4_tools_without_reasoning_stays_chat():
|
||||
"""gpt-5.4 with tools only should not be force-routed to Responses API."""
|
||||
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=[{"type": "function", "function": {"name": "get_capital"}}],
|
||||
reasoning_effort=None,
|
||||
)
|
||||
|
||||
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,
|
||||
):
|
||||
"""When routed to Responses, preserve reasoning_effort summary dict."""
|
||||
mock_responses_completion.return_value = MagicMock()
|
||||
|
||||
import litellm
|
||||
|
||||
litellm.completion(
|
||||
model="gpt-5.4",
|
||||
messages=[{"role": "user", "content": "What is the capital of France?"}],
|
||||
tools=[
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_capital",
|
||||
"description": "Get the capital of a country",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {"country": {"type": "string"}},
|
||||
},
|
||||
},
|
||||
}
|
||||
],
|
||||
reasoning_effort={"effort": "xhigh", "summary": "detailed"},
|
||||
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": "xhigh",
|
||||
"summary": "detailed",
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue