From 0168f0f259d927b07dcef9ff186b17894c76e62f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Gar=C3=A9?= <90070734+FelipeRodriguesGare@users.noreply.github.com> Date: Fri, 22 May 2026 08:49:29 -0300 Subject: [PATCH 1/8] fix(anthropic): handle empty streaming tool calls (#28549) Co-authored-by: shin-berri Co-authored-by: yuneng-jiang --- .../adapters/transformation.py | 2 +- ...al_pass_through_adapters_transformation.py | 45 +++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py b/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py index 0e198daf089e..51a1e739a0f3 100644 --- a/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py +++ b/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py @@ -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 ( diff --git a/tests/test_litellm/llms/anthropic/experimental_pass_through/adapters/test_anthropic_experimental_pass_through_adapters_transformation.py b/tests/test_litellm/llms/anthropic/experimental_pass_through/adapters/test_anthropic_experimental_pass_through_adapters_transformation.py index 11465e6f7180..44530fecebdf 100644 --- a/tests/test_litellm/llms/anthropic/experimental_pass_through/adapters/test_anthropic_experimental_pass_through_adapters_transformation.py +++ b/tests/test_litellm/llms/anthropic/experimental_pass_through/adapters/test_anthropic_experimental_pass_through_adapters_transformation.py @@ -1203,6 +1203,51 @@ def test_streaming_chunk_with_both_text_and_tool_calls_issue_18238(): assert content_block_start["id"] == "toolu_bdrk_013xRVejhv3ybmLEGCoZib2b" +def test_streaming_chunk_with_text_and_empty_tool_calls_returns_text_delta(): + """ + Some OpenAI-compatible providers emit `tool_calls: []` on regular text chunks. + + Empty tool_calls should be treated as no tool call so the Anthropic adapter + does not shadow text with an empty input_json_delta. + """ + choices = [ + StreamingChoices( + finish_reason=None, + index=0, + delta=Delta( + provider_specific_fields=None, + content="Hello from vLLM", + role="assistant", + function_call=None, + tool_calls=[], + audio=None, + ), + logprobs=None, + ) + ] + + adapter = LiteLLMAnthropicMessagesAdapter() + + ( + type_of_content, + content_block_delta, + ) = adapter._translate_streaming_openai_chunk_to_anthropic(choices=choices) + + assert type_of_content == "text_delta" + assert content_block_delta["type"] == "text_delta" + assert content_block_delta["text"] == "Hello from vLLM" + + ( + block_type, + content_block_start, + ) = adapter._translate_streaming_openai_chunk_to_anthropic_content_block( + choices=choices + ) + + assert block_type == "text" + assert content_block_start == {"type": "text", "text": ""} + + # ============================================================================ # Cache Control Transformation Tests # ============================================================================ From 1c80fa9c3b83b485eb3becafee3eeb6536a3e3a2 Mon Sep 17 00:00:00 2001 From: withomasmicrosoft Date: Fri, 22 May 2026 04:50:08 -0700 Subject: [PATCH 2/8] [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. --- .../get_supported_openai_params.py | 29 +- litellm/llms/azure/azure.py | 4 +- litellm/main.py | 12 +- litellm/utils.py | 61 +++- .../chat/test_azure_base_model_routing.py | 274 ++++++++++++++++++ 5 files changed, 354 insertions(+), 26 deletions(-) create mode 100644 tests/test_litellm/llms/azure/chat/test_azure_base_model_routing.py diff --git a/litellm/litellm_core_utils/get_supported_openai_params.py b/litellm/litellm_core_utils/get_supported_openai_params.py index 9d8bd7523db4..46c643f3e37e 100644 --- a/litellm/litellm_core_utils/get_supported_openai_params.py +++ b/litellm/litellm_core_utils/get_supported_openai_params.py @@ -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) diff --git a/litellm/llms/azure/azure.py b/litellm/llms/azure/azure.py index 9291269d153d..734b8ecef166 100644 --- a/litellm/llms/azure/azure.py +++ b/litellm/llms/azure/azure.py @@ -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, diff --git a/litellm/main.py b/litellm/main.py index b5364f8ba172..16528c7cb3e9 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -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(): diff --git a/litellm/utils.py b/litellm/utils.py index 2487d39bd0de..03ca4bac5b6f 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -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 _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( @@ -4702,22 +4709,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) @@ -4739,7 +4751,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, api_version=api_version, # type: ignore drop_params=( drop_params @@ -8124,10 +8136,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, @@ -8267,11 +8277,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() @@ -8329,13 +8347,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: @@ -8344,6 +8367,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 = ( diff --git a/tests/test_litellm/llms/azure/chat/test_azure_base_model_routing.py b/tests/test_litellm/llms/azure/chat/test_azure_base_model_routing.py new file mode 100644 index 000000000000..1e8e23c38ca3 --- /dev/null +++ b/tests/test_litellm/llms/azure/chat/test_azure_base_model_routing.py @@ -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 From b9b715157bfdd7fc7eda91eb07169be3d6447f0f Mon Sep 17 00:00:00 2001 From: mubashir1osmani Date: Mon, 18 May 2026 23:27:52 -0400 Subject: [PATCH 3/8] gemini-3.1-flash-lite pricing (#27933) * feat(model_prices): add gemini-3.1-flash-lite pricing with standard/batch/flex/priority tiers * fix pricing * add service tier --------- Co-authored-by: shin-berri --- model_prices_and_context_window.json | 67 +++++++++++++++++++--------- 1 file changed, 47 insertions(+), 20 deletions(-) diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index 31a5993a240e..2140493ec4a0 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -15006,10 +15006,16 @@ "supports_service_tier": true }, "gemini-3.1-flash-lite": { - "cache_read_input_token_cost": 4.5e-08, - "cache_read_input_token_cost_per_audio_token": 9e-08, - "input_cost_per_audio_token": 9e-07, - "input_cost_per_token": 4.5e-07, + "cache_read_input_token_cost": 2.5e-08, + "cache_read_input_token_cost_batches": 1.25e-08, + "cache_read_input_token_cost_flex": 1.25e-08, + "cache_read_input_token_cost_per_audio_token": 5e-08, + "cache_read_input_token_cost_priority": 4.5e-08, + "input_cost_per_audio_token": 5e-07, + "input_cost_per_token": 2.5e-07, + "input_cost_per_token_batches": 1.25e-07, + "input_cost_per_token_flex": 1.25e-07, + "input_cost_per_token_priority": 4.5e-07, "litellm_provider": "vertex_ai-language-models", "max_audio_length_hours": 8.4, "max_audio_per_prompt": 1, @@ -15021,9 +15027,12 @@ "max_video_length": 1, "max_videos_per_prompt": 10, "mode": "chat", - "output_cost_per_reasoning_token": 2.7e-06, - "output_cost_per_token": 2.7e-06, - "source": "https://cloud.google.com/vertex-ai/generative-ai/pricing#gemini-models", + "output_cost_per_reasoning_token": 1.5e-06, + "output_cost_per_token": 1.5e-06, + "output_cost_per_token_batches": 7.5e-07, + "output_cost_per_token_flex": 7.5e-07, + "output_cost_per_token_priority": 2.7e-06, + "source": "https://ai.google.dev/gemini-api/docs/pricing#gemini-3.1-flash-lite", "supported_endpoints": [ "/v1/chat/completions", "/v1/completions", @@ -17128,10 +17137,16 @@ "supports_service_tier": true }, "gemini/gemini-3.1-flash-lite": { - "cache_read_input_token_cost": 4.5e-08, - "cache_read_input_token_cost_per_audio_token": 9e-08, - "input_cost_per_audio_token": 9e-07, - "input_cost_per_token": 4.5e-07, + "cache_read_input_token_cost": 2.5e-08, + "cache_read_input_token_cost_batches": 1.25e-08, + "cache_read_input_token_cost_flex": 1.25e-08, + "cache_read_input_token_cost_per_audio_token": 5e-08, + "cache_read_input_token_cost_priority": 4.5e-08, + "input_cost_per_audio_token": 5e-07, + "input_cost_per_token": 2.5e-07, + "input_cost_per_token_batches": 1.25e-07, + "input_cost_per_token_flex": 1.25e-07, + "input_cost_per_token_priority": 4.5e-07, "litellm_provider": "gemini", "max_audio_length_hours": 8.4, "max_audio_per_prompt": 1, @@ -17143,10 +17158,13 @@ "max_video_length": 1, "max_videos_per_prompt": 10, "mode": "chat", - "output_cost_per_reasoning_token": 2.7e-06, - "output_cost_per_token": 2.7e-06, + "output_cost_per_reasoning_token": 1.5e-06, + "output_cost_per_token": 1.5e-06, + "output_cost_per_token_batches": 7.5e-07, + "output_cost_per_token_flex": 7.5e-07, + "output_cost_per_token_priority": 2.7e-06, "rpm": 15, - "source": "https://ai.google.dev/gemini-api/docs/pricing", + "source": "https://ai.google.dev/gemini-api/docs/pricing#gemini-3.1-flash-lite", "supported_endpoints": [ "/v1/chat/completions", "/v1/completions", @@ -33932,10 +33950,16 @@ "web_search_billing_unit": "per_query" }, "vertex_ai/gemini-3.1-flash-lite": { - "cache_read_input_token_cost": 4.5e-08, - "cache_read_input_token_cost_per_audio_token": 9e-08, - "input_cost_per_audio_token": 9e-07, - "input_cost_per_token": 4.5e-07, + "cache_read_input_token_cost": 2.5e-08, + "cache_read_input_token_cost_batches": 1.25e-08, + "cache_read_input_token_cost_flex": 1.25e-08, + "cache_read_input_token_cost_per_audio_token": 5e-08, + "cache_read_input_token_cost_priority": 4.5e-08, + "input_cost_per_audio_token": 5e-07, + "input_cost_per_token": 2.5e-07, + "input_cost_per_token_batches": 1.25e-07, + "input_cost_per_token_flex": 1.25e-07, + "input_cost_per_token_priority": 4.5e-07, "litellm_provider": "vertex_ai-language-models", "max_audio_length_hours": 8.4, "max_audio_per_prompt": 1, @@ -33947,8 +33971,11 @@ "max_video_length": 1, "max_videos_per_prompt": 10, "mode": "chat", - "output_cost_per_reasoning_token": 2.7e-06, - "output_cost_per_token": 2.7e-06, + "output_cost_per_reasoning_token": 1.5e-06, + "output_cost_per_token": 1.5e-06, + "output_cost_per_token_batches": 7.5e-07, + "output_cost_per_token_flex": 7.5e-07, + "output_cost_per_token_priority": 2.7e-06, "source": "https://cloud.google.com/vertex-ai/generative-ai/pricing#gemini-models", "supported_endpoints": [ "/v1/chat/completions", From 6a13fed20915d6bf1060ce899f0227ab669d588c Mon Sep 17 00:00:00 2001 From: cwang-otto Date: Thu, 21 May 2026 10:55:28 +0800 Subject: [PATCH 4/8] fix(openai-responses): strip Anthropic cache_control from Responses API requests (#28431) Squash-merged by litellm-agent from cwang-otto's PR. --- .../llms/openai/responses/transformation.py | 46 +++++++++- .../test_openai_responses_transformation.py | 84 +++++++++++++++++++ 2 files changed, 129 insertions(+), 1 deletion(-) diff --git a/litellm/llms/openai/responses/transformation.py b/litellm/llms/openai/responses/transformation.py index b7d5340d8d46..5e7a501bf5fa 100644 --- a/litellm/llms/openai/responses/transformation.py +++ b/litellm/llms/openai/responses/transformation.py @@ -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 @@ -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 + def _validate_input_param( self, input: Union[str, ResponseInputParam] ) -> Union[str, ResponseInputParam]: diff --git a/tests/test_litellm/llms/openai/responses/test_openai_responses_transformation.py b/tests/test_litellm/llms/openai/responses/test_openai_responses_transformation.py index acb9fa9b64c0..4b2e9471fb79 100644 --- a/tests/test_litellm/llms/openai/responses/test_openai_responses_transformation.py +++ b/tests/test_litellm/llms/openai/responses/test_openai_responses_transformation.py @@ -86,6 +86,90 @@ def test_transform_responses_api_request(self): self.validate_responses_api_request_params(result, expected_fields) + def test_transform_strips_cache_control_from_input_content_blocks(self): + """`cache_control` markers (Anthropic-only) must be stripped from + Responses API input content blocks before sending to OpenAI. + + OpenAI rejects unknown params on input content blocks with HTTP 400: + "Unknown parameter: 'input[0].content[0].cache_control'" + Chat Completions strips these via + `remove_cache_control_flag_from_messages_and_tools`; the Responses + path must do the same. + """ + input_with_cache_control = [ + { + "role": "user", + "content": [ + { + "type": "input_text", + "text": "Hello", + "cache_control": {"type": "ephemeral"}, + } + ], + } + ] + + result = self.config.transform_responses_api_request( + model=self.model, + input=input_with_cache_control, + response_api_optional_request_params={}, + litellm_params={}, + headers={}, + ) + + assert "cache_control" not in result["input"][0]["content"][0] + assert result["input"][0]["content"][0]["type"] == "input_text" + assert result["input"][0]["content"][0]["text"] == "Hello" + + def test_transform_strips_cache_control_from_tools(self): + """`cache_control` markers must also be stripped from tools for + symmetry with the Chat Completions path. OpenAI currently accepts + cache_control on tools silently but stripping keeps the wire payload + clean and matches `remove_cache_control_flag_from_messages_and_tools`. + """ + tools_with_cache_control = [ + { + "type": "function", + "name": "get_weather", + "description": "Get the weather", + "parameters": { + "type": "object", + "properties": {"city": {"type": "string"}}, + }, + "cache_control": {"type": "ephemeral"}, + } + ] + + result = self.config.transform_responses_api_request( + model=self.model, + input="hi", + response_api_optional_request_params={"tools": tools_with_cache_control}, + litellm_params={}, + headers={}, + ) + + assert "cache_control" not in result["tools"][0] + assert result["tools"][0]["name"] == "get_weather" + + def test_transform_preserves_input_without_cache_control(self): + """Inputs without cache_control must pass through unmodified.""" + input_clean = [ + { + "role": "user", + "content": [{"type": "input_text", "text": "Hello"}], + } + ] + + result = self.config.transform_responses_api_request( + model=self.model, + input=input_clean, + response_api_optional_request_params={}, + litellm_params={}, + headers={}, + ) + + assert result["input"] == input_clean + def test_transform_streaming_response(self): """Test streaming response transformation""" # Test with a text delta event From 0372f6f0c7560a116d21be0c7edebc7a2767c70a Mon Sep 17 00:00:00 2001 From: Aditya Singh <60082699+adityasingh2400@users.noreply.github.com> Date: Thu, 21 May 2026 20:06:01 -0700 Subject: [PATCH 5/8] Treat None litellm_provider as wildcard in _check_provider_match (#28523) Squash-merged by litellm-agent from adityasingh2400's PR. --- litellm/utils.py | 15 +- .../test_register_model_custom_pricing.py | 161 ++++++++++++++++++ tests/test_litellm/test_utils.py | 28 +++ 3 files changed, 203 insertions(+), 1 deletion(-) diff --git a/litellm/utils.py b/litellm/utils.py index 03ca4bac5b6f..18ee811f0f12 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -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) @@ -5522,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[ diff --git a/tests/test_litellm/test_register_model_custom_pricing.py b/tests/test_litellm/test_register_model_custom_pricing.py index 1efd698fb64e..719cb8eecd2b 100644 --- a/tests/test_litellm/test_register_model_custom_pricing.py +++ b/tests/test_litellm/test_register_model_custom_pricing.py @@ -190,3 +190,164 @@ def test_build_custom_pricing_entry_time_based(): assert entry["litellm_provider"] == "openai" assert entry["input_cost_per_second"] == 0.01 assert entry["output_cost_per_second"] == 0.02 + + +def test_register_model_strips_none_litellm_provider(): + """``get_model_info`` returns ``litellm_provider: None`` for deployments + registered without a provider (e.g. ``Router.add_deployment`` flows). + ``register_model`` must not persist that None into ``model_cost``, + otherwise ``_check_provider_match`` will drop custom pricing on + subsequent cost lookups. + + Regression test for https://github.com/BerriAI/litellm/issues/28336. + """ + from litellm.utils import _check_provider_match + + model_key = "test-custom-pricing-no-provider-28336" + litellm.model_cost.pop(model_key, None) + + try: + litellm.register_model( + { + model_key: { + "input_cost_per_token": 0.001, + "output_cost_per_token": 0.002, + } + } + ) + + registered = litellm.model_cost.get(model_key) + assert registered is not None, f"{model_key} should be in model_cost" + # The key may be absent entirely, but if present it must not be None. + assert ( + "litellm_provider" not in registered + or registered["litellm_provider"] is not None + ) + # Downstream consumers must accept this entry for any provider, + # mirroring what the cost calculator does. + assert _check_provider_match(registered, "openai") is True + assert _check_provider_match(registered, "anthropic") is True + finally: + litellm.model_cost.pop(model_key, None) + + +def test_register_model_strips_none_litellm_provider_from_get_model_info(monkeypatch): + """Directly exercise the strip in ``register_model``. + + The companion test above hits the ``except Exception`` branch where + ``existing_model`` is an empty dict, so the ``pop`` is a no-op. This + test patches ``get_model_info`` to return the failure mode the strip + was added to handle, namely a populated dict whose ``litellm_provider`` + is ``None``. Without the strip, the merged entry in + ``litellm.model_cost`` would carry ``litellm_provider: None`` and + ``_check_provider_match`` would drop custom pricing. + + Regression test for https://github.com/BerriAI/litellm/issues/28336. + """ + from litellm import utils as litellm_utils + from litellm.utils import _check_provider_match + + model_key = "test-strip-none-provider-from-get-model-info-28336" + litellm.model_cost.pop(model_key, None) + + def _fake_get_model_info(model, *args, **kwargs): + assert model == model_key + return { + "key": model_key, + "litellm_provider": None, + "mode": "chat", + "max_tokens": 4096, + } + + # ``register_model`` calls ``get_model_info.cache_clear`` via + # ``_invalidate_model_cost_lowercase_map``, so the replacement must + # expose a no-op ``cache_clear`` attribute. + _fake_get_model_info.cache_clear = lambda: None + monkeypatch.setattr(litellm_utils, "get_model_info", _fake_get_model_info) + + try: + litellm.register_model( + { + model_key: { + "input_cost_per_token": 0.001, + "output_cost_per_token": 0.002, + } + } + ) + + registered = litellm.model_cost.get(model_key) + assert registered is not None, f"{model_key} should be in model_cost" + # The strip must have removed the None-valued provider that + # ``get_model_info`` returned. The key may be absent entirely, but + # it must never be present with value ``None``. + assert "litellm_provider" not in registered or ( + registered["litellm_provider"] is not None + ), ( + "register_model failed to strip litellm_provider=None returned " + f"by get_model_info, got {registered.get('litellm_provider')!r}" + ) + # Metadata from the patched ``get_model_info`` must still flow + # through, so we know the strip did not nuke the rest of the entry. + assert registered.get("mode") == "chat" + assert registered.get("max_tokens") == 4096 + # And custom pricing from the registration call must be preserved. + assert registered.get("input_cost_per_token") == 0.001 + assert registered.get("output_cost_per_token") == 0.002 + # Downstream _check_provider_match must accept any provider for + # this entry, mirroring the cost calculator path. + assert _check_provider_match(registered, "openai") is True + assert _check_provider_match(registered, "anthropic") is True + finally: + litellm.model_cost.pop(model_key, None) + + +def test_register_model_router_add_deployment_custom_pricing_applies(): + """End-to-end regression for https://github.com/BerriAI/litellm/issues/28336. + + ``Router.add_deployment`` registers custom pricing without passing + ``litellm_provider``. Cost calculation must still pick up the custom + pricing instead of falling back to the default provider price. + """ + from litellm import Router + + model_key = "router-add-deployment-custom-pricing-28336" + deployment_model = f"openai/{model_key}" + litellm.model_cost.pop(model_key, None) + litellm.model_cost.pop(deployment_model, None) + + router = Router( + model_list=[ + { + "model_name": model_key, + "litellm_params": { + "model": deployment_model, + "api_key": "fake-key-for-registration", + "input_cost_per_token": 0.00042, + "output_cost_per_token": 0.00084, + }, + "model_info": {"id": "deployment-28336"}, + } + ] + ) + + try: + # ``add_deployment`` runs as part of ``Router.__init__``; the + # registered entry must not block ``_check_provider_match`` for + # the deployment's provider. + from litellm.utils import _check_provider_match + + registered_keys = [ + k for k in (deployment_model, model_key) if k in litellm.model_cost + ] + assert registered_keys, ( + "Router.add_deployment did not register custom pricing for " + f"{model_key} / {deployment_model}" + ) + for k in registered_keys: + assert _check_provider_match(litellm.model_cost[k], "openai") is True, ( + f"custom pricing for {k} was dropped by _check_provider_match" + ) + finally: + litellm.model_cost.pop(model_key, None) + litellm.model_cost.pop(deployment_model, None) + del router diff --git a/tests/test_litellm/test_utils.py b/tests/test_litellm/test_utils.py index de286aede934..0efb3083139f 100644 --- a/tests/test_litellm/test_utils.py +++ b/tests/test_litellm/test_utils.py @@ -1140,6 +1140,34 @@ def test_check_provider_match(): assert litellm.utils._check_provider_match(model_info, "openai") is False +def test_check_provider_match_none_value_matches_any_provider(): + """ + A ``litellm_provider`` of None must be treated the same as a missing + key: both mean "no provider constraint" and should match any + ``custom_llm_provider``. + + Regression test for https://github.com/BerriAI/litellm/issues/28336. + Before the fix, ``register_model`` persisted ``litellm_provider: None`` + via ``get_model_info`` for deployments registered without a provider + (e.g. ``Router.add_deployment``), which caused ``_check_provider_match`` + to drop custom pricing intermittently. + """ + # Missing key already returned True; None must behave identically. + assert litellm.utils._check_provider_match({}, "openai") is True + assert ( + litellm.utils._check_provider_match({"litellm_provider": None}, "openai") + is True + ) + assert ( + litellm.utils._check_provider_match({"litellm_provider": None}, "anthropic") + is True + ) + # When custom_llm_provider is also None nothing constrains the match. + assert ( + litellm.utils._check_provider_match({"litellm_provider": None}, None) is True + ) + + def test_get_provider_rerank_config(): """ Test the get_provider_rerank_config function for various providers From 89e07ee31f44163e180c3d9c3fe118efd2846b5a Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Fri, 22 May 2026 18:32:27 +0530 Subject: [PATCH 6/8] fix greptile --- litellm/main.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/litellm/main.py b/litellm/main.py index 16528c7cb3e9..e17a5ad9a483 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -1673,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 @@ -1716,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 @@ -1769,9 +1775,6 @@ def completion( # type: ignore # noqa: PLR0915 if max_retries is not None: optional_params["max_retries"] = max_retries - # 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 ): From acba5537c892f29a6a8e3e46cdeba491e2a92e5e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 22 May 2026 13:11:42 +0000 Subject: [PATCH 7/8] fix: use _azure_detection_model in default Azure branch of get_supported_openai_params Co-authored-by: Yassin Kortam --- litellm/litellm_core_utils/get_supported_openai_params.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/litellm/litellm_core_utils/get_supported_openai_params.py b/litellm/litellm_core_utils/get_supported_openai_params.py index 46c643f3e37e..b8cdc8210fcc 100644 --- a/litellm/litellm_core_utils/get_supported_openai_params.py +++ b/litellm/litellm_core_utils/get_supported_openai_params.py @@ -154,7 +154,9 @@ def get_supported_openai_params( # noqa: PLR0915 model=_azure_detection_model ) 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": From c044ec3f6da2ef34ea9c3e7e5998d4f40dc8edee Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 22 May 2026 13:21:19 +0000 Subject: [PATCH 8/8] fix(openai-responses): strip cache_control on compact endpoint as well Co-authored-by: Yassin Kortam --- litellm/llms/openai/responses/transformation.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/litellm/llms/openai/responses/transformation.py b/litellm/llms/openai/responses/transformation.py index 5e7a501bf5fa..5043d25ee37a 100644 --- a/litellm/llms/openai/responses/transformation.py +++ b/litellm/llms/openai/responses/transformation.py @@ -648,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