diff --git a/litellm/litellm_core_utils/get_supported_openai_params.py b/litellm/litellm_core_utils/get_supported_openai_params.py index b8cdc8210fc..7c4f9941523 100644 --- a/litellm/litellm_core_utils/get_supported_openai_params.py +++ b/litellm/litellm_core_utils/get_supported_openai_params.py @@ -22,9 +22,11 @@ def get_supported_openai_params( # noqa: PLR0915 ``` 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. + base_model: An optional capability hint for deployments whose ``model`` + label isn't recognized on its own (e.g. an Azure deployment name, or a + friendly Bedrock alias). It is additive: the result is the union of the + params supported by ``model`` and by ``base_model``, so a hint can only + add capabilities, never strip ones the real model already supports. Returns: - List if custom_llm_provider is mapped @@ -52,7 +54,15 @@ def get_supported_openai_params( # noqa: PLR0915 provider_config = None if provider_config and request_type == "chat_completion": - return provider_config.get_supported_openai_params(model=base_model or model) + supported_params = provider_config.get_supported_openai_params(model=model) + if base_model and base_model != model: + base_model_params = provider_config.get_supported_openai_params( + model=base_model + ) + supported_params = list( + dict.fromkeys([*supported_params, *base_model_params]) + ) + return supported_params if custom_llm_provider == "bedrock": return litellm.AmazonConverseConfig().get_supported_openai_params(model=model) diff --git a/litellm/main.py b/litellm/main.py index da8624d11b8..62b64716e96 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -1319,7 +1319,9 @@ def completion( # type: ignore # noqa: PLR0915 preset_cache_key = kwargs.get("preset_cache_key", None) hf_model_name = kwargs.get("hf_model_name", None) supports_system_message = kwargs.get("supports_system_message", None) - base_model = kwargs.get("base_model", None) + base_model = kwargs.get("base_model", None) or ( + model_info.get("base_model") if isinstance(model_info, dict) else None + ) ### DISABLE FLAGS ### disable_add_transform_inline_image_block = kwargs.get( "disable_add_transform_inline_image_block", None @@ -1531,11 +1533,7 @@ def completion( # type: ignore # noqa: PLR0915 "logit_bias": logit_bias, "user": user, # params to identify the model - "model": ( - model_info.get("base_model") - if isinstance(model_info, dict) and model_info.get("base_model") - else model - ), + "model": model, "custom_llm_provider": custom_llm_provider, "response_format": response_format, "seed": seed, diff --git a/tests/test_litellm/litellm_core_utils/test_get_supported_openai_params.py b/tests/test_litellm/litellm_core_utils/test_get_supported_openai_params.py new file mode 100644 index 00000000000..3c280c6ba92 --- /dev/null +++ b/tests/test_litellm/litellm_core_utils/test_get_supported_openai_params.py @@ -0,0 +1,134 @@ +import os +import sys + +import pytest + +sys.path.insert(0, os.path.abspath("../../..")) + +from litellm.litellm_core_utils.get_supported_openai_params import ( + get_supported_openai_params, +) + +BEDROCK_REAL_MODEL = "eu.anthropic.claude-haiku-4-5-20251001-v1:0" +BEDROCK_LABEL = "claude-haiku-4-5" + + +def test_base_model_label_does_not_strip_bedrock_tools(): + """Regression for #29618. + + A Bedrock deployment whose ``model_info.base_model`` is a friendly label + (``claude-haiku-4-5``) must still advertise ``tools``/``tool_choice``. The label + on its own resolves to no tool support, so before the fix it stripped the + capability the real model id exposes, silently dropping function calling under + ``drop_params``.""" + params = get_supported_openai_params( + model=BEDROCK_REAL_MODEL, + custom_llm_provider="bedrock", + base_model=BEDROCK_LABEL, + ) + + assert params is not None + assert "tools" in params + assert "tool_choice" in params + + +def test_base_model_label_alone_lacks_bedrock_tools(): + """The label by itself does not advertise tools; this is what made the union + necessary. Guards against the discrepancy disappearing (and the regression test + above silently passing for the wrong reason).""" + params = get_supported_openai_params( + model=BEDROCK_LABEL, custom_llm_provider="bedrock" + ) + + assert params is not None + assert "tools" not in params + + +def test_base_model_is_additive_not_replacement(): + """``base_model`` may only add capabilities, never remove ones the real model has. + + Bedrock: real id supports ``tools`` but not the label's reasoning hint; the union + must contain the real model's ``tools`` regardless of the label being a subset.""" + real_only = set( + get_supported_openai_params( + model=BEDROCK_REAL_MODEL, custom_llm_provider="bedrock" + ) + ) + label_only = set( + get_supported_openai_params(model=BEDROCK_LABEL, custom_llm_provider="bedrock") + ) + combined = set( + get_supported_openai_params( + model=BEDROCK_REAL_MODEL, + custom_llm_provider="bedrock", + base_model=BEDROCK_LABEL, + ) + ) + + assert combined == real_only | label_only + assert real_only - label_only # the label really is a strict subset here + assert real_only <= combined + + +def test_base_model_adds_capabilities_the_real_model_lacks(): + """Regression for #27717 (the behavior the union must preserve). + + ``gemini-3.1-pro`` isn't in the cost map so it advertises no reasoning support, + but the registered ``gemini-3.1-pro-preview`` base_model does. The hint must add + ``reasoning_effort``/``thinking`` without the call erroring.""" + real_only = set( + get_supported_openai_params( + model="gemini-3.1-pro", custom_llm_provider="gemini" + ) + ) + assert "reasoning_effort" not in real_only + + combined = set( + get_supported_openai_params( + model="gemini-3.1-pro", + custom_llm_provider="gemini", + base_model="gemini-3.1-pro-preview", + ) + ) + assert "reasoning_effort" in combined + assert "thinking" in combined + + +def test_no_base_model_is_unchanged(): + """Omitting ``base_model`` must resolve purely from ``model``.""" + with_none = get_supported_openai_params( + model=BEDROCK_REAL_MODEL, custom_llm_provider="bedrock", base_model=None + ) + plain = get_supported_openai_params( + model=BEDROCK_REAL_MODEL, custom_llm_provider="bedrock" + ) + + assert with_none == plain + + +def test_base_model_equal_to_model_is_unchanged(): + """A ``base_model`` identical to ``model`` must not double-resolve or reorder.""" + plain = get_supported_openai_params( + model=BEDROCK_REAL_MODEL, custom_llm_provider="bedrock" + ) + same = get_supported_openai_params( + model=BEDROCK_REAL_MODEL, + custom_llm_provider="bedrock", + base_model=BEDROCK_REAL_MODEL, + ) + + assert same == plain + + +def test_azure_base_model_detection_preserved(): + """Azure relies on ``base_model`` for model-type detection when the deployment + name is opaque; the union must keep advertising the gpt-5 capabilities.""" + params = get_supported_openai_params( + model="my-opaque-deployment", + custom_llm_provider="azure", + base_model="azure/gpt-5.2", + ) + + assert params is not None + assert "reasoning_effort" in params + assert "tools" in params diff --git a/tests/test_litellm/test_main.py b/tests/test_litellm/test_main.py index b03579c2dbd..113e1bc0df8 100644 --- a/tests/test_litellm/test_main.py +++ b/tests/test_litellm/test_main.py @@ -849,12 +849,13 @@ def test_gpt_5_4_responses_bridge_preserves_reasoning_summary_dict( @pytest.mark.parametrize( - "model, model_info, expected_model_param", + "model, model_info, expected_model_param, expected_base_model_param", [ - ("gemini/gemini-3.1-pro", None, "gemini-3.1-pro"), + ("gemini/gemini-3.1-pro", None, "gemini-3.1-pro", None), ( "gemini/gemini-3.1-pro", {"base_model": "gemini-3.1-pro-preview"}, + "gemini-3.1-pro", "gemini-3.1-pro-preview", ), ], @@ -863,7 +864,13 @@ def test_completion_optional_params_base_model( model: str, model_info: dict | None, expected_model_param: str, + expected_base_model_param: str | None, ): + """``model_info.base_model`` must reach ``get_optional_params`` as ``base_model`` + (an additive capability hint), without overwriting ``model`` with the label. + + Regression for #29618: overwriting ``model`` with a friendly ``base_model`` + label made Bedrock drop ``tools``/``tool_choice`` under ``drop_params``.""" with patch("litellm.main.get_optional_params") as mock_get_optional_params: mock_get_optional_params.return_value = MagicMock() @@ -881,10 +888,9 @@ def test_completion_optional_params_base_model( litellm.completion(**kwargs) assert mock_get_optional_params.called is True - get_optional_params_model_param = mock_get_optional_params.call_args.kwargs[ - "model" - ] - assert get_optional_params_model_param == expected_model_param + call_kwargs = mock_get_optional_params.call_args.kwargs + assert call_kwargs["model"] == expected_model_param + assert call_kwargs["base_model"] == expected_base_model_param @patch("litellm.completion_extras.responses_api_bridge.completion") diff --git a/tests/test_litellm/test_utils.py b/tests/test_litellm/test_utils.py index 2d75671f1cb..f2b9bef9230 100644 --- a/tests/test_litellm/test_utils.py +++ b/tests/test_litellm/test_utils.py @@ -4144,3 +4144,51 @@ def test_original_dict_not_mutated(self): validate_and_fix_thinking_param(thinking=thinking) assert "budgetTokens" in thinking assert "budget_tokens" not in thinking + + +class TestBedrockBaseModelLabelKeepsTools: + """Regression for #29618: a Bedrock deployment whose ``base_model`` is a friendly + label must not silently drop ``tools``/``tool_choice`` under ``drop_params``.""" + + TOOLS = [ + { + "type": "function", + "function": { + "name": "get_weather", + "parameters": { + "type": "object", + "properties": {"city": {"type": "string"}}, + }, + }, + } + ] + + def test_base_model_label_keeps_tools_with_drop_params(self): + from litellm.utils import get_optional_params + + result = get_optional_params( + model="eu.anthropic.claude-haiku-4-5-20251001-v1:0", + custom_llm_provider="bedrock", + base_model="claude-haiku-4-5", + tools=self.TOOLS, + tool_choice="auto", + drop_params=True, + ) + + assert "tools" in result + assert "tool_choice" in result + + def test_base_model_label_alone_drops_tools(self): + """Without the real model id the label resolves to no tool support, so passing + the label as ``model`` is exactly what dropped tools before the fix.""" + from litellm.utils import get_optional_params + + result = get_optional_params( + model="claude-haiku-4-5", + custom_llm_provider="bedrock", + tools=self.TOOLS, + tool_choice="auto", + drop_params=True, + ) + + assert "tools" not in result