diff --git a/litellm/llms/azure/responses/transformation.py b/litellm/llms/azure/responses/transformation.py index 6a0740b2f15..637d3cfc219 100644 --- a/litellm/llms/azure/responses/transformation.py +++ b/litellm/llms/azure/responses/transformation.py @@ -37,6 +37,16 @@ _AZURE_RESPONSES_PROMPT_CACHE_BREAKPOINT: Final = TypeAdapter(AzureResponsesProm _DATED_GA_API_VERSION: Final = re.compile(r"\d{4}-\d{2}-\d{2}") +_DATED_API_VERSION: Final = re.compile(r"\d{4}-\d{2}-\d{2}(?:-preview)?") +_MIN_RESPONSES_API_DATE: Final = "2025-03-01" + + +def _predates_responses_api(api_version: str | None) -> bool: + return ( + api_version is not None + and _DATED_API_VERSION.fullmatch(api_version) is not None + and api_version[:10] < _MIN_RESPONSES_API_DATE + ) def _str_param(litellm_params: Mapping[str, object], key: str) -> str | None: @@ -45,16 +55,14 @@ def _str_param(litellm_params: Mapping[str, object], key: str) -> str | None: def _resolve_responses_api_version(api_base: str | None, litellm_params: Mapping[str, object]) -> str | None: - """ - Dated GA api_versions (e.g. 2024-10-21) predate the Responses API on Azure and 404 there, so a deployment - pinned to one for chat gets the default Responses version unless it sets responses_api_version. - """ - api_version: Final = _str_param(litellm_params, "api_version") - if api_base is not None and "api-version" in httpx.URL(api_base).params: - return api_version responses_api_version: Final = _str_param(litellm_params, "responses_api_version") if responses_api_version is not None: return responses_api_version + api_version: Final = _str_param(litellm_params, "api_version") + if api_base is not None and "api-version" in httpx.URL(api_base).params: + return api_version + if _predates_responses_api(api_version): + return None if api_version is not None and _DATED_GA_API_VERSION.fullmatch(api_version): return AZURE_DEFAULT_RESPONSES_API_VERSION return api_version @@ -188,7 +196,7 @@ class AzureOpenAIResponsesAPIConfig(OpenAIResponsesAPIConfig): def get_complete_url( self, api_base: str | None, - litellm_params: dict, + litellm_params: dict[str, object], ) -> str: """ Constructs a complete URL for the API request. @@ -206,7 +214,17 @@ class AzureOpenAIResponsesAPIConfig(OpenAIResponsesAPIConfig): - A complete URL string, e.g., "https://litellm8397336933.openai.azure.com/openai/responses?api-version=2024-05-01-preview" """ - resolved_api_base: Final = BaseAzureLLM.resolve_azure_api_base(api_base) + base: Final = BaseAzureLLM.resolve_azure_api_base(api_base) + parsed_url: Final = httpx.URL(base) if base is not None else None + resolved_api_base: Final = ( + str(parsed_url.copy_with(params=parsed_url.params.remove("api-version"))) + if parsed_url is not None + and ( + _predates_responses_api(parsed_url.params.get("api-version")) + or _str_param(litellm_params, "responses_api_version") is not None + ) + else base + ) responses_params: Final = _with_responses_api_version(resolved_api_base, litellm_params) parsed_base: Final = urlsplit(resolved_api_base or "") path: Final = parsed_base.path.rstrip("/") diff --git a/tests/unit/llms/azure/response/test_azure_transformation.py b/tests/unit/llms/azure/response/test_azure_transformation.py index 05c0817e19b..3a8c9d96da4 100644 --- a/tests/unit/llms/azure/response/test_azure_transformation.py +++ b/tests/unit/llms/azure/response/test_azure_transformation.py @@ -93,13 +93,13 @@ def test_get_complete_url(): """ azure_openai_responses_apiconfig = AzureOpenAIResponsesAPIConfig() api_base = "https://litellm8397336933.openai.azure.com" - litellm_params = {"api_version": "2024-05-01-preview"} + litellm_params = {"api_version": "2025-04-01-preview"} result = azure_openai_responses_apiconfig.get_complete_url( api_base=api_base, litellm_params=litellm_params ) - expected = "https://litellm8397336933.openai.azure.com/openai/responses?api-version=2024-05-01-preview" + expected = "https://litellm8397336933.openai.azure.com/openai/responses?api-version=2025-04-01-preview" assert result == expected @@ -467,7 +467,12 @@ class TestAzureResponsesAPIConfig: litellm_params={"api_version": "2024-10-21", "responses_api_version": responses_api_version}, ) - assert url == api_base + expected: Final = ( + "https://litellm8397336933.openai.azure.com/openai/v1/responses?api-version=v1" + if responses_api_version is not None + else api_base + ) + assert url == expected def test_api_version_in_env_api_base_query_wins_over_a_dated_ga_api_version(self, monkeypatch: pytest.MonkeyPatch): env_api_base: Final = ( @@ -1015,6 +1020,43 @@ def _unset_api_version_responses_url() -> httpx.URL: return httpx.URL(AzureOpenAIResponsesAPIConfig().get_complete_url(api_base=_AZURE_RESOURCE, litellm_params={})) +@pytest.mark.parametrize("api_version", ["2025-02-01-preview", "2025-02-28", "2024-05-01-preview"]) +@pytest.mark.parametrize("source", ["params", "query", "both"]) +def test_pre_responses_api_version_uses_the_native_responses_default(api_version: str, source: str) -> None: + base: Final = ( + f"{_AZURE_RESOURCE}?api-version={api_version}&tenant=test" + if source in ("query", "both") + else f"{_AZURE_RESOURCE}?tenant=test" + ) + params: Final = {"api_version": api_version} if source in ("params", "both") else {} + url: Final = httpx.URL(AzureOpenAIResponsesAPIConfig().get_complete_url(api_base=base, litellm_params=params)) + expected: Final = _unset_api_version_responses_url() + + assert url.copy_with(query=None) == expected.copy_with(query=None) + assert dict(url.params) == {**dict(expected.params), "tenant": "test"} + + +@pytest.mark.parametrize("api_version", ["2025-03-01-preview", "2025-04-01-preview", "preview", "latest", "v1"]) +@pytest.mark.parametrize("source", ["params", "query"]) +def test_supported_responses_api_versions_are_preserved(api_version: str, source: str) -> None: + base: Final = f"{_AZURE_RESOURCE}?api-version={api_version}" if source == "query" else _AZURE_RESOURCE + params: Final = {"api_version": api_version} if source == "params" else {} + url: Final = httpx.URL(AzureOpenAIResponsesAPIConfig().get_complete_url(api_base=base, litellm_params=params)) + + assert url.params["api-version"] == api_version + + +@pytest.mark.parametrize("api_version", ["2025-02-01-preview", "2025-04-01-preview"]) +def test_responses_override_wins_over_query_and_chat_version(api_version: str) -> None: + config: Final = AzureOpenAIResponsesAPIConfig() + params: Final = {"api_version": api_version, "responses_api_version": "2025-03-01-preview"} + url: Final = config.get_complete_url( + api_base=f"{_AZURE_RESOURCE}?api-version={api_version}", litellm_params=params + ) + + assert url == f"{_AZURE_RESOURCE}/openai/responses?api-version=2025-03-01-preview" + + def _azure_responses_create(api_version: str) -> str | None: response: Final = litellm.responses( model="azure/gpt-5.4-nano",