mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-23 00:25:00 +00:00
Revert "QA: improve gpt-5.4 code/bugs"
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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.
|
||||
|
||||
:::
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user