Skip to content
33 changes: 25 additions & 8 deletions litellm/litellm_core_utils/get_supported_openai_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -130,16 +140,23 @@ 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
Comment thread
cursor[bot] marked this conversation as resolved.
)
else:
return litellm.AzureOpenAIConfig().get_supported_openai_params(model=model)
return litellm.AzureOpenAIConfig().get_supported_openai_params(
model=_azure_detection_model
)
elif custom_llm_provider == "openrouter":
return litellm.OpenrouterConfig().get_supported_openai_params(model=model)
elif custom_llm_provider == "vercel_ai_gateway":
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1476,7 +1476,7 @@ def _translate_streaming_openai_chunk_to_anthropic(
for choice in choices:
if choice.delta.content is not None and len(choice.delta.content) > 0:
text += choice.delta.content
if choice.delta.tool_calls is not None:
if choice.delta.tool_calls:
partial_json = ""
for tool in choice.delta.tool_calls:
if (
Expand Down
4 changes: 3 additions & 1 deletion litellm/llms/azure/azure.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,9 @@ def completion( # noqa: PLR0915
)

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,
Expand Down
52 changes: 51 additions & 1 deletion litellm/llms/openai/responses/transformation.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,9 +126,21 @@ def transform_responses_api_request(
litellm_params: GenericLiteLLMParams,
headers: dict,
) -> Dict:
"""No transform applied since inputs are in OpenAI spec already"""
"""Strip Anthropic-only `cache_control` markers before sending to OpenAI.

OpenAI's Responses API rejects unknown fields on input content blocks
with HTTP 400 ("Unknown parameter: 'input[0].content[0].cache_control'").
Chat Completions strips these in
`remove_cache_control_flag_from_messages_and_tools`; mirror that here.
"""

input = self._validate_input_param(input)
tools = response_api_optional_request_params.get("tools")
input, tools = self.remove_cache_control_flag_from_input_and_tools(
model=model, input=input, tools=tools
)
if tools is not None:
response_api_optional_request_params["tools"] = tools
final_request_params = dict(
ResponsesAPIRequestParams(
model=model, input=input, **response_api_optional_request_params
Expand All @@ -137,6 +149,38 @@ def transform_responses_api_request(

return final_request_params

def remove_cache_control_flag_from_input_and_tools(
self,
model: str, # allows overrides to selectively run this
input: Union[str, ResponseInputParam],
tools: Optional[List[ALL_RESPONSES_API_TOOL_PARAMS]] = None,
) -> Tuple[
Union[str, ResponseInputParam],
Optional[List[ALL_RESPONSES_API_TOOL_PARAMS]],
]:
"""Sibling of `remove_cache_control_flag_from_messages_and_tools` on
the chat path. Strips Anthropic-only `cache_control` markers from
Responses API input content blocks and tools.

`filter_value_from_dict` mutates each dict in place, so the same
objects are returned.
"""
from litellm.litellm_core_utils.prompt_templates.common_utils import (
filter_value_from_dict,
)

if isinstance(input, list):
for item in input:
if isinstance(item, dict):
filter_value_from_dict(cast(dict, item), "cache_control")

if tools is not None:
for tool in tools:
if isinstance(tool, dict):
filter_value_from_dict(cast(dict, tool), "cache_control")

return input, tools

Comment thread
Sameerlite marked this conversation as resolved.
def _validate_input_param(
self, input: Union[str, ResponseInputParam]
) -> Union[str, ResponseInputParam]:
Expand Down Expand Up @@ -604,6 +648,12 @@ def transform_compact_response_api_request(
url = str(parsed_url.copy_with(path=compact_path))

input = self._validate_input_param(input)
tools = response_api_optional_request_params.get("tools")
input, tools = self.remove_cache_control_flag_from_input_and_tools(
model=model, input=input, tools=tools
)
if tools is not None:
response_api_optional_request_params["tools"] = tools
data = dict(
ResponsesAPIRequestParams(
model=model, input=input, **response_api_optional_request_params
Expand Down
17 changes: 14 additions & 3 deletions litellm/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -1670,6 +1673,10 @@ def completion( # type: ignore # noqa: PLR0915
reasoning_summary=_reasoning_summary_for_bridge,
)

# Use base_model (the true underlying model) for Azure model-type
# detection when the deployment name differs from the model name.
_azure_detection_model = base_model or model

if responses_api_model_info.get("mode") == "responses":
from litellm.completion_extras import responses_api_bridge

Expand Down Expand Up @@ -1713,7 +1720,9 @@ def completion( # type: ignore # noqa: PLR0915
and OpenAIGPT5Config.is_model_gpt_5_model(model)
) or (
custom_llm_provider == "azure"
and litellm.AzureOpenAIGPT5Config.is_model_gpt_5_model(model)
and litellm.AzureOpenAIGPT5Config.is_model_gpt_5_model(
_azure_detection_model
)
):
optional_params, _ = strip_reasoning_summary_aliases_from_optional_params(
optional_params
Expand Down Expand Up @@ -1766,7 +1775,9 @@ 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):
Comment thread
cursor[bot] marked this conversation as resolved.
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():
Expand Down
76 changes: 59 additions & 17 deletions litellm/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2933,6 +2933,13 @@ def register_model(model_cost: Union[str, dict]): # noqa: PLR0915
except Exception:
existing_model = {}
model_cost_key = key
# ``get_model_info`` returns ``litellm_provider: None`` when the
# provider is unknown (e.g. custom deployments registered via
# ``Router.add_deployment``). Persisting that None into
# ``litellm.model_cost`` causes ``_check_provider_match`` to drop
# custom pricing on subsequent cost lookups.
if existing_model.get("litellm_provider") is None:
existing_model.pop("litellm_provider", None)
## override / add new keys to the existing model cost dictionary
updated_dictionary = _update_dictionary(existing_model, value)
litellm.model_cost.setdefault(model_cost_key, {}).update(updated_dictionary)
Expand Down Expand Up @@ -4019,16 +4026,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,
Expand Down Expand Up @@ -4091,7 +4105,7 @@ def _check_valid_arg(supported_params: List[str]):
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(
Expand Down Expand Up @@ -4702,22 +4716,27 @@ def _check_valid_arg(supported_params: List[str]):
),
)
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)
Expand All @@ -4739,7 +4758,7 @@ def _check_valid_arg(supported_params: List[str]):
optional_params = litellm.AzureOpenAIConfig().map_openai_params(
non_default_params=non_default_params,
optional_params=optional_params,
model=model,
model=_azure_detection_model,
Comment thread
cursor[bot] marked this conversation as resolved.
api_version=api_version, # type: ignore
drop_params=(
drop_params
Expand Down Expand Up @@ -5510,9 +5529,15 @@ def _get_model_info_from_model_cost(key: str) -> dict:
def _check_provider_match(model_info: dict, custom_llm_provider: Optional[str]) -> bool:
"""
Check if the model info provider matches the custom provider.

A missing ``litellm_provider`` key and a ``litellm_provider`` set to
``None`` both mean "no specific provider constraint" and are treated
as a wildcard match. ``register_model`` may persist ``None`` here via
``get_model_info`` when a deployment is registered without a provider,
so normalising the two cases keeps custom pricing applied consistently.
"""
if custom_llm_provider and (
"litellm_provider" in model_info
model_info.get("litellm_provider") is not None
and model_info["litellm_provider"] != custom_llm_provider
):
if custom_llm_provider == "vertex_ai" and model_info[
Expand Down Expand Up @@ -8124,10 +8149,8 @@ def _build_provider_config_map() -> dict[LlmProviders, tuple[Callable, bool]]:
# 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,
Expand Down Expand Up @@ -8267,11 +8290,19 @@ def _build_provider_config_map() -> dict[LlmProviders, tuple[Callable, bool]]:
}

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

Expand Down Expand Up @@ -8329,13 +8360,18 @@ def _get_langgraph_config() -> BaseConfig:

@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:
Expand All @@ -8344,6 +8380,12 @@ def get_provider_chat_config( # noqa: PLR0915
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 = (
Expand Down
Loading
Loading