[Feature][Bug Fix] Decouple Azure OpenAI Deployment ID from model name via base_model to fix gpt5 model routing (#28490)

* feat(azure): decouple deployment ID from model name via base_model

Azure OpenAI deployments have arbitrary names (deployment IDs) that may
not match the underlying model. Previously, model-type detection
(o-series, gpt-5, etc.) relied on substring matching against the
deployment name, causing misrouted configs and rejected params when
deployment names were non-standard (e.g. 'my-deployment-id' for gpt-5.2).

This change extends the existing base_model field to drive model-type
detection, config selection, supported param resolution, and param
mapping throughout the Azure call path:

- _get_azure_config() uses base_model for is_o_series/is_gpt_5 checks
- get_provider_chat_config() threads base_model for Azure
- get_supported_openai_params() accepts and uses base_model
- get_optional_params() accepts base_model and passes it to all Azure
  config method calls (get_supported_openai_params, map_openai_params)
- azure.py completion handler uses base_model for GPT-5 detection
- Config internal methods (e.g. is_model_gpt_5_2_model) now receive
  base_model so features like logprobs are correctly enabled

Fully backward compatible - when base_model is unset, behavior is
identical. Existing o_series/ and gpt5_series/ prefix workarounds
continue to work.

Usage in proxy config:
  model_list:
    - model_name: my-gpt5
      litellm_params:
        model: azure/my-deployment-id
      model_info:
        base_model: azure/gpt-5.2

Fixes: non-standard deployment names like 'prefix-gpt-5.2' rejecting
logprobs/top_logprobs despite the underlying model supporting them.

* Addressing Greptile comments.
This commit is contained in:
withomasmicrosoft 2026-05-22 04:50:08 -07:00 committed by GitHub
parent 0168f0f259
commit 1c80fa9c3b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 354 additions and 26 deletions

View file

@ -11,6 +11,7 @@ def get_supported_openai_params( # noqa: PLR0915
request_type: Literal[
"chat_completion", "embeddings", "transcription"
] = "chat_completion",
base_model: Optional[str] = None,
) -> Optional[list]:
"""
Returns the supported openai params for a given model + provider
@ -20,6 +21,11 @@ def get_supported_openai_params( # noqa: PLR0915
get_supported_openai_params(model="anthropic.claude-3", custom_llm_provider="bedrock")
```
Args:
base_model: For Azure, the true underlying model (e.g. ``"azure/gpt-5.2"``)
when the deployment name differs. Used for model-type detection so that
non-standard deployment names route to the correct config.
Returns:
- List if custom_llm_provider is mapped
- None if unmapped
@ -32,17 +38,21 @@ def get_supported_openai_params( # noqa: PLR0915
if custom_llm_provider in LlmProvidersSet:
provider_config = litellm.ProviderConfigManager.get_provider_chat_config(
model=model, provider=LlmProviders(custom_llm_provider)
model=model,
provider=LlmProviders(custom_llm_provider),
base_model=base_model,
)
elif custom_llm_provider.split("/")[0] in LlmProvidersSet:
provider_config = litellm.ProviderConfigManager.get_provider_chat_config(
model=model, provider=LlmProviders(custom_llm_provider.split("/")[0])
model=model,
provider=LlmProviders(custom_llm_provider.split("/")[0]),
base_model=base_model,
)
else:
provider_config = None
if provider_config and request_type == "chat_completion":
return provider_config.get_supported_openai_params(model=model)
return provider_config.get_supported_openai_params(model=base_model or model)
if custom_llm_provider == "bedrock":
return litellm.AmazonConverseConfig().get_supported_openai_params(model=model)
@ -130,13 +140,18 @@ def get_supported_openai_params( # noqa: PLR0915
model=model
)
elif custom_llm_provider == "azure":
if litellm.AzureOpenAIO1Config().is_o_series_model(model=model):
_azure_detection_model = base_model or model
if litellm.AzureOpenAIO1Config().is_o_series_model(
model=_azure_detection_model
):
return litellm.AzureOpenAIO1Config().get_supported_openai_params(
model=model
model=_azure_detection_model
)
elif litellm.AzureOpenAIGPT5Config.is_model_gpt_5_model(model=model):
elif litellm.AzureOpenAIGPT5Config.is_model_gpt_5_model(
model=_azure_detection_model
):
return litellm.AzureOpenAIGPT5Config().get_supported_openai_params(
model=model
model=_azure_detection_model
)
else:
return litellm.AzureOpenAIConfig().get_supported_openai_params(model=model)

View file

@ -239,7 +239,9 @@ class AzureChatCompletion(BaseAzureLLM, BaseLLM):
)
data = {"model": None, "messages": messages, **optional_params}
elif litellm.AzureOpenAIGPT5Config.is_model_gpt_5_model(model=model):
elif litellm.AzureOpenAIGPT5Config.is_model_gpt_5_model(
model=litellm_params.get("base_model") or model
):
data = litellm.AzureOpenAIGPT5Config().transform_request(
model=model,
messages=messages,

View file

@ -1491,7 +1491,9 @@ def completion( # type: ignore # noqa: PLR0915
provider.value for provider in LlmProviders
]:
provider_config = ProviderConfigManager.get_provider_chat_config(
model=model, provider=LlmProviders(custom_llm_provider)
model=model,
provider=LlmProviders(custom_llm_provider),
base_model=base_model,
)
if provider_config is not None:
@ -1550,6 +1552,7 @@ def completion( # type: ignore # noqa: PLR0915
"safety_identifier": safety_identifier,
"service_tier": service_tier,
"allowed_openai_params": kwargs.get("allowed_openai_params"),
"base_model": base_model,
}
optional_params = get_optional_params(
**optional_param_args, **non_default_params
@ -1766,7 +1769,12 @@ def completion( # type: ignore # noqa: PLR0915
if max_retries is not None:
optional_params["max_retries"] = max_retries
if litellm.AzureOpenAIO1Config().is_o_series_model(model=model):
# Use base_model (the true underlying model) for model-type
# detection when the deployment name differs from the model name.
_azure_detection_model = base_model or model
if litellm.AzureOpenAIO1Config().is_o_series_model(
model=_azure_detection_model
):
## LOAD CONFIG - if set
config = litellm.AzureOpenAIO1Config.get_config()
for k, v in config.items():

View file

@ -4019,16 +4019,23 @@ def get_optional_params( # noqa: PLR0915
thinking: Optional[AnthropicThinkingParam] = None,
web_search_options: Optional[OpenAIWebSearchOptions] = None,
safety_identifier: Optional[str] = None,
base_model: Optional[str] = None,
**kwargs,
):
passed_params = locals().copy()
special_params = passed_params.pop("kwargs")
# Remove base_model from passed_params so it doesn't interfere with
# non_default_params / _check_valid_arg — it's a routing hint, not an
# OpenAI param.
passed_params.pop("base_model", None)
provider_config: Optional[BaseConfig] = None
if custom_llm_provider is not None and custom_llm_provider in [
provider.value for provider in LlmProviders
]:
provider_config = ProviderConfigManager.get_provider_chat_config(
model=model, provider=LlmProviders(custom_llm_provider)
model=model,
provider=LlmProviders(custom_llm_provider),
base_model=base_model,
)
non_default_params = pre_process_non_default_params(
passed_params=passed_params,
@ -4091,7 +4098,7 @@ def get_optional_params( # noqa: PLR0915
sys.modules[__name__], "get_supported_openai_params"
)
supported_params = get_supported_openai_params(
model=model, custom_llm_provider=custom_llm_provider
model=model, custom_llm_provider=custom_llm_provider, base_model=base_model
)
if supported_params is None:
supported_params = get_supported_openai_params(
@ -4702,22 +4709,27 @@ def get_optional_params( # noqa: PLR0915
),
)
elif custom_llm_provider == "azure":
if litellm.AzureOpenAIO1Config().is_o_series_model(model=model):
_azure_detection_model = base_model or model
if litellm.AzureOpenAIO1Config().is_o_series_model(
model=_azure_detection_model
):
optional_params = litellm.AzureOpenAIO1Config().map_openai_params(
non_default_params=non_default_params,
optional_params=optional_params,
model=model,
model=_azure_detection_model,
drop_params=(
drop_params
if drop_params is not None and isinstance(drop_params, bool)
else False
),
)
elif litellm.AzureOpenAIGPT5Config.is_model_gpt_5_model(model=model):
elif litellm.AzureOpenAIGPT5Config.is_model_gpt_5_model(
model=_azure_detection_model
):
optional_params = litellm.AzureOpenAIGPT5Config().map_openai_params(
non_default_params=non_default_params,
optional_params=optional_params,
model=model,
model=_azure_detection_model,
drop_params=(
drop_params
if drop_params is not None and isinstance(drop_params, bool)
@ -4739,7 +4751,7 @@ def get_optional_params( # noqa: PLR0915
optional_params = litellm.AzureOpenAIConfig().map_openai_params(
non_default_params=non_default_params,
optional_params=optional_params,
model=model,
model=_azure_detection_model,
api_version=api_version, # type: ignore
drop_params=(
drop_params
@ -8124,10 +8136,8 @@ class ProviderConfigManager:
# Format: (factory_function, needs_model_parameter: bool)
LlmProviders.OPENAI: (lambda: litellm.OpenAIGPTConfig(), False),
LlmProviders.ANTHROPIC: (lambda: litellm.AnthropicConfig(), False),
LlmProviders.AZURE: (
lambda model: ProviderConfigManager._get_azure_config(model),
True,
),
# AZURE is handled as a special case in get_provider_chat_config()
# so that base_model can be threaded through for model-type detection.
LlmProviders.AZURE_AI: (
lambda model: ProviderConfigManager._get_azure_ai_config(model),
True,
@ -8267,11 +8277,19 @@ class ProviderConfigManager:
}
@staticmethod
def _get_azure_config(model: str) -> BaseConfig:
"""Get Azure config based on model type."""
if litellm.AzureOpenAIO1Config().is_o_series_model(model=model):
def _get_azure_config(model: str, base_model: Optional[str] = None) -> BaseConfig:
"""Get Azure config based on model type.
When *base_model* is provided (e.g. ``"azure/gpt-5.2"``), it is used
for model-type detection instead of *model* (the deployment name).
This allows non-standard deployment names like ``"azure/foo"`` to be
routed through the correct config when the user specifies the true
underlying model via ``base_model``.
"""
detection_model = base_model or model
if litellm.AzureOpenAIO1Config().is_o_series_model(model=detection_model):
return litellm.AzureOpenAIO1Config()
if litellm.AzureOpenAIGPT5Config.is_model_gpt_5_model(model=model):
if litellm.AzureOpenAIGPT5Config.is_model_gpt_5_model(model=detection_model):
return litellm.AzureOpenAIGPT5Config()
return litellm.AzureOpenAIConfig()
@ -8329,13 +8347,18 @@ class ProviderConfigManager:
@staticmethod
def get_provider_chat_config( # noqa: PLR0915
model: str, provider: LlmProviders
model: str,
provider: LlmProviders,
base_model: Optional[str] = None,
) -> Optional[BaseConfig]:
"""
Returns the provider config for a given provider.
Uses O(1) dictionary lookup for fast provider resolution.
Python classes take priority over JSON (they have custom overrides).
For Azure, *base_model* (when set) drives model-type detection so that
non-standard deployment names still route to the correct config.
"""
# Handle OpenAI special cases (O-series and GPT-5 models)
if provider == LlmProviders.OPENAI:
@ -8344,6 +8367,12 @@ class ProviderConfigManager:
if litellm.OpenAIGPT5Config.is_model_gpt_5_model(model=model):
return litellm.OpenAIGPT5Config()
# Handle Azure before the generic map so base_model can be threaded through
if provider == LlmProviders.AZURE:
return ProviderConfigManager._get_azure_config(
model=model, base_model=base_model
)
# Initialize provider config map lazily (avoids circular imports)
if ProviderConfigManager._PROVIDER_CONFIG_MAP is None:
ProviderConfigManager._PROVIDER_CONFIG_MAP = (

View file

@ -0,0 +1,274 @@
"""Tests for decoupling Azure deployment IDs from underlying model names.
When users name their Azure deployment something non-standard (e.g. "my-deployment-id"),
setting ``base_model`` should drive model-type detection (o-series, gpt-5,
etc.) so the correct config, supported params, and param mapping are used.
"""
import pytest
import litellm
from litellm.llms.azure.chat.gpt_5_transformation import AzureOpenAIGPT5Config
from litellm.llms.azure.chat.o_series_transformation import AzureOpenAIO1Config
from litellm.utils import ProviderConfigManager, get_optional_params
# ---------------------------------------------------------------------------
# _get_azure_config — routes to the correct config based on base_model
# ---------------------------------------------------------------------------
class TestGetAzureConfigWithBaseModel:
"""ProviderConfigManager._get_azure_config should use base_model for detection."""
def test_should_return_gpt5_config_when_base_model_is_gpt5(self):
config = ProviderConfigManager._get_azure_config(
model="my-deployment-id", base_model="azure/gpt-5.2"
)
assert isinstance(config, AzureOpenAIGPT5Config)
def test_should_return_o_series_config_when_base_model_is_o_series(self):
config = ProviderConfigManager._get_azure_config(
model="my-deployment-id", base_model="azure/o4-mini"
)
assert isinstance(config, AzureOpenAIO1Config)
def test_should_return_default_config_when_base_model_is_regular(self):
config = ProviderConfigManager._get_azure_config(
model="my-deployment-id", base_model="azure/gpt-4o"
)
assert type(config).__name__ == "AzureOpenAIConfig"
def test_should_fallback_to_model_when_base_model_is_none(self):
config = ProviderConfigManager._get_azure_config(
model="gpt-5.2", base_model=None
)
assert isinstance(config, AzureOpenAIGPT5Config)
def test_should_return_default_config_when_both_are_non_standard(self):
config = ProviderConfigManager._get_azure_config(
model="my-deployment-id", base_model=None
)
assert type(config).__name__ == "AzureOpenAIConfig"
# ---------------------------------------------------------------------------
# get_provider_chat_config — threads base_model through for Azure
# ---------------------------------------------------------------------------
class TestGetProviderChatConfigWithBaseModel:
"""get_provider_chat_config should pass base_model to Azure config selection."""
def test_should_return_gpt5_config_for_custom_deployment_with_base_model(self):
from litellm.types.utils import LlmProviders
config = ProviderConfigManager.get_provider_chat_config(
model="my-deployment-id",
provider=LlmProviders.AZURE,
base_model="azure/gpt-5",
)
assert isinstance(config, AzureOpenAIGPT5Config)
def test_should_return_o_series_config_for_custom_deployment_with_base_model(self):
from litellm.types.utils import LlmProviders
config = ProviderConfigManager.get_provider_chat_config(
model="my-other-deployment",
provider=LlmProviders.AZURE,
base_model="azure/o3-mini",
)
assert isinstance(config, AzureOpenAIO1Config)
# ---------------------------------------------------------------------------
# get_supported_openai_params — base_model drives Azure param detection
# ---------------------------------------------------------------------------
class TestGetSupportedOpenAIParamsWithBaseModel:
"""get_supported_openai_params should use base_model for Azure detection."""
def test_should_return_gpt5_params_for_custom_deployment_with_gpt5_base_model(
self,
):
params = litellm.get_supported_openai_params(
model="my-deployment-id",
custom_llm_provider="azure",
base_model="azure/gpt-5",
)
assert params is not None
assert "reasoning_effort" in params
# gpt-5 maps max_tokens -> max_completion_tokens, verifying we got GPT-5 config
assert "max_completion_tokens" in params
def test_should_return_o_series_params_for_custom_deployment_with_o_series_base_model(
self,
):
params = litellm.get_supported_openai_params(
model="my-other-deployment",
custom_llm_provider="azure",
base_model="azure/o4-mini",
)
assert params is not None
assert "reasoning_effort" in params
def test_should_return_regular_params_when_no_base_model(self):
"""When base_model is not set and model is non-standard, default Azure config."""
params = litellm.get_supported_openai_params(
model="my-deployment-id",
custom_llm_provider="azure",
)
assert params is not None
# Default Azure config supports temperature
assert "temperature" in params
# ---------------------------------------------------------------------------
# get_optional_params — base_model drives Azure param mapping
# ---------------------------------------------------------------------------
class TestGetOptionalParamsWithBaseModel:
"""get_optional_params should use base_model for Azure model-type detection."""
def test_should_map_max_tokens_for_custom_deployment_with_gpt5_base_model(self):
"""A non-standard deployment name + gpt-5 base_model should map max_tokens -> max_completion_tokens."""
params = get_optional_params(
model="my-deployment-id",
custom_llm_provider="azure",
max_tokens=100,
base_model="azure/gpt-5",
)
assert params.get("max_completion_tokens") == 100
assert "max_tokens" not in params
def test_should_keep_max_tokens_for_custom_deployment_without_base_model(self):
"""A non-standard deployment name without base_model should use default Azure config."""
params = get_optional_params(
model="my-deployment-id",
custom_llm_provider="azure",
max_tokens=100,
api_version="2024-05-01-preview",
)
# Default AzureOpenAIConfig keeps max_tokens as-is (or maps based on api_version)
assert "max_tokens" in params or "max_completion_tokens" in params
def test_should_support_reasoning_effort_for_custom_deployment_with_o_series_base_model(
self,
):
"""A non-standard deployment name + o-series base_model should accept reasoning_effort."""
params = get_optional_params(
model="my-other-deployment",
custom_llm_provider="azure",
reasoning_effort="low",
base_model="azure/o4-mini",
)
assert params.get("reasoning_effort") == "low"
def test_should_reject_temperature_for_custom_deployment_with_gpt5_base_model(
self,
):
"""A non-standard deployment + gpt-5 base_model should reject temperature."""
with pytest.raises(litellm.UnsupportedParamsError):
get_optional_params(
model="my-deployment-id",
custom_llm_provider="azure",
temperature=0.5,
base_model="azure/gpt-5",
)
# ---------------------------------------------------------------------------
# Backward compatibility — existing patterns still work
# ---------------------------------------------------------------------------
class TestBackwardCompatibility:
"""Existing model-name-based and prefix-based patterns must keep working."""
def test_should_detect_gpt5_from_model_name(self):
config = ProviderConfigManager._get_azure_config(model="gpt-5.2")
assert isinstance(config, AzureOpenAIGPT5Config)
def test_should_detect_gpt5_from_gpt5_series_prefix(self):
config = ProviderConfigManager._get_azure_config(
model="gpt5_series/my-deployment"
)
assert isinstance(config, AzureOpenAIGPT5Config)
def test_should_detect_o_series_from_model_name(self):
config = ProviderConfigManager._get_azure_config(model="o4-mini")
assert isinstance(config, AzureOpenAIO1Config)
def test_should_detect_o_series_from_o_series_prefix(self):
config = ProviderConfigManager._get_azure_config(model="o_series/my-deployment")
assert isinstance(config, AzureOpenAIO1Config)
def test_should_handle_gpt5_chat_model_correctly(self):
"""gpt-5-chat models should NOT be routed to GPT-5 config."""
config = ProviderConfigManager._get_azure_config(model="gpt-5-chat")
assert type(config).__name__ == "AzureOpenAIConfig"
def test_base_model_overrides_model_detection(self):
"""base_model should take priority over model for type detection."""
# model looks like o-series, but base_model says gpt-5
config = ProviderConfigManager._get_azure_config(
model="o3-mini", base_model="azure/gpt-5.2"
)
assert isinstance(config, AzureOpenAIGPT5Config)
# ---------------------------------------------------------------------------
# Deep config method awareness — base_model flows into config internals
# ---------------------------------------------------------------------------
class TestBaseModelFlowsIntoConfigInternals:
"""base_model should be used by config internal methods (e.g. is_model_gpt_5_2_model)."""
def test_should_support_logprobs_for_prefixed_deployment_with_gpt52_base_model(
self,
):
"""Deployment 'my-gpt-5.2' with base_model='azure/gpt-5.2' should support logprobs."""
params = litellm.get_supported_openai_params(
model="gpt5_series/my-gpt-5.2",
custom_llm_provider="azure",
base_model="azure/gpt-5.2",
)
assert params is not None
assert "logprobs" in params
assert "top_logprobs" in params
def test_should_support_logprobs_for_plain_deployment_with_gpt52_base_model(self):
"""Deployment 'my-deployment-id' with base_model='azure/gpt-5.2' should support logprobs."""
params = litellm.get_supported_openai_params(
model="my-deployment-id",
custom_llm_provider="azure",
base_model="azure/gpt-5.2",
)
assert params is not None
assert "logprobs" in params
assert "top_logprobs" in params
def test_should_not_support_logprobs_for_gpt5_base_model(self):
"""Deployment with base_model='azure/gpt-5' (not 5.2) should NOT support logprobs."""
params = litellm.get_supported_openai_params(
model="my-deployment-id",
custom_llm_provider="azure",
base_model="azure/gpt-5",
)
assert params is not None
assert "logprobs" not in params
assert "top_logprobs" not in params
def test_should_pass_logprobs_through_get_optional_params(self):
"""logprobs should pass validation in get_optional_params when base_model is gpt-5.2."""
params = get_optional_params(
model="gpt5_series/my-gpt-5.2",
custom_llm_provider="azure",
logprobs=True,
top_logprobs=5,
base_model="azure/gpt-5.2",
)
assert params.get("logprobs") is True
assert params.get("top_logprobs") == 5
def test_should_map_max_tokens_for_prefixed_deployment_with_gpt5_base_model(self):
"""my-gpt-5.2 with base_model should correctly map max_tokens -> max_completion_tokens."""
params = get_optional_params(
model="gpt5_series/my-gpt-5.2",
custom_llm_provider="azure",
max_tokens=200,
base_model="azure/gpt-5.2",
)
assert params.get("max_completion_tokens") == 200
assert "max_tokens" not in params