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.
This commit is contained in:
Hexecu 2026-06-15 11:21:39 +02:00
parent 2655d1dd5e
commit c6aa604c2d
5 changed files with 225 additions and 7 deletions

View file

@ -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:

View file

@ -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

View file

@ -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,

View file

@ -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

View file

@ -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