Merge pull request #23566 from BerriAI/revert-23535-litellm_improve_qa-5.4

Revert "QA:  improve gpt-5.4 code/bugs"
This commit is contained in:
yuneng-jiang
2026-03-13 10:16:34 -07:00
committed by GitHub
15 changed files with 111 additions and 233 deletions
+3 -1
View File
@@ -638,7 +638,9 @@ This is useful when you want to use [Responses API](https://platform.openai.com/
:::tip gpt-5.4 + reasoning_effort + function tools
OpenAI does not support `reasoning_effort` with function tools for `gpt-5.4` in `/v1/chat/completions`. Use the responses bridge instead:
LiteLLM drops `reasoning_effort` from `gpt-5.4` requests to `litellm.completion()` that include tools, since that combination is supported in the Responses API.
If you need reasoning **and** tools together, use the responses bridge instead:
```python
response = litellm.completion(
+3 -1
View File
@@ -594,7 +594,9 @@ Expected Response
:::tip gpt-5.4: reasoning_effort + function tools
OpenAI does not support `reasoning_effort` with function tools for `gpt-5.4` in `/v1/chat/completions`. Use `openai/responses/gpt-5.4` to route through the Responses API instead. See [Responses API Bridge](/docs/providers/openai#openai-chat-completion-to-responses-api-bridge) for details.
LiteLLM drops `reasoning_effort` from `gpt-5.4` requests to `litellm.completion()` that include tools, since that combination is supported in the Responses API.
If you need reasoning **and** tools together, use `openai/responses/gpt-5.4` to route through the Responses API instead. See [Responses API Bridge](/docs/providers/openai#openai-chat-completion-to-responses-api-bridge) for details.
:::
-21
View File
@@ -39,15 +39,6 @@ base_llm_http_handler = BaseLLMHTTPHandler()
#################################################
def _get_tool_config_from_kwargs(kwargs: Dict[str, Any]) -> Optional[Dict[str, Any]]:
"""Read toolConfig/tool_config without dropping intentionally empty dicts."""
if "toolConfig" in kwargs:
return kwargs["toolConfig"]
if "tool_config" in kwargs:
return kwargs["tool_config"]
return None
class GenerateContentSetupResult(BaseModel):
"""Internal Type - Result of setting up a generate content call"""
@@ -180,14 +171,12 @@ class GenerateContentHelper:
system_instruction = kwargs.get("systemInstruction") or kwargs.get(
"system_instruction"
)
tool_config = _get_tool_config_from_kwargs(kwargs)
request_body = (
generate_content_provider_config.transform_generate_content_request(
model=model,
contents=contents,
tools=tools,
generate_content_config_dict=generate_content_config_dict,
tool_config=tool_config,
system_instruction=system_instruction,
)
)
@@ -334,7 +323,6 @@ def generate_content(
system_instruction = kwargs.get("systemInstruction") or kwargs.get(
"system_instruction"
)
tool_config = _get_tool_config_from_kwargs(kwargs)
# Check if we should use the adapter (when provider config is None)
if setup_result.generate_content_provider_config is None:
@@ -366,7 +354,6 @@ def generate_content(
_is_async=_is_async,
client=kwargs.get("client"),
litellm_metadata=kwargs.get("litellm_metadata", {}),
tool_config=tool_config,
system_instruction=system_instruction,
)
@@ -427,7 +414,6 @@ async def agenerate_content_stream(
system_instruction = kwargs.get("systemInstruction") or kwargs.get(
"system_instruction"
)
tool_config = _get_tool_config_from_kwargs(kwargs)
# Check if we should use the adapter (when provider config is None)
if setup_result.generate_content_provider_config is None:
@@ -466,7 +452,6 @@ async def agenerate_content_stream(
client=kwargs.get("client"),
stream=True,
litellm_metadata=kwargs.get("litellm_metadata", {}),
tool_config=tool_config,
system_instruction=system_instruction,
)
@@ -535,10 +520,6 @@ def generate_content_stream(
)
# Call the handler with streaming enabled (sync version)
system_instruction = kwargs.get("systemInstruction") or kwargs.get(
"system_instruction"
)
tool_config = _get_tool_config_from_kwargs(kwargs)
return base_llm_http_handler.generate_content_handler(
model=setup_result.model,
contents=contents,
@@ -555,8 +536,6 @@ def generate_content_stream(
client=kwargs.get("client"),
stream=True,
litellm_metadata=kwargs.get("litellm_metadata", {}),
tool_config=tool_config,
system_instruction=system_instruction,
)
except Exception as e:
@@ -152,7 +152,6 @@ class BaseGoogleGenAIGenerateContentConfig(ABC):
contents: GenerateContentContentListUnionDict,
tools: Optional[ToolConfigDict],
generate_content_config_dict: Dict,
tool_config: Optional[Dict[str, Any]] = None,
system_instruction: Optional[Any] = None,
) -> dict:
"""
@@ -162,7 +161,6 @@ class BaseGoogleGenAIGenerateContentConfig(ABC):
model: The model name
contents: Input contents
tools: Tools
tool_config: Tool configuration
generate_content_config_dict: Generation config parameters
system_instruction: Optional system instruction
@@ -9334,7 +9334,6 @@ class BaseLLMHTTPHandler:
client: Optional[Union[HTTPHandler, AsyncHTTPHandler]] = None,
stream: bool = False,
litellm_metadata: Optional[Dict[str, Any]] = None,
tool_config: Optional[Dict[str, Any]] = None,
system_instruction: Optional[Any] = None,
) -> Any:
"""
@@ -9352,7 +9351,6 @@ class BaseLLMHTTPHandler:
generate_content_provider_config=generate_content_provider_config,
generate_content_config_dict=generate_content_config_dict,
tools=tools,
tool_config=tool_config,
custom_llm_provider=custom_llm_provider,
litellm_params=litellm_params,
logging_obj=logging_obj,
@@ -9391,7 +9389,6 @@ class BaseLLMHTTPHandler:
model=model,
contents=contents,
tools=tools,
tool_config=tool_config,
generate_content_config_dict=generate_content_config_dict,
system_instruction=system_instruction,
)
@@ -9464,7 +9461,6 @@ class BaseLLMHTTPHandler:
client: Optional[AsyncHTTPHandler] = None,
stream: bool = False,
litellm_metadata: Optional[Dict[str, Any]] = None,
tool_config: Optional[Dict[str, Any]] = None,
system_instruction: Optional[Any] = None,
) -> Any:
"""
@@ -9502,7 +9498,6 @@ class BaseLLMHTTPHandler:
model=model,
contents=contents,
tools=tools,
tool_config=tool_config,
generate_content_config_dict=generate_content_config_dict,
system_instruction=system_instruction,
)
@@ -308,7 +308,6 @@ class GoogleGenAIConfig(BaseGoogleGenAIGenerateContentConfig, VertexLLM):
contents: GenerateContentContentListUnionDict,
tools: Optional[ToolConfigDict],
generate_content_config_dict: Dict,
tool_config: Optional[Dict[str, Any]] = None,
system_instruction: Optional[Any] = None,
) -> dict:
from litellm.types.google_genai.main import (
@@ -327,8 +326,6 @@ class GoogleGenAIConfig(BaseGoogleGenAIGenerateContentConfig, VertexLLM):
if system_instruction is not None:
request_dict["systemInstruction"] = system_instruction
if tool_config is not None:
request_dict["toolConfig"] = tool_config
return request_dict
def transform_generate_content_response(
@@ -188,9 +188,11 @@ class OpenAIGPT5Config(OpenAIGPTConfig):
) or optional_params.get("reasoning_effort")
effective_effort = _get_effort_level(raw_reasoning_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:
# 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"}:
normalized = _normalize_reasoning_effort_for_chat_completion(
raw_reasoning_effort
)
@@ -221,6 +223,16 @@ 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:
@@ -73,7 +73,6 @@ class VertexAIGoogleGenAIConfig(GoogleGenAIConfig):
contents: Any,
tools: Optional[Any],
generate_content_config_dict: Dict,
tool_config: Optional[Dict[str, Any]] = None,
system_instruction: Optional[Any] = None,
) -> dict:
"""
@@ -90,11 +89,8 @@ class VertexAIGoogleGenAIConfig(GoogleGenAIConfig):
if tools:
result["tools"] = tools
if tool_config is not None:
result["toolConfig"] = tool_config
# Add systemInstruction if provided
if system_instruction is not None:
if system_instruction:
result["systemInstruction"] = system_instruction
# Handle generationConfig - Vertex AI expects it in the same format
-20
View File
@@ -99,7 +99,6 @@ 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,
@@ -935,8 +934,6 @@ 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:
@@ -954,17 +951,6 @@ 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))
@@ -1610,17 +1596,11 @@ 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,
@@ -174,7 +174,6 @@ async def test_google_gemini_httpx_request_direct():
],
"role": "user"
},
"toolConfig": {"functionCallingConfig": {"mode": "ANY"}},
"config": { # Note: already transformed from generationConfig
"temperature": 0,
"topP": 1,
@@ -241,7 +240,6 @@ async def test_google_gemini_httpx_request_direct():
generate_content_provider_config=provider_config,
generate_content_config_dict=sample_payload["config"],
tools=None,
tool_config=sample_payload["toolConfig"],
custom_llm_provider="gemini",
litellm_params=litellm_params,
logging_obj=logging_obj,
@@ -267,7 +265,6 @@ async def test_google_gemini_httpx_request_direct():
request_data = call_kwargs.get('json')
if request_data:
assert 'contents' in request_data, "Expected 'contents' in request data"
assert request_data["toolConfig"] == sample_payload["toolConfig"]
# The config should be included in the request as generationConfig
if 'generationConfig' in request_data:
@@ -1,13 +1,24 @@
#!/usr/bin/env python3
"""Tests for Google GenAI main entrypoints."""
"""
Test to verify the Google GenAI generate_content adapter functionality
"""
import json
import os
import sys
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
sys.path.insert(0, os.path.abspath("../../.."))
sys.path.insert(
0, os.path.abspath("../../..")
) # Adds the parent directory to the system path
import json
import os
import sys
import pytest
import litellm
@pytest.mark.asyncio
@@ -15,6 +26,8 @@ async def test_agenerate_content_stream():
"""
Test that the agenerate_content_stream function works
"""
from unittest.mock import AsyncMock, patch
from litellm.google_genai.main import (
agenerate_content_stream,
base_llm_http_handler,
@@ -23,40 +36,10 @@ async def test_agenerate_content_stream():
with patch.object(
base_llm_http_handler, "generate_content_handler", new=AsyncMock()
) as mock_post:
await agenerate_content_stream(
result = await agenerate_content_stream(
model="gemini/gemini-2.0-flash-001",
contents="Hello, world!",
stream=True,
)
mock_post.assert_called_once()
assert mock_post.call_args.kwargs["stream"] is True
def test_generate_content_stream_forwards_system_instruction():
"""Test that generate_content_stream forwards systemInstruction and toolConfig."""
from litellm.google_genai.main import (
base_llm_http_handler,
generate_content_stream,
)
mock_response = MagicMock()
tool_config = {"functionCallingConfig": {"mode": "ANY"}}
with patch.object(
base_llm_http_handler, "generate_content_handler", return_value=mock_response
) as mock_post:
result = generate_content_stream(
model="gemini/gemini-2.0-flash-001",
contents="Hello, world!",
stream=True,
systemInstruction={"parts": [{"text": "You are helpful"}]},
toolConfig=tool_config,
)
assert result is mock_response
mock_post.assert_called_once()
assert mock_post.call_args.kwargs["stream"] is True
assert mock_post.call_args.kwargs["tool_config"] == tool_config
assert mock_post.call_args.kwargs["system_instruction"] == {
"parts": [{"text": "You are helpful"}]
}
mock_post.call_args.kwargs["stream"] == True
@@ -12,9 +12,6 @@ sys.path.insert(
import pytest
from litellm.llms.gemini.google_genai.transformation import GoogleGenAIConfig
from litellm.llms.vertex_ai.google_genai.transformation import (
VertexAIGoogleGenAIConfig,
)
from litellm.responses.litellm_completion_transformation.transformation import (
LiteLLMCompletionResponsesConfig,
)
@@ -176,26 +173,6 @@ def test_map_generate_content_optional_params_response_mime_type():
assert "responseJsonSchema" in result
@pytest.mark.parametrize(
"config_cls",
[GoogleGenAIConfig, VertexAIGoogleGenAIConfig],
)
def test_transform_generate_content_request_preserves_tool_config(config_cls):
config = config_cls()
tool_config = {"functionCallingConfig": {"mode": "ANY"}}
result = config.transform_generate_content_request(
model="gemini-3-flash-preview",
contents=[{"role": "user", "parts": [{"text": "hello"}]}],
tools=[{"functionDeclarations": [{"name": "execute_command"}]}],
tool_config=tool_config,
generate_content_config_dict={"temperature": 1},
system_instruction={"parts": [{"text": "system"}]},
)
assert result["toolConfig"] == tool_config
def test_responses_api_reasoning_dict_format():
"""Test that reasoning parameter with dict format is mapped to reasoning_effort"""
from litellm.types.llms.openai import ResponsesAPIOptionalRequestParams
@@ -297,7 +274,6 @@ def test_transform_generate_content_request_with_system_instruction():
model="gemini-3-flash-preview",
contents=contents,
tools=None,
tool_config=None,
generate_content_config_dict=generate_content_config_dict,
system_instruction=system_instruction,
)
@@ -329,7 +305,6 @@ def test_transform_generate_content_request_without_system_instruction():
model="gemini-3-flash-preview",
contents=contents,
tools=None,
tool_config=None,
generate_content_config_dict=generate_content_config_dict,
system_instruction=None,
)
@@ -381,7 +356,6 @@ def test_transform_generate_content_request_system_instruction_with_tools():
model="gemini-3-flash-preview",
contents=contents,
tools=tools,
tool_config=None,
generate_content_config_dict=generate_content_config_dict,
system_instruction=system_instruction,
)
@@ -9,11 +9,11 @@ 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,
)
from litellm.llms.openai.chat.gpt_5_transformation import OpenAIGPT5Config
class TestOpenAIGPTConfig:
@@ -460,11 +460,8 @@ class TestGPT5ReasoningEffortPreservation:
assert "reasoning_effort" not in non_default_params
def test_reasoning_effort_dict_none_treated_as_none_for_tools(self):
"""none-dict: {"effort": "none", "summary": "detailed"} is treated as effort=none.
Tool-drop guard should NOT fire; reasoning_effort should be kept.
"""
def test_reasoning_effort_dict_none_dropped_for_gpt5_4_with_tools(self):
"""none-dict with tools on gpt-5.4: reasoning_effort is dropped."""
tools = [{"type": "function", "function": {"name": "test", "description": "test"}}]
non_default_params = {"reasoning_effort": {"effort": "none", "summary": "detailed"}, "tools": tools}
optional_params = {}
@@ -476,7 +473,7 @@ class TestGPT5ReasoningEffortPreservation:
drop_params=False,
)
assert non_default_params.get("reasoning_effort") == {"effort": "none", "summary": "detailed"}
assert "reasoning_effort" not in non_default_params
assert non_default_params.get("tools") == tools
def test_reasoning_effort_dict_none_treated_as_none_for_sampling(self):
@@ -324,15 +324,19 @@ def test_gpt5_4_pro_allows_reasoning_effort_xhigh(config: OpenAIConfig):
assert params["reasoning_effort"] == "xhigh"
def test_gpt5_normalizes_reasoning_effort_dict_with_summary(config: OpenAIConfig):
"""Dict with summary/generate_summary is normalized for chat completions."""
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.
"""
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"] == "high"
assert params["reasoning_effort"] == {"effort": "high", "summary": "detailed"}
def test_gpt5_xhigh_dict_triggers_validation(config: OpenAIConfig):
@@ -358,14 +362,14 @@ def test_gpt5_xhigh_dict_accepted_for_supported_model(config: OpenAIConfig):
model="gpt-5.4",
drop_params=False,
)
assert params["reasoning_effort"] == "xhigh"
assert params["reasoning_effort"] == {"effort": "xhigh", "summary": "detailed"}
def test_gpt5_none_dict_with_tools_no_tool_drop(config: OpenAIConfig):
"""Dict with effort='none' and tools: no tool-drop, reasoning_effort preserved.
"""Dict with effort='none' and tools: reasoning_effort dropped for gpt-5.4.
Regression: effective_effort='none' must be used for tool-drop guard so
{"effort": "none", "summary": "detailed"} is not incorrectly treated as non-none.
gpt-5.4 drops all reasoning_effort when tools are present,
since that combination is only supported in the Responses API.
"""
tools = [{"type": "function", "function": {"name": "test", "description": "test"}}]
params = config.map_openai_params(
@@ -374,7 +378,7 @@ def test_gpt5_none_dict_with_tools_no_tool_drop(config: OpenAIConfig):
model="gpt-5.4",
drop_params=False,
)
assert params["reasoning_effort"] == "none"
assert "reasoning_effort" not in params
assert params["tools"] == tools
@@ -394,20 +398,70 @@ def test_gpt5_none_dict_with_sampling_params_allowed(config: OpenAIConfig):
model="gpt-5.1",
drop_params=False,
)
assert params["reasoning_effort"] == "none"
assert params["reasoning_effort"] == {"effort": "none", "summary": "detailed"}
assert params["logprobs"] is True
assert params["top_p"] == 0.9
def test_gpt5_normalizes_reasoning_effort_dict_with_summary_from_optional_params(config: OpenAIConfig):
"""reasoning_effort dict with summary in optional_params is normalized."""
def test_gpt5_preserves_reasoning_effort_dict_with_summary_from_optional_params(config: OpenAIConfig):
"""reasoning_effort dict with summary in optional_params is preserved."""
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"] == "medium"
assert params["reasoning_effort"] == {"effort": "medium", "summary": "detailed"}
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."""
tools = [{"type": "function", "function": {"name": "test", "description": "test"}}]
params = config.map_openai_params(
non_default_params={"reasoning_effort": "high", "tools": tools},
optional_params={},
model="gpt-5.4",
drop_params=False,
)
assert "reasoning_effort" not in params
assert params["tools"] == tools
def test_gpt5_4_keeps_reasoning_effort_when_no_tools(config: OpenAIConfig):
"""reasoning_effort is kept when tools are not present."""
params = config.map_openai_params(
non_default_params={"reasoning_effort": "high"},
optional_params={},
model="gpt-5.4",
drop_params=False,
)
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."""
tools = [{"type": "function", "function": {"name": "test", "description": "test"}}]
params = config.map_openai_params(
non_default_params={"reasoning_effort": "none", "tools": tools},
optional_params={},
model="gpt-5.4",
drop_params=False,
)
assert "reasoning_effort" not in params
assert params["tools"] == tools
def test_gpt5_2_keeps_reasoning_effort_with_tools(config: OpenAIConfig):
"""gpt-5.2: reasoning_effort drop only applies to gpt-5.4, not gpt-5.2."""
tools = [{"type": "function", "function": {"name": "test", "description": "test"}}]
params = config.map_openai_params(
non_default_params={"reasoning_effort": "high", "tools": tools},
optional_params={},
model="gpt-5.2",
drop_params=False,
)
assert params["reasoning_effort"] == "high"
assert params["tools"] == tools
def test_gpt5_4_pro_rejects_non_default_temperature(config: OpenAIConfig):
-88
View File
@@ -627,94 +627,6 @@ 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