diff --git a/litellm/llms/openai/chat/gpt_5_transformation.py b/litellm/llms/openai/chat/gpt_5_transformation.py index af02235c34..beb76f3d80 100644 --- a/litellm/llms/openai/chat/gpt_5_transformation.py +++ b/litellm/llms/openai/chat/gpt_5_transformation.py @@ -64,6 +64,12 @@ class OpenAIGPT5Config(OpenAIGPTConfig): model_name = model.split("/")[-1] return model_name.startswith("gpt-5.2") or model_name.startswith("gpt-5.4") + @classmethod + def is_model_gpt_5_4_model(cls, model: str) -> bool: + """Check if the model is a gpt-5.4 variant (including pro).""" + model_name = model.split("/")[-1] + return model_name.startswith("gpt-5.4") + @classmethod def _supports_reasoning_effort_level(cls, model: str, level: str) -> bool: """Check if the model supports a specific reasoning_effort level. @@ -179,6 +185,17 @@ class OpenAIGPT5Config(OpenAIGPTConfig): "max_tokens" ) + # gpt-5.4: function calls not supported when reasoning_effort != "none" + # Drop reasoning_effort when tools are present (small minority of volume) + 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 reasoning_effort not in (None, "none"): + non_default_params.pop("reasoning_effort", None) + optional_params.pop("reasoning_effort", None) + 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: diff --git a/tests/test_litellm/llms/openai/test_gpt5_transformation.py b/tests/test_litellm/llms/openai/test_gpt5_transformation.py index 415325d0da..b136f8774b 100644 --- a/tests/test_litellm/llms/openai/test_gpt5_transformation.py +++ b/tests/test_litellm/llms/openai/test_gpt5_transformation.py @@ -349,6 +349,56 @@ def test_gpt5_normalizes_reasoning_effort_dict_from_optional_params(config: Open assert params["reasoning_effort"] == "medium" +def test_gpt5_4_drops_reasoning_effort_when_tools_present(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_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}, + optional_params={}, + model="gpt-5.4", + drop_params=False, + ) + assert params["reasoning_effort"] == "none" + 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): with pytest.raises(litellm.utils.UnsupportedParamsError): config.map_openai_params(