From da5189dc0d2e5a0aa030fd6cf18eb3cf24af05f5 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 14 Jul 2026 17:10:56 +0000 Subject: [PATCH] fix(openai): default reasoning_effort to none for gpt-5.4+ function tools on chat completions --- .../llms/openai/chat/gpt_5_transformation.py | 16 +++++ .../llms/openai/test_gpt5_transformation.py | 72 +++++++++++++++++++ 2 files changed, 88 insertions(+) diff --git a/litellm/llms/openai/chat/gpt_5_transformation.py b/litellm/llms/openai/chat/gpt_5_transformation.py index f0a859deba0..aa49adc0985 100644 --- a/litellm/llms/openai/chat/gpt_5_transformation.py +++ b/litellm/llms/openai/chat/gpt_5_transformation.py @@ -114,6 +114,12 @@ class OpenAIGPT5Config(OpenAIGPTConfig): except (ValueError, IndexError): return False + @staticmethod + def _has_function_tools(tools: Union[list, None]) -> bool: + if not tools: + return False + return any(isinstance(tool, dict) and (tool.get("type") == "function" or "function" in tool) for tool in tools) + @classmethod def _supports_reasoning_effort_level(cls, model: str, level: str) -> bool: """Check if the model supports a specific reasoning_effort level. @@ -212,6 +218,16 @@ class OpenAIGPT5Config(OpenAIGPTConfig): raw_reasoning_effort = non_default_params.get("reasoning_effort") or optional_params.get("reasoning_effort") effective_effort = _get_effort_level(raw_reasoning_effort) + if ( + effective_effort is None + and self.is_model_gpt_5_4_plus_model(model) + and self._has_function_tools(non_default_params.get("tools")) + and self._supports_reasoning_effort_level(model, "none") + ): + non_default_params["reasoning_effort"] = "none" + raw_reasoning_effort = "none" + effective_effort = "none" + # 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: diff --git a/tests/test_litellm/llms/openai/test_gpt5_transformation.py b/tests/test_litellm/llms/openai/test_gpt5_transformation.py index d279b119efe..50eb9bb2044 100644 --- a/tests/test_litellm/llms/openai/test_gpt5_transformation.py +++ b/tests/test_litellm/llms/openai/test_gpt5_transformation.py @@ -274,6 +274,78 @@ def test_gpt5_drops_reasoning_effort_xhigh_when_requested(config: OpenAIConfig): assert "reasoning_effort" not in params +FUNCTION_TOOL = { + "type": "function", + "function": { + "name": "get_capital", + "parameters": {"type": "object", "properties": {"country": {"type": "string"}}}, + }, +} + + +@pytest.mark.parametrize("model", ["gpt-5.6-sol", "gpt-5.6-luna", "gpt-5.6-terra", "gpt-5.4"]) +def test_gpt5_4_plus_function_tools_default_reasoning_effort_none(config: OpenAIConfig, model: str): + """gpt-5.4+ reject function tools with a non-'none' reasoning_effort on chat completions. + + Regression for https://github.com/BerriAI/litellm/issues/33221: when the caller omits + reasoning_effort, litellm must default it to 'none' so tool calls succeed instead of + hitting 'Function tools with reasoning_effort are not supported ... in /v1/chat/completions'. + """ + params = config.map_openai_params( + non_default_params={"tools": [FUNCTION_TOOL]}, + optional_params={}, + model=model, + drop_params=False, + ) + assert params["reasoning_effort"] == "none" + + +def test_gpt5_4_plus_function_tools_preserves_explicit_reasoning_effort(config: OpenAIConfig): + """An explicit reasoning_effort must not be overwritten by the tools default. + + (Explicit effort + tools is routed to the Responses API bridge upstream; the chat + mapping itself must leave the caller's value untouched.) + """ + params = config.map_openai_params( + non_default_params={"tools": [FUNCTION_TOOL], "reasoning_effort": "high"}, + optional_params={}, + model="gpt-5.6-sol", + drop_params=False, + ) + assert params["reasoning_effort"] == "high" + + +def test_gpt5_4_plus_without_tools_does_not_default_reasoning_effort(config: OpenAIConfig): + params = config.map_openai_params( + non_default_params={}, + optional_params={}, + model="gpt-5.6-sol", + drop_params=False, + ) + assert "reasoning_effort" not in params + + +def test_gpt5_4_plus_non_function_tools_do_not_default_reasoning_effort(config: OpenAIConfig): + params = config.map_openai_params( + non_default_params={"tools": [{"type": "custom", "custom": {"name": "x"}}]}, + optional_params={}, + model="gpt-5.6-sol", + drop_params=False, + ) + assert "reasoning_effort" not in params + + +def test_gpt5_1_function_tools_do_not_default_reasoning_effort(config: OpenAIConfig): + """The tools default is scoped to gpt-5.4+; gpt-5.1 must keep its existing behavior.""" + params = config.map_openai_params( + non_default_params={"tools": [FUNCTION_TOOL]}, + optional_params={}, + model="gpt-5.1", + drop_params=False, + ) + assert "reasoning_effort" not in params + + # GPT-5.1 temperature handling tests def test_gpt5_1_model_detection(gpt5_config: OpenAIGPT5Config): """Test that models supporting reasoning_effort='none' are correctly detected via model map."""