From c6aa604c2dc99d5b3cc83181f364dc958948ce44 Mon Sep 17 00:00:00 2001 From: Hexecu Date: Mon, 15 Jun 2026 11:21:39 +0200 Subject: [PATCH 1/3] fix(azure): apply api_version gating to o-series and gpt-5 param mapping Azure deployments backed by the dedicated o-series (AzureOpenAIO1Config) and GPT-5 (AzureOpenAIGPT5Config) transformations bypassed the Azure api_version gating that the standard Azure path applies to tool_choice and response_format. Two sibling Azure deployments behaved differently for the same request: the non-o-series path correctly raised on tool_choice="required" for old api_versions and converted json_schema response_format into a tool call, while o-series and GPT-5 forwarded both params unchanged. Root cause: get_optional_params did not pass api_version to the o-series and GPT-5 map_openai_params calls, and AzureOpenAIO1Config inherited the OpenAI mapper (which is unaware of api_version). This routes tool_choice and response_format through AzureOpenAIConfig.map_openai_params so the gating is consistent across all Azure model families. --- .../llms/azure/chat/gpt_5_transformation.py | 24 +++++- .../azure/chat/o_series_transformation.py | 45 +++++++++++ litellm/utils.py | 14 ++-- ...test_azure_chat_o_series_transformation.py | 81 +++++++++++++++++++ .../chat/test_azure_gpt5_transformation.py | 68 ++++++++++++++++ 5 files changed, 225 insertions(+), 7 deletions(-) diff --git a/litellm/llms/azure/chat/gpt_5_transformation.py b/litellm/llms/azure/chat/gpt_5_transformation.py index e94f50380c0..1c49507eb5a 100644 --- a/litellm/llms/azure/chat/gpt_5_transformation.py +++ b/litellm/llms/azure/chat/gpt_5_transformation.py @@ -131,14 +131,36 @@ class AzureOpenAIGPT5Config(AzureOpenAIConfig, OpenAIGPT5Config): ), ) + azure_gated_params = { + param: value + for param, value in non_default_params.items() + if param in ("tool_choice", "response_format") + } + result = OpenAIGPT5Config.map_openai_params( self, - non_default_params=non_default_params, + non_default_params={ + param: value + for param, value in non_default_params.items() + if param not in azure_gated_params + }, optional_params=optional_params, model=model, drop_params=drop_params, ) + # tool_choice and response_format need Azure's api_version gating, which the + # OpenAI gpt-5 mapper does not apply. + if azure_gated_params: + result = AzureOpenAIConfig.map_openai_params( + self, + non_default_params=azure_gated_params, + optional_params=result, + model=model, + drop_params=drop_params, + api_version=api_version, + ) + # Only drop reasoning_effort='none' for models that don't support it result_effort = _get_effort_level(result.get("reasoning_effort")) if result_effort == "none" and not supports_none: diff --git a/litellm/llms/azure/chat/o_series_transformation.py b/litellm/llms/azure/chat/o_series_transformation.py index 0a73597a4e4..02d3e6a1633 100644 --- a/litellm/llms/azure/chat/o_series_transformation.py +++ b/litellm/llms/azure/chat/o_series_transformation.py @@ -20,9 +20,54 @@ from litellm.types.llms.openai import AllMessageValues from litellm.utils import get_model_info, supports_reasoning from ...openai.chat.o_series_transformation import OpenAIOSeriesConfig +from .gpt_transformation import AzureOpenAIConfig class AzureOpenAIO1Config(OpenAIOSeriesConfig): + def map_openai_params( + self, + non_default_params: dict, + optional_params: dict, + model: str, + drop_params: bool, + api_version: str = "", + ) -> dict: + """ + Map OpenAI params for Azure O-Series models. + + O-Series specific translations (e.g. max_tokens -> max_completion_tokens) are + handled by the OpenAI o-series mapper. tool_choice and response_format must + additionally go through the Azure api_version gating in AzureOpenAIConfig, which + the OpenAI mapper is not aware of. + """ + azure_gated_params = { + param: value + for param, value in non_default_params.items() + if param in ("tool_choice", "response_format") + } + + optional_params = super().map_openai_params( + non_default_params={ + param: value + for param, value in non_default_params.items() + if param not in azure_gated_params + }, + optional_params=optional_params, + model=model, + drop_params=drop_params, + ) + + if azure_gated_params: + optional_params = AzureOpenAIConfig().map_openai_params( + non_default_params=azure_gated_params, + optional_params=optional_params, + model=model, + drop_params=drop_params, + api_version=api_version, + ) + + return optional_params + def get_supported_openai_params(self, model: str) -> list: """ Get the supported OpenAI params for the Azure O-Series models diff --git a/litellm/utils.py b/litellm/utils.py index 4c67abdf937..b897ef54a71 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -4841,6 +4841,12 @@ def get_optional_params( # noqa: PLR0915 ) elif custom_llm_provider == "azure": _azure_detection_model = base_model or model + api_version = ( + api_version + or litellm.api_version + or get_secret("AZURE_API_VERSION") + or litellm.AZURE_DEFAULT_API_VERSION + ) if litellm.AzureOpenAIO1Config().is_o_series_model( model=_azure_detection_model ): @@ -4848,6 +4854,7 @@ def get_optional_params( # noqa: PLR0915 non_default_params=non_default_params, optional_params=optional_params, model=_azure_detection_model, + api_version=api_version, # type: ignore drop_params=( drop_params if drop_params is not None and isinstance(drop_params, bool) @@ -4861,6 +4868,7 @@ def get_optional_params( # noqa: PLR0915 non_default_params=non_default_params, optional_params=optional_params, model=_azure_detection_model, + api_version=api_version, # type: ignore drop_params=( drop_params if drop_params is not None and isinstance(drop_params, bool) @@ -4873,12 +4881,6 @@ def get_optional_params( # noqa: PLR0915 api_version, litellm.api_version, get_secret("AZURE_API_VERSION") ) ) - api_version = ( - api_version - or litellm.api_version - or get_secret("AZURE_API_VERSION") - or litellm.AZURE_DEFAULT_API_VERSION - ) optional_params = litellm.AzureOpenAIConfig().map_openai_params( non_default_params=non_default_params, optional_params=optional_params, diff --git a/tests/test_litellm/llms/azure/chat/test_azure_chat_o_series_transformation.py b/tests/test_litellm/llms/azure/chat/test_azure_chat_o_series_transformation.py index 31c76c42599..3c670fa6518 100644 --- a/tests/test_litellm/llms/azure/chat/test_azure_chat_o_series_transformation.py +++ b/tests/test_litellm/llms/azure/chat/test_azure_chat_o_series_transformation.py @@ -28,3 +28,84 @@ async def test_azure_chat_o_series_transformation(): ) print(response) assert response["model"] == "web-interface-o1-mini" + + +@pytest.mark.parametrize("model", ["o1", "o3-mini"]) +def test_azure_o_series_tool_choice_required_gated_by_api_version(model): + """ + tool_choice='required' is not supported by Azure on api_version<=2024-05-01. + + Azure o-series deployments must honor the same api_version gating as the + non-o-series Azure path instead of silently forwarding 'required'. + """ + tools = [ + { + "type": "function", + "function": { + "name": "f", + "parameters": {"type": "object", "properties": {}}, + }, + } + ] + with pytest.raises(litellm.UnsupportedParamsError): + litellm.get_optional_params( + model=model, + custom_llm_provider="azure", + tool_choice="required", + tools=tools, + api_version="2024-05-01-preview", + drop_params=False, + ) + + # newer api_version supports it + params = litellm.get_optional_params( + model=model, + custom_llm_provider="azure", + tool_choice="required", + tools=tools, + api_version="2025-01-01-preview", + drop_params=False, + ) + assert params["tool_choice"] == "required" + + +@pytest.mark.parametrize("model", ["o1", "o3-mini"]) +def test_azure_o_series_response_format_falls_back_to_tools_on_old_api_version(model): + """ + On api_versions that predate native json_schema support, Azure o-series should + convert response_format into a tool call, matching the non-o-series Azure path. + """ + response_format = { + "type": "json_schema", + "json_schema": { + "name": "r", + "schema": { + "type": "object", + "properties": {"a": {"type": "string"}}, + "required": ["a"], + }, + "strict": True, + }, + } + params = litellm.get_optional_params( + model=model, + custom_llm_provider="azure", + response_format=response_format, + api_version="2024-02-01", + drop_params=False, + ) + assert "response_format" not in params + assert "tools" in params + + +def test_azure_o_series_still_maps_max_tokens(): + """The o-series max_tokens -> max_completion_tokens translation must be preserved.""" + params = litellm.get_optional_params( + model="o3-mini", + custom_llm_provider="azure", + max_tokens=64, + api_version="2025-01-01-preview", + drop_params=False, + ) + assert params["max_completion_tokens"] == 64 + assert "max_tokens" not in params diff --git a/tests/test_litellm/llms/azure/chat/test_azure_gpt5_transformation.py b/tests/test_litellm/llms/azure/chat/test_azure_gpt5_transformation.py index 83562331b9a..3e7945f9863 100644 --- a/tests/test_litellm/llms/azure/chat/test_azure_gpt5_transformation.py +++ b/tests/test_litellm/llms/azure/chat/test_azure_gpt5_transformation.py @@ -299,3 +299,71 @@ def test_azure_gpt5_1_does_not_support_logprobs(config: AzureOpenAIGPT5Config): supported_params = config.get_supported_openai_params(model="gpt-5.1") assert "logprobs" not in supported_params assert "top_logprobs" not in supported_params + + +@pytest.mark.parametrize("model", ["gpt-5", "gpt-5.4-mini"]) +def test_azure_gpt5_tool_choice_required_gated_by_api_version(model): + """ + tool_choice='required' is not supported by Azure on api_version<=2024-05-01. + + Azure GPT-5 deployments must honor the same api_version gating as the + non-GPT-5 Azure path instead of silently forwarding 'required'. + """ + tools = [ + { + "type": "function", + "function": { + "name": "f", + "parameters": {"type": "object", "properties": {}}, + }, + } + ] + with pytest.raises(litellm.UnsupportedParamsError): + litellm.get_optional_params( + model=model, + custom_llm_provider="azure", + tool_choice="required", + tools=tools, + api_version="2024-05-01-preview", + drop_params=False, + ) + + # newer api_version supports it + params = litellm.get_optional_params( + model=model, + custom_llm_provider="azure", + tool_choice="required", + tools=tools, + api_version="2025-01-01-preview", + drop_params=False, + ) + assert params["tool_choice"] == "required" + + +@pytest.mark.parametrize("model", ["gpt-5", "gpt-5.4-mini"]) +def test_azure_gpt5_response_format_falls_back_to_tools_on_old_api_version(model): + """ + On api_versions that predate native json_schema support, Azure GPT-5 should + convert response_format into a tool call, matching the non-GPT-5 Azure path. + """ + response_format = { + "type": "json_schema", + "json_schema": { + "name": "r", + "schema": { + "type": "object", + "properties": {"a": {"type": "string"}}, + "required": ["a"], + }, + "strict": True, + }, + } + params = litellm.get_optional_params( + model=model, + custom_llm_provider="azure", + response_format=response_format, + api_version="2024-02-01", + drop_params=False, + ) + assert "response_format" not in params + assert "tools" in params From 037f0f2504783127d1111979a02d1ae9ed0c22b6 Mon Sep 17 00:00:00 2001 From: Hexecu Date: Mon, 15 Jun 2026 11:37:19 +0200 Subject: [PATCH 2/3] test(azure): cover non-gated param path for o-series and gpt-5 Add coverage for the branch where no api_version-gated params (tool_choice/response_format) are passed, ensuring the o-series and GPT-5 mappings still delegate cleanly to the OpenAI path. --- .../test_azure_chat_o_series_transformation.py | 18 ++++++++++++++++++ .../chat/test_azure_gpt5_transformation.py | 18 ++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/tests/test_litellm/llms/azure/chat/test_azure_chat_o_series_transformation.py b/tests/test_litellm/llms/azure/chat/test_azure_chat_o_series_transformation.py index 3c670fa6518..aa2872380cd 100644 --- a/tests/test_litellm/llms/azure/chat/test_azure_chat_o_series_transformation.py +++ b/tests/test_litellm/llms/azure/chat/test_azure_chat_o_series_transformation.py @@ -109,3 +109,21 @@ def test_azure_o_series_still_maps_max_tokens(): ) assert params["max_completion_tokens"] == 64 assert "max_tokens" not in params + + +def test_azure_o_series_without_gated_params_is_unchanged(): + """ + When no api_version-gated params (tool_choice/response_format) are passed, + the o-series mapping should behave exactly like the OpenAI o-series path. + """ + params = litellm.get_optional_params( + model="o3-mini", + custom_llm_provider="azure", + max_tokens=64, + reasoning_effort="high", + api_version="2024-02-01", + drop_params=False, + ) + assert params["max_completion_tokens"] == 64 + assert params["reasoning_effort"] == "high" + assert "tools" not in params diff --git a/tests/test_litellm/llms/azure/chat/test_azure_gpt5_transformation.py b/tests/test_litellm/llms/azure/chat/test_azure_gpt5_transformation.py index 3e7945f9863..18262b11fbf 100644 --- a/tests/test_litellm/llms/azure/chat/test_azure_gpt5_transformation.py +++ b/tests/test_litellm/llms/azure/chat/test_azure_gpt5_transformation.py @@ -367,3 +367,21 @@ def test_azure_gpt5_response_format_falls_back_to_tools_on_old_api_version(model ) assert "response_format" not in params assert "tools" in params + + +def test_azure_gpt5_without_gated_params_is_unchanged(): + """ + When no api_version-gated params (tool_choice/response_format) are passed, + the GPT-5 mapping should behave exactly like the OpenAI GPT-5 path. + """ + params = litellm.get_optional_params( + model="gpt-5", + custom_llm_provider="azure", + max_tokens=64, + reasoning_effort="high", + api_version="2024-02-01", + drop_params=False, + ) + assert params["max_completion_tokens"] == 64 + assert params["reasoning_effort"] == "high" + assert "tools" not in params From b2ed84139bb6b8d02df9338adda3e2b4a7b22ac7 Mon Sep 17 00:00:00 2001 From: Hexecu Date: Mon, 15 Jun 2026 11:43:16 +0200 Subject: [PATCH 3/3] docs(azure): clarify why o-series instantiates AzureOpenAIConfig AzureOpenAIO1Config does not inherit from AzureOpenAIConfig, unlike the GPT-5 path, so the Azure gating is reached via an instance. Add a short comment so the two call styles are not accidentally unified later. --- litellm/llms/azure/chat/o_series_transformation.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/litellm/llms/azure/chat/o_series_transformation.py b/litellm/llms/azure/chat/o_series_transformation.py index 02d3e6a1633..7b541e37322 100644 --- a/litellm/llms/azure/chat/o_series_transformation.py +++ b/litellm/llms/azure/chat/o_series_transformation.py @@ -58,6 +58,9 @@ class AzureOpenAIO1Config(OpenAIOSeriesConfig): ) if azure_gated_params: + # AzureOpenAIO1Config does not inherit from AzureOpenAIConfig, so the + # Azure gating is reached via an instance rather than the unbound-self + # dispatch used in the GPT-5 path. optional_params = AzureOpenAIConfig().map_openai_params( non_default_params=azure_gated_params, optional_params=optional_params,