diff --git a/litellm/llms/openai/chat/gpt_5_transformation.py b/litellm/llms/openai/chat/gpt_5_transformation.py index e3b3805023..bb5783011a 100644 --- a/litellm/llms/openai/chat/gpt_5_transformation.py +++ b/litellm/llms/openai/chat/gpt_5_transformation.py @@ -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 ) diff --git a/litellm/main.py b/litellm/main.py index 69706376aa..4f3768a390 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -955,7 +955,7 @@ def responses_api_bridge_check( model_info["mode"] = "responses" model = model.replace("responses/", "") - # OpenAI gpt-5.4 chat-completions calls with both tools + reasoning_effort + # OpenAI gpt-5.4+ chat-completions calls with both tools + reasoning_effort # must be bridged to Responses API. if ( custom_llm_provider == "openai" @@ -1617,6 +1617,10 @@ def completion( # type: ignore # noqa: PLR0915 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, diff --git a/tests/test_litellm/llms/openai/test_gpt5_transformation.py b/tests/test_litellm/llms/openai/test_gpt5_transformation.py index aca328ee43..09e8954c13 100644 --- a/tests/test_litellm/llms/openai/test_gpt5_transformation.py +++ b/tests/test_litellm/llms/openai/test_gpt5_transformation.py @@ -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,7 +358,7 @@ 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): @@ -378,7 +374,7 @@ def test_gpt5_none_dict_with_tools_no_tool_drop(config: OpenAIConfig): model="gpt-5.4", drop_params=False, ) - assert params["reasoning_effort"] == {"effort": "none", "summary": "detailed"} + assert params["reasoning_effort"] == "none" assert params["tools"] == tools @@ -398,20 +394,20 @@ 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_pro_rejects_non_default_temperature(config: OpenAIConfig): diff --git a/tests/test_litellm/test_main.py b/tests/test_litellm/test_main.py index ab0a18490e..6ac988b2c2 100644 --- a/tests/test_litellm/test_main.py +++ b/tests/test_litellm/test_main.py @@ -678,6 +678,43 @@ def test_responses_api_bridge_check_gpt_5_4_tools_without_reasoning_stays_chat() 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