From 79e262d12bf409deb674da1d96378cb4d3b9ebfc Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Mon, 11 Aug 2025 23:40:05 -0700 Subject: [PATCH] feat(common_utils.py): make default azure openai responses api use `/openai/v1/responses` logic Fixes https://github.com/BerriAI/litellm/issues/13527#issuecomment-3177882103 --- litellm/constants.py | 2 +- litellm/llms/azure/common_utils.py | 12 +- tests/llm_translation/test_azure_openai.py | 14 ++ .../response/test_azure_transformation.py | 140 +++++++++++++----- .../test_openai_responses_transformation.py | 69 --------- 5 files changed, 129 insertions(+), 108 deletions(-) diff --git a/litellm/constants.py b/litellm/constants.py index 8d502afaf2..afdb207362 100644 --- a/litellm/constants.py +++ b/litellm/constants.py @@ -2,7 +2,7 @@ import os from typing import List, Literal AZURE_DEFAULT_RESPONSES_API_VERSION = str( - os.getenv("AZURE_DEFAULT_RESPONSES_API_VERSION", "2025-04-01-preview") + os.getenv("AZURE_DEFAULT_RESPONSES_API_VERSION", "preview") ) ROUTER_MAX_FALLBACKS = int(os.getenv("ROUTER_MAX_FALLBACKS", 5)) DEFAULT_BATCH_SIZE = int(os.getenv("DEFAULT_BATCH_SIZE", 512)) diff --git a/litellm/llms/azure/common_utils.py b/litellm/llms/azure/common_utils.py index 5764fcdd1b..09b1888e04 100644 --- a/litellm/llms/azure/common_utils.py +++ b/litellm/llms/azure/common_utils.py @@ -694,9 +694,17 @@ class BaseAzureLLM(BaseOpenAILLM): api_base: Optional[str], litellm_params: Optional[Union[GenericLiteLLMParams, Dict[str, Any]]], route: Literal["/openai/responses", "/openai/vector_stores"], - default_api_version: Optional[str] = None, + default_api_version: Optional[Union[str, Literal["latest", "preview"]]] = None, ) -> str: - from litellm.constants import AZURE_DEFAULT_RESPONSES_API_VERSION + """ + Get the base Azure URL for the given route and API version. + + Args: + api_base: The base URL of the Azure API. + litellm_params: The litellm parameters. + route: The route to the API. + default_api_version: The default API version to use if no api_version is provided. If 'latest', it will use `openai/v1/...` route. + """ api_base = api_base or litellm.api_base or get_secret_str("AZURE_API_BASE") if api_base is None: diff --git a/tests/llm_translation/test_azure_openai.py b/tests/llm_translation/test_azure_openai.py index a27d0dd165..a1b05cbb4a 100644 --- a/tests/llm_translation/test_azure_openai.py +++ b/tests/llm_translation/test_azure_openai.py @@ -630,3 +630,17 @@ def test_azure_openai_responses_bridge(): == "test-azure-computer-use-preview" ) assert mock_responses.call_args.kwargs["custom_llm_provider"] == "azure" + + +def test_azure_openai_gpt_5_responses_api(): + from litellm import responses + + litellm._turn_on_debug() + + response = responses( + model="azure/gpt-5", + input="Hello world", + api_key=os.getenv("AZURE_SWEDEN_API_KEY"), + api_base=os.getenv("AZURE_SWEDEN_API_BASE"), + ) + print(f"response: {response}") diff --git a/tests/test_litellm/llms/azure/response/test_azure_transformation.py b/tests/test_litellm/llms/azure/response/test_azure_transformation.py index 51edf91b70..5a0db987ef 100644 --- a/tests/test_litellm/llms/azure/response/test_azure_transformation.py +++ b/tests/test_litellm/llms/azure/response/test_azure_transformation.py @@ -8,10 +8,14 @@ sys.path.insert( 0, os.path.abspath("../../../../..") ) # Adds the parent directory to the system path +from unittest.mock import MagicMock + +from litellm.llms.azure.responses.o_series_transformation import ( + AzureOpenAIOSeriesResponsesAPIConfig, +) from litellm.llms.azure.responses.transformation import AzureOpenAIResponsesAPIConfig -from litellm.llms.azure.responses.o_series_transformation import AzureOpenAIOSeriesResponsesAPIConfig -from litellm.types.router import GenericLiteLLMParams from litellm.types.llms.openai import ResponsesAPIOptionalRequestParams +from litellm.types.router import GenericLiteLLMParams @pytest.mark.serial @@ -27,6 +31,7 @@ def test_validate_environment_api_key_within_litellm_params(): assert result == expected + @pytest.mark.serial def test_validate_environment_api_key_within_litellm(): azure_openai_responses_apiconfig = AzureOpenAIResponsesAPIConfig() @@ -41,6 +46,7 @@ def test_validate_environment_api_key_within_litellm(): assert result == expected + @pytest.mark.serial def test_validate_environment_azure_key_within_litellm(): azure_openai_responses_apiconfig = AzureOpenAIResponsesAPIConfig() @@ -55,6 +61,7 @@ def test_validate_environment_azure_key_within_litellm(): assert result == expected + @pytest.mark.serial def test_validate_environment_azure_key_within_headers(): azure_openai_responses_apiconfig = AzureOpenAIResponsesAPIConfig() @@ -93,10 +100,10 @@ def test_azure_o_series_responses_api_supported_params(): """Test that Azure OpenAI O-series responses API excludes temperature from supported parameters.""" config = AzureOpenAIOSeriesResponsesAPIConfig() supported_params = config.get_supported_openai_params("o_series/gpt-o1") - + # Temperature should not be in supported params for O-series models assert "temperature" not in supported_params - + # Other parameters should still be supported assert "input" in supported_params assert "max_output_tokens" in supported_params @@ -108,35 +115,32 @@ def test_azure_o_series_responses_api_supported_params(): def test_azure_o_series_responses_api_drop_temperature_param(): """Test that temperature parameter is dropped when drop_params is True for O-series models.""" config = AzureOpenAIOSeriesResponsesAPIConfig() - + # Create request params with temperature request_params = ResponsesAPIOptionalRequestParams( - temperature=0.7, - max_output_tokens=1000, - stream=False, - top_p=0.9 + temperature=0.7, max_output_tokens=1000, stream=False, top_p=0.9 ) - + # Test with drop_params=True mapped_params_with_drop = config.map_openai_params( response_api_optional_params=request_params, model="o_series/gpt-o1", - drop_params=True + drop_params=True, ) - + # Temperature should be dropped assert "temperature" not in mapped_params_with_drop # Other params should remain assert mapped_params_with_drop["max_output_tokens"] == 1000 assert mapped_params_with_drop["top_p"] == 0.9 - + # Test with drop_params=False mapped_params_without_drop = config.map_openai_params( response_api_optional_params=request_params, model="o_series/gpt-o1", - drop_params=False + drop_params=False, ) - + # Temperature should still be present when drop_params=False assert mapped_params_without_drop["temperature"] == 0.7 assert mapped_params_without_drop["max_output_tokens"] == 1000 @@ -147,21 +151,19 @@ def test_azure_o_series_responses_api_drop_temperature_param(): def test_azure_o_series_responses_api_drop_params_no_temperature(): """Test that map_openai_params works correctly when temperature is not present for O-series models.""" config = AzureOpenAIOSeriesResponsesAPIConfig() - + # Create request params without temperature request_params = ResponsesAPIOptionalRequestParams( - max_output_tokens=1000, - stream=False, - top_p=0.9 + max_output_tokens=1000, stream=False, top_p=0.9 ) - + # Should work fine even with drop_params=True mapped_params = config.map_openai_params( response_api_optional_params=request_params, model="o_series/gpt-o1", - drop_params=True + drop_params=True, ) - + assert "temperature" not in mapped_params assert mapped_params["max_output_tokens"] == 1000 assert mapped_params["top_p"] == 0.9 @@ -172,10 +174,10 @@ def test_azure_regular_responses_api_supports_temperature(): """Test that regular Azure OpenAI responses API (non-O-series) supports temperature parameter.""" config = AzureOpenAIResponsesAPIConfig() supported_params = config.get_supported_openai_params("gpt-4o") - + # Regular Azure models should support temperature assert "temperature" in supported_params - + # Other parameters should still be supported assert "input" in supported_params assert "max_output_tokens" in supported_params @@ -187,11 +189,11 @@ def test_azure_regular_responses_api_supports_temperature(): def test_o_series_model_detection(): """Test that the O-series configuration correctly identifies O-series models.""" config = AzureOpenAIOSeriesResponsesAPIConfig() - + # Test explicit o_series naming assert config.is_o_series_model("o_series/gpt-o1") == True assert config.is_o_series_model("azure/o_series/gpt-o3") == True - + # Test regular models assert config.is_o_series_model("gpt-4o") == False assert config.is_o_series_model("gpt-3.5-turbo") == False @@ -200,28 +202,94 @@ def test_o_series_model_detection(): @pytest.mark.serial def test_provider_config_manager_o_series_selection(): """Test that ProviderConfigManager returns the correct config for O-series vs regular models.""" - from litellm.utils import ProviderConfigManager import litellm - + from litellm.utils import ProviderConfigManager + # Test O-series model selection o_series_config = ProviderConfigManager.get_provider_responses_api_config( - provider=litellm.LlmProviders.AZURE, - model="o_series/gpt-o1" + provider=litellm.LlmProviders.AZURE, model="o_series/gpt-o1" ) assert isinstance(o_series_config, AzureOpenAIOSeriesResponsesAPIConfig) - + # Test regular model selection regular_config = ProviderConfigManager.get_provider_responses_api_config( - provider=litellm.LlmProviders.AZURE, - model="gpt-4o" + provider=litellm.LlmProviders.AZURE, model="gpt-4o" ) assert isinstance(regular_config, AzureOpenAIResponsesAPIConfig) assert not isinstance(regular_config, AzureOpenAIOSeriesResponsesAPIConfig) - + # Test with no model specified (should default to regular) default_config = ProviderConfigManager.get_provider_responses_api_config( - provider=litellm.LlmProviders.AZURE, - model=None + provider=litellm.LlmProviders.AZURE, model=None ) assert isinstance(default_config, AzureOpenAIResponsesAPIConfig) assert not isinstance(default_config, AzureOpenAIOSeriesResponsesAPIConfig) + + +class TestAzureResponsesAPIConfig: + def setup_method(self): + self.config = AzureOpenAIResponsesAPIConfig() + self.model = "gpt-4o" + self.logging_obj = MagicMock() + + def test_azure_get_complete_url_with_version_types(self): + """Test Azure get_complete_url with different API version types""" + base_url = "https://litellm8397336933.openai.azure.com" + + # Test with preview version - should use openai/v1/responses + result_preview = self.config.get_complete_url( + api_base=base_url, + litellm_params={"api_version": "preview"}, + ) + assert ( + result_preview + == "https://litellm8397336933.openai.azure.com/openai/v1/responses?api-version=preview" + ) + + # Test with latest version - should use openai/v1/responses + result_latest = self.config.get_complete_url( + api_base=base_url, + litellm_params={"api_version": "latest"}, + ) + assert ( + result_latest + == "https://litellm8397336933.openai.azure.com/openai/v1/responses?api-version=latest" + ) + + # Test with date-based version - should use openai/responses + result_date = self.config.get_complete_url( + api_base=base_url, + litellm_params={"api_version": "2025-01-01"}, + ) + assert ( + result_date + == "https://litellm8397336933.openai.azure.com/openai/responses?api-version=2025-01-01" + ) + + def test_azure_get_complete_url_with_default_api_version(self): + """Test Azure get_complete_url uses default API version when none is provided""" + from litellm.constants import AZURE_DEFAULT_RESPONSES_API_VERSION + + base_url = "https://litellm8397336933.openai.azure.com" + + # Test with no api_version provided - should use default + result_no_version = self.config.get_complete_url( + api_base=base_url, + litellm_params={}, + ) + expected_url = f"https://litellm8397336933.openai.azure.com/openai/v1/responses?api-version={AZURE_DEFAULT_RESPONSES_API_VERSION}" + assert result_no_version == expected_url + + # Test with empty litellm_params - should use default + result_empty_params = self.config.get_complete_url( + api_base=base_url, + litellm_params={}, + ) + assert result_empty_params == expected_url + + # Test with None api_version - should use default + result_none_version = self.config.get_complete_url( + api_base=base_url, + litellm_params={"api_version": None}, + ) + assert result_none_version == expected_url diff --git a/tests/test_litellm/llms/openai/responses/test_openai_responses_transformation.py b/tests/test_litellm/llms/openai/responses/test_openai_responses_transformation.py index a587832a1a..9b8e56ab49 100644 --- a/tests/test_litellm/llms/openai/responses/test_openai_responses_transformation.py +++ b/tests/test_litellm/llms/openai/responses/test_openai_responses_transformation.py @@ -283,75 +283,6 @@ class TestOpenAIResponsesAPIConfig: assert result.type == "test" -class TestAzureResponsesAPIConfig: - def setup_method(self): - self.config = AzureOpenAIResponsesAPIConfig() - self.model = "gpt-4o" - self.logging_obj = MagicMock() - - def test_azure_get_complete_url_with_version_types(self): - """Test Azure get_complete_url with different API version types""" - base_url = "https://litellm8397336933.openai.azure.com" - - # Test with preview version - should use openai/v1/responses - result_preview = self.config.get_complete_url( - api_base=base_url, - litellm_params={"api_version": "preview"}, - ) - assert ( - result_preview - == "https://litellm8397336933.openai.azure.com/openai/v1/responses?api-version=preview" - ) - - # Test with latest version - should use openai/v1/responses - result_latest = self.config.get_complete_url( - api_base=base_url, - litellm_params={"api_version": "latest"}, - ) - assert ( - result_latest - == "https://litellm8397336933.openai.azure.com/openai/v1/responses?api-version=latest" - ) - - # Test with date-based version - should use openai/responses - result_date = self.config.get_complete_url( - api_base=base_url, - litellm_params={"api_version": "2025-01-01"}, - ) - assert ( - result_date - == "https://litellm8397336933.openai.azure.com/openai/responses?api-version=2025-01-01" - ) - - def test_azure_get_complete_url_with_default_api_version(self): - """Test Azure get_complete_url uses default API version when none is provided""" - from litellm.constants import AZURE_DEFAULT_RESPONSES_API_VERSION - - base_url = "https://litellm8397336933.openai.azure.com" - - # Test with no api_version provided - should use default - result_no_version = self.config.get_complete_url( - api_base=base_url, - litellm_params={}, - ) - expected_url = f"https://litellm8397336933.openai.azure.com/openai/responses?api-version={AZURE_DEFAULT_RESPONSES_API_VERSION}" - assert result_no_version == expected_url - - # Test with empty litellm_params - should use default - result_empty_params = self.config.get_complete_url( - api_base=base_url, - litellm_params={}, - ) - assert result_empty_params == expected_url - - # Test with None api_version - should use default - result_none_version = self.config.get_complete_url( - api_base=base_url, - litellm_params={"api_version": None}, - ) - assert result_none_version == expected_url - - class TestTransformListInputItemsRequest: """Test suite for transform_list_input_items_request function"""