From f13fcf5e1accead6a19149535ea8af243686d9d0 Mon Sep 17 00:00:00 2001 From: Priyansh Nandwana Date: Sat, 24 Oct 2026 15:01:58 +0530 Subject: [PATCH 1/3] fix(openai_like): keep tool params for models missing from the model map JSON-defined providers removed tools, tool_choice and the other tool params whenever supports_function_calling returned False, and that helper returns False for any model it cannot find. So every model a provider ships before it reaches the model map lost tool calling, silently under drop_params. Only drop the tool params when the model map explicitly says the model does not support function calling, which is how the Python-defined OpenAI compatible providers already behave. Fixes #48145 --- litellm/llms/openai_like/dynamic_config.py | 20 ++++++++--- .../llms/openai_like/test_dynamic_config.py | 35 ++++++++++++++++++- .../llms/openai_like/test_json_providers.py | 29 +++++++++------ 3 files changed, 67 insertions(+), 17 deletions(-) diff --git a/litellm/llms/openai_like/dynamic_config.py b/litellm/llms/openai_like/dynamic_config.py index 685561f55a5..27d8679e951 100644 --- a/litellm/llms/openai_like/dynamic_config.py +++ b/litellm/llms/openai_like/dynamic_config.py @@ -21,6 +21,18 @@ if TYPE_CHECKING: from litellm.llms.openai_like.responses.transformation import OpenAILikeResponsesConfig +def _function_calling_disabled_in_model_map(model: str, custom_llm_provider: str) -> bool: + from litellm.utils import get_model_info_helper + + try: + return ( + get_model_info_helper(model=model, custom_llm_provider=custom_llm_provider).get("supports_function_calling") + is False + ) + except Exception: # noqa: BLE001 # get_model_info_helper raises bare Exception for unmapped models + return False + + def create_config_class(provider: SimpleProviderConfig): """Generate config class dynamically from JSON configuration""" @@ -98,14 +110,12 @@ def create_config_class(provider: SimpleProviderConfig): def get_supported_openai_params(self, model: str) -> list: """Get supported OpenAI params, excluding tool-related params for models - that don't support function calling.""" - from litellm.utils import supports_function_calling, supports_reasoning + the model map marks as not supporting function calling.""" + from litellm.utils import supports_reasoning supported_params: Final = super().get_supported_openai_params(model=model) - _supports_fc: Final = supports_function_calling(model=model, custom_llm_provider=provider.slug) - - if not _supports_fc: + if _function_calling_disabled_in_model_map(model=model, custom_llm_provider=provider.slug): tool_params: Final = [ "tools", "tool_choice", diff --git a/tests/unit/llms/openai_like/test_dynamic_config.py b/tests/unit/llms/openai_like/test_dynamic_config.py index de70f98c3f1..3d5f380a7ef 100644 --- a/tests/unit/llms/openai_like/test_dynamic_config.py +++ b/tests/unit/llms/openai_like/test_dynamic_config.py @@ -1,7 +1,10 @@ +from typing import Final + import pytest +import litellm from litellm.llms.openai_like import dynamic_config -from litellm.llms.openai_like.dynamic_config import create_responses_config_class +from litellm.llms.openai_like.dynamic_config import create_config_class, create_responses_config_class from litellm.llms.openai_like.json_loader import SimpleProviderConfig from litellm.types.router import GenericLiteLLMParams @@ -139,3 +142,33 @@ class TestForceStoreFalse: headers={}, ) assert params["store"] is True + + +class TestToolParamsFollowTheModelMap: + _TOOLS: Final = [{"type": "function", "function": {"name": "get_weather", "parameters": {"type": "object"}}}] + + @pytest.mark.parametrize( + ("model_map_entry", "tools_forwarded"), + [ + (None, True), + ({"litellm_provider": "tools_map", "mode": "chat", "supports_function_calling": True}, True), + ({"litellm_provider": "tools_map", "mode": "chat", "supports_function_calling": False}, False), + ], + ids=["unmapped", "mapped_as_supported", "mapped_as_unsupported"], + ) + def test_tools_are_dropped_only_when_the_model_map_says_unsupported( + self, model_map_entry: dict | None, tools_forwarded: bool, monkeypatch: pytest.MonkeyPatch + ) -> None: + if model_map_entry is not None: + monkeypatch.setitem(litellm.model_cost, "tools_map/some-model", model_map_entry) + config: Final = create_config_class(_provider("tools_map"))() + + optional_params: Final = config.map_openai_params( + non_default_params={"tools": self._TOOLS, "tool_choice": "auto"}, + optional_params={}, + model="some-model", + drop_params=True, + ) + + assert ("tools" in optional_params) is tools_forwarded + assert ("tool_choice" in optional_params) is tools_forwarded diff --git a/tests/unit/llms/openai_like/test_json_providers.py b/tests/unit/llms/openai_like/test_json_providers.py index 9dbdaa28a76..2f8ef151ed9 100644 --- a/tests/unit/llms/openai_like/test_json_providers.py +++ b/tests/unit/llms/openai_like/test_json_providers.py @@ -4,7 +4,6 @@ Tests for JSON-based provider configuration system. import os import sys -from unittest.mock import patch try: import pytest @@ -94,20 +93,24 @@ class TestJSONProviderLoader: assert isinstance(supported, list) assert len(supported) > 0 - def test_tool_params_excluded_when_function_calling_not_supported(self): - """Test that tool-related params are excluded for models that don't support - function calling. Regression test for https://github.com/BerriAI/litellm/issues/21125 + def test_tool_params_excluded_when_function_calling_not_supported(self, monkeypatch): + """Test that tool-related params are excluded for models the model map marks as not + supporting function calling. Regression test for https://github.com/BerriAI/litellm/issues/21125 """ + import litellm from litellm.llms.openai_like.dynamic_config import create_config_class from litellm.llms.openai_like.json_loader import JSONProviderRegistry + monkeypatch.setitem( + litellm.model_cost, + "publicai/some-model-without-fc", + {"litellm_provider": "publicai", "mode": "chat", "supports_function_calling": False}, + ) provider = JSONProviderRegistry.get("publicai") config_class = create_config_class(provider) config = config_class() - # Mock supports_function_calling to return False - with patch("litellm.utils.supports_function_calling", return_value=False): - supported = config.get_supported_openai_params("some-model-without-fc") + supported = config.get_supported_openai_params("some-model-without-fc") tool_params = [ "tools", @@ -126,18 +129,22 @@ class TestJSONProviderLoader: assert "max_tokens" in supported assert "stop" in supported - def test_tool_params_included_when_function_calling_supported(self): + def test_tool_params_included_when_function_calling_supported(self, monkeypatch): """Test that tool-related params are included for models that support function calling.""" + import litellm from litellm.llms.openai_like.dynamic_config import create_config_class from litellm.llms.openai_like.json_loader import JSONProviderRegistry + monkeypatch.setitem( + litellm.model_cost, + "publicai/some-model-with-fc", + {"litellm_provider": "publicai", "mode": "chat", "supports_function_calling": True}, + ) provider = JSONProviderRegistry.get("publicai") config_class = create_config_class(provider) config = config_class() - # Mock supports_function_calling to return True - with patch("litellm.utils.supports_function_calling", return_value=True): - supported = config.get_supported_openai_params("some-model-with-fc") + supported = config.get_supported_openai_params("some-model-with-fc") assert "tools" in supported assert "tool_choice" in supported From e367ae5ed39e0c9d12a1628e3663ef655d24998c Mon Sep 17 00:00:00 2001 From: Priyansh Nandwana Date: Wed, 28 Oct 2026 07:37:35 +0530 Subject: [PATCH 2/3] fix(openai_like): honor a provider-wide supports_function_calling for unmapped models A JSON provider can declare supports_function_calling in provider_info. When the model has no model map entry, fall back to that value before defaulting to keeping the tool params. A model map entry still wins over the provider. --- litellm/llms/openai_like/dynamic_config.py | 21 ++++++--- .../llms/openai_like/test_dynamic_config.py | 43 ++++++++++++++----- 2 files changed, 47 insertions(+), 17 deletions(-) diff --git a/litellm/llms/openai_like/dynamic_config.py b/litellm/llms/openai_like/dynamic_config.py index 27d8679e951..cef594dca4c 100644 --- a/litellm/llms/openai_like/dynamic_config.py +++ b/litellm/llms/openai_like/dynamic_config.py @@ -21,16 +21,23 @@ if TYPE_CHECKING: from litellm.llms.openai_like.responses.transformation import OpenAILikeResponsesConfig -def _function_calling_disabled_in_model_map(model: str, custom_llm_provider: str) -> bool: +def _model_map_function_calling(model: str, custom_llm_provider: str) -> bool | None: from litellm.utils import get_model_info_helper try: - return ( - get_model_info_helper(model=model, custom_llm_provider=custom_llm_provider).get("supports_function_calling") - is False + value: Final = get_model_info_helper(model=model, custom_llm_provider=custom_llm_provider).get( + "supports_function_calling" ) except Exception: # noqa: BLE001 # get_model_info_helper raises bare Exception for unmapped models - return False + return None + return value if isinstance(value, bool) else None + + +def _function_calling_disabled(model: str, provider: SimpleProviderConfig) -> bool: + from_model_map: Final = _model_map_function_calling(model=model, custom_llm_provider=provider.slug) + if from_model_map is not None: + return not from_model_map + return provider.provider_info.get("supports_function_calling") is False def create_config_class(provider: SimpleProviderConfig): @@ -110,12 +117,12 @@ def create_config_class(provider: SimpleProviderConfig): def get_supported_openai_params(self, model: str) -> list: """Get supported OpenAI params, excluding tool-related params for models - the model map marks as not supporting function calling.""" + the model map, or failing that the provider, marks as not supporting function calling.""" from litellm.utils import supports_reasoning supported_params: Final = super().get_supported_openai_params(model=model) - if _function_calling_disabled_in_model_map(model=model, custom_llm_provider=provider.slug): + if _function_calling_disabled(model=model, provider=provider): tool_params: Final = [ "tools", "tool_choice", diff --git a/tests/unit/llms/openai_like/test_dynamic_config.py b/tests/unit/llms/openai_like/test_dynamic_config.py index 3d5f380a7ef..bf1d381cd07 100644 --- a/tests/unit/llms/openai_like/test_dynamic_config.py +++ b/tests/unit/llms/openai_like/test_dynamic_config.py @@ -148,20 +148,43 @@ class TestToolParamsFollowTheModelMap: _TOOLS: Final = [{"type": "function", "function": {"name": "get_weather", "parameters": {"type": "object"}}}] @pytest.mark.parametrize( - ("model_map_entry", "tools_forwarded"), + ("model_supports_fc", "provider_supports_fc", "tools_forwarded"), [ - (None, True), - ({"litellm_provider": "tools_map", "mode": "chat", "supports_function_calling": True}, True), - ({"litellm_provider": "tools_map", "mode": "chat", "supports_function_calling": False}, False), + (None, None, True), + (None, False, False), + (None, True, True), + (True, None, True), + (False, None, False), + (True, False, True), + (False, True, False), + ], + ids=[ + "unmapped", + "unmapped_provider_unsupported", + "unmapped_provider_supported", + "mapped_as_supported", + "mapped_as_unsupported", + "model_supported_overrides_provider", + "model_unsupported_overrides_provider", ], - ids=["unmapped", "mapped_as_supported", "mapped_as_unsupported"], ) - def test_tools_are_dropped_only_when_the_model_map_says_unsupported( - self, model_map_entry: dict | None, tools_forwarded: bool, monkeypatch: pytest.MonkeyPatch + def test_tools_are_dropped_only_when_the_model_or_provider_says_unsupported( + self, + model_supports_fc: bool | None, + provider_supports_fc: bool | None, + tools_forwarded: bool, + monkeypatch: pytest.MonkeyPatch, ) -> None: - if model_map_entry is not None: - monkeypatch.setitem(litellm.model_cost, "tools_map/some-model", model_map_entry) - config: Final = create_config_class(_provider("tools_map"))() + if model_supports_fc is not None: + monkeypatch.setitem( + litellm.model_cost, + "tools_map/some-model", + {"litellm_provider": "tools_map", "mode": "chat", "supports_function_calling": model_supports_fc}, + ) + provider_overrides: Final = ( + {} if provider_supports_fc is None else {"provider_info": {"supports_function_calling": provider_supports_fc}} + ) + config: Final = create_config_class(_provider("tools_map", **provider_overrides))() optional_params: Final = config.map_openai_params( non_default_params={"tools": self._TOOLS, "tool_choice": "auto"}, From 61d34079235e67ad7228ac02e3b4cf5a34d10c42 Mon Sep 17 00:00:00 2001 From: Priyansh Nandwana Date: Wed, 28 Oct 2026 07:38:01 +0530 Subject: [PATCH 3/3] style: wrap the provider_info override in the tool params test --- tests/unit/llms/openai_like/test_dynamic_config.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/unit/llms/openai_like/test_dynamic_config.py b/tests/unit/llms/openai_like/test_dynamic_config.py index bf1d381cd07..c722eb40a2f 100644 --- a/tests/unit/llms/openai_like/test_dynamic_config.py +++ b/tests/unit/llms/openai_like/test_dynamic_config.py @@ -182,7 +182,9 @@ class TestToolParamsFollowTheModelMap: {"litellm_provider": "tools_map", "mode": "chat", "supports_function_calling": model_supports_fc}, ) provider_overrides: Final = ( - {} if provider_supports_fc is None else {"provider_info": {"supports_function_calling": provider_supports_fc}} + {} + if provider_supports_fc is None + else {"provider_info": {"supports_function_calling": provider_supports_fc}} ) config: Final = create_config_class(_provider("tools_map", **provider_overrides))()