From 2027549c43eb14ce2303938b2ac6b55f364d609a Mon Sep 17 00:00:00 2001 From: Oxygen56 Date: Fri, 2 Oct 2026 03:00:28 +0800 Subject: [PATCH] fix: preserve reasoning_effort for GPT-4 chat models --- litellm/llms/azure/chat/gpt_transformation.py | 5 +- .../llms/openai/chat/gpt_transformation.py | 8 ++ tests/llm_translation/test_optional_params.py | 103 ++++++++---------- 3 files changed, 56 insertions(+), 60 deletions(-) diff --git a/litellm/llms/azure/chat/gpt_transformation.py b/litellm/llms/azure/chat/gpt_transformation.py index 355714c0daf..4dcdc02d47b 100644 --- a/litellm/llms/azure/chat/gpt_transformation.py +++ b/litellm/llms/azure/chat/gpt_transformation.py @@ -103,7 +103,7 @@ class AzureOpenAIConfig(BaseConfig): return super().get_config() def get_supported_openai_params(self, model: str) -> list[str]: - return [ + supported_params: Final = [ "temperature", "n", "stream", @@ -135,6 +135,9 @@ class AzureOpenAIConfig(BaseConfig): "prompt_cache_key", "store", ] + if litellm.OpenAIGPTConfig.supports_reasoning_effort_passthrough(model): + supported_params.append("reasoning_effort") + return supported_params @classmethod def requires_max_completion_tokens(cls, model: str) -> bool: diff --git a/litellm/llms/openai/chat/gpt_transformation.py b/litellm/llms/openai/chat/gpt_transformation.py index 3b38825c83d..73a9b2d6fee 100644 --- a/litellm/llms/openai/chat/gpt_transformation.py +++ b/litellm/llms/openai/chat/gpt_transformation.py @@ -182,6 +182,8 @@ class OpenAIGPTConfig(BaseLLMModelInfo, BaseConfig): model_specific_params.append( "user" ) # user is not a param supported by all openai-compatible endpoints - e.g. azure ai + if OpenAIGPTConfig.supports_reasoning_effort_passthrough(model): + model_specific_params.append("reasoning_effort") return base_params + model_specific_params @staticmethod @@ -192,6 +194,12 @@ class OpenAIGPTConfig(BaseLLMModelInfo, BaseConfig): or model_for_check in litellm.open_ai_text_completion_models ) + @staticmethod + def supports_reasoning_effort_passthrough(model: str) -> bool: + raw_model: Final = model.split("responses/", 1)[1] if "responses/" in model else model + model_for_check: Final = raw_model.split("/", 1)[1] if "/" in raw_model else raw_model + return model_for_check.startswith(("gpt-4.1", "gpt-4o")) + def _map_openai_params( self, non_default_params: dict, diff --git a/tests/llm_translation/test_optional_params.py b/tests/llm_translation/test_optional_params.py index 58446014bdf..fe597b490bd 100644 --- a/tests/llm_translation/test_optional_params.py +++ b/tests/llm_translation/test_optional_params.py @@ -45,24 +45,18 @@ def test_supports_system_message(): ## confirm you can make a openai call with this param - response = litellm.completion( - model="gpt-3.5-turbo", messages=new_messages, supports_system_message=False - ) + response = litellm.completion(model="gpt-3.5-turbo", messages=new_messages, supports_system_message=False) assert isinstance(response, litellm.ModelResponse) -@pytest.mark.parametrize( - "stop_sequence, expected_count", [("\n", 0), (["\n"], 0), (["finish_reason"], 1)] -) +@pytest.mark.parametrize("stop_sequence, expected_count", [("\n", 0), (["\n"], 0), (["finish_reason"], 1)]) def test_anthropic_optional_params(stop_sequence, expected_count): """ Test if whitespace character optional param is dropped by anthropic """ litellm.drop_params = True - optional_params = get_optional_params( - model="claude-3", custom_llm_provider="anthropic", stop=stop_sequence - ) + optional_params = get_optional_params(model="claude-3", custom_llm_provider="anthropic", stop=stop_sequence) assert len(optional_params) == expected_count @@ -141,6 +135,28 @@ def test_get_optional_params_with_allowed_openai_params(): assert optional_params["reasoning_effort"] == reasoning_effort +@pytest.mark.parametrize("custom_llm_provider", ["openai", "azure"]) +@pytest.mark.parametrize("model", ["gpt-4.1", "gpt-4.1-mini", "gpt-4o", "gpt-4o-mini"]) +def test_gpt4_reasoning_effort_is_not_silently_dropped(custom_llm_provider, model): + optional_params = get_optional_params( + model=model, + custom_llm_provider=custom_llm_provider, + reasoning_effort="low", + drop_params=True, + ) + + assert optional_params["reasoning_effort"] == "low" + + optional_params_without_drop = get_optional_params( + model=model, + custom_llm_provider=custom_llm_provider, + reasoning_effort="low", + drop_params=False, + ) + + assert optional_params_without_drop["reasoning_effort"] == "low" + + def test_allowed_openai_params_does_not_forward_unset_params(): """ Regression test for https://github.com/BerriAI/litellm/issues/25697 @@ -263,9 +279,7 @@ def test_bedrock_optional_params_simple(model): ("bedrock/cohere.embed-multilingual-v3", True, None), ], ) -def test_bedrock_optional_params_embeddings_dimension( - model, expected_dimensions, dimensions_kwarg -): +def test_bedrock_optional_params_embeddings_dimension(model, expected_dimensions, dimensions_kwarg): litellm.drop_params = True optional_params = get_optional_params_embeddings( model=model, @@ -499,10 +513,10 @@ def test_azure_tool_choice(api_version): if api_version == "2024-07-01": assert optional_params["tool_choice"] == "required" else: - assert ( - "tool_choice" not in optional_params - ), "tool choice should not be present. Got - tool_choice={} for api version={}".format( - optional_params["tool_choice"], api_version + assert "tool_choice" not in optional_params, ( + "tool choice should not be present. Got - tool_choice={} for api version={}".format( + optional_params["tool_choice"], api_version + ) ) @@ -532,9 +546,7 @@ def test_dynamic_drop_params(drop_params): def test_dynamic_drop_params_e2e(): - with patch( - "litellm.llms.custom_httpx.http_handler.HTTPHandler.post", new=MagicMock() - ) as mock_response: + with patch("litellm.llms.custom_httpx.http_handler.HTTPHandler.post", new=MagicMock()) as mock_response: try: response = litellm.completion( model="command-r-08-2024", @@ -551,9 +563,7 @@ def test_dynamic_drop_params_e2e(): def test_dynamic_pass_additional_params(): - with patch( - "litellm.llms.custom_httpx.http_handler.HTTPHandler.post", new=MagicMock() - ) as mock_response: + with patch("litellm.llms.custom_httpx.http_handler.HTTPHandler.post", new=MagicMock()) as mock_response: try: response = litellm.completion( model="command-r-08-2024", @@ -601,9 +611,7 @@ def test_dynamic_drop_params_parallel_tool_calls(): """ https://github.com/BerriAI/litellm/issues/4584 """ - with patch( - "litellm.llms.custom_httpx.http_handler.HTTPHandler.post", new=MagicMock() - ) as mock_response: + with patch("litellm.llms.custom_httpx.http_handler.HTTPHandler.post", new=MagicMock()) as mock_response: try: response = litellm.completion( model="command-r-08-2024", @@ -658,9 +666,7 @@ def test_dynamic_drop_additional_params_stream_options(): def test_dynamic_drop_additional_params_e2e(): - with patch( - "litellm.llms.custom_httpx.http_handler.HTTPHandler.post", new=MagicMock() - ) as mock_response: + with patch("litellm.llms.custom_httpx.http_handler.HTTPHandler.post", new=MagicMock()) as mock_response: try: response = litellm.completion( model="command-r-08-2024", @@ -679,16 +685,12 @@ def test_dynamic_drop_additional_params_e2e(): def test_get_optional_params_image_gen(): - response = litellm.utils.get_optional_params_image_gen( - aws_region_name="us-east-1", custom_llm_provider="openai" - ) + response = litellm.utils.get_optional_params_image_gen(aws_region_name="us-east-1", custom_llm_provider="openai") print(response) assert "aws_region_name" not in response - response = litellm.utils.get_optional_params_image_gen( - aws_region_name="us-east-1", custom_llm_provider="bedrock" - ) + response = litellm.utils.get_optional_params_image_gen(aws_region_name="us-east-1", custom_llm_provider="bedrock") print(response) @@ -751,9 +753,7 @@ def test_vertex_safety_settings(provider): }, ] - optional_params = get_optional_params( - model="gemini-1.5-pro", custom_llm_provider=provider - ) + optional_params = get_optional_params(model="gemini-1.5-pro", custom_llm_provider=provider) assert len(optional_params) == 1 @@ -862,9 +862,7 @@ def _check_additional_properties(schema): if isinstance(schema, dict): # Remove the 'additionalProperties' key if it exists and is set to False if "additionalProperties" in schema or "strict" in schema: - raise ValueError( - "additionalProperties and strict should not be in the schema" - ) + raise ValueError("additionalProperties and strict should not be in the schema") # Recursively process all dictionary values for key, value in schema.items(): @@ -1067,32 +1065,23 @@ def test_vertex_schema_field(): ) print(optional_params) print(optional_params["tools"][0]["function_declarations"][0]) - assert ( - "$schema" - not in optional_params["tools"][0]["function_declarations"][0]["parameters"] - ) + assert "$schema" not in optional_params["tools"][0]["function_declarations"][0]["parameters"] def test_watsonx_tool_choice(): - optional_params = get_optional_params( - model="gemini-1.5-pro", custom_llm_provider="watsonx", tool_choice="auto" - ) + optional_params = get_optional_params(model="gemini-1.5-pro", custom_llm_provider="watsonx", tool_choice="auto") print(optional_params) assert optional_params["tool_choice_option"] == "auto" def test_watsonx_text_top_k(): - optional_params = get_optional_params( - model="gemini-1.5-pro", custom_llm_provider="watsonx_text", top_k=10 - ) + optional_params = get_optional_params(model="gemini-1.5-pro", custom_llm_provider="watsonx_text", top_k=10) print(optional_params) assert optional_params["top_k"] == 10 def test_together_ai_model_params(): - optional_params = get_optional_params( - model="together_ai", custom_llm_provider="together_ai", logprobs=1 - ) + optional_params = get_optional_params(model="together_ai", custom_llm_provider="together_ai", logprobs=1) print(optional_params) assert optional_params["logprobs"] == 1 @@ -1201,9 +1190,7 @@ def test_groq_response_format_json_schema(): def test_gemini_frequency_penalty(): - optional_params = get_optional_params( - model="gemini-1.5-flash", custom_llm_provider="gemini", frequency_penalty=0.5 - ) + optional_params = get_optional_params(model="gemini-1.5-flash", custom_llm_provider="gemini", frequency_penalty=0.5) assert optional_params["frequency_penalty"] == 0.5 @@ -2007,9 +1994,7 @@ def test_validate_openai_optional_params_integration(): mock_response.usage.completion_tokens = 5 mock_response.usage.total_tokens = 15 - mock_client.return_value.chat.completions.create.return_value = ( - mock_response - ) + mock_client.return_value.chat.completions.create.return_value = mock_response # Call completion with more than 4 stop sequences response = litellm.completion(