diff --git a/litellm/llms/openai_like/dynamic_config.py b/litellm/llms/openai_like/dynamic_config.py index d7aafe9f06e..4e20203c14a 100644 --- a/litellm/llms/openai_like/dynamic_config.py +++ b/litellm/llms/openai_like/dynamic_config.py @@ -105,16 +105,28 @@ def create_config_class(provider: SimpleProviderConfig): return ensure_api_route(api_base, "chat/completions") raise ValueError(f"api_base is required for provider {provider.slug}") + @staticmethod + def _supports_tool_params(model: str) -> bool: + from litellm.utils import is_explicitly_disabled_factory, supports_function_calling + + if supports_function_calling(model=model, custom_llm_provider=provider.slug): + return True + + if provider.provider_info.get("supports_function_calling") is False: + return False + + return not is_explicitly_disabled_factory( + model=model, custom_llm_provider=provider.slug, key="supports_function_calling" + ) + 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 + 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 not self._supports_tool_params(model): 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..ea61a1094a6 100644 --- a/tests/unit/llms/openai_like/test_dynamic_config.py +++ b/tests/unit/llms/openai_like/test_dynamic_config.py @@ -1,8 +1,13 @@ -import pytest +import json +from typing import Final +import pytest +import respx + +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.json_loader import SimpleProviderConfig +from litellm.llms.openai_like.json_loader import JSONProviderRegistry, SimpleProviderConfig from litellm.types.router import GenericLiteLLMParams _BASE = {"base_url": "https://api.example.com/v1", "api_key_env": "EXAMPLE_API_KEY"} @@ -20,7 +25,6 @@ def _isolate_generated_class_cache(): class TestClassCaching: - def test_cache_is_keyed_on_slug_not_on_the_provider_instance(self): first = create_responses_config_class(_provider("cache_by_slug")) second = create_responses_config_class(_provider("cache_by_slug")) @@ -139,3 +143,66 @@ class TestForceStoreFalse: headers={}, ) assert params["store"] is True + + +@pytest.mark.parametrize( + "model_info,provider_capability,drop_params,expected_tools", + [ + pytest.param(None, None, False, True, id="unknown-strict"), + pytest.param(None, None, True, True, id="unknown-drop"), + pytest.param({}, None, True, True, id="missing-capability"), + pytest.param({"supports_function_calling": None}, None, True, True, id="null-capability"), + pytest.param({"supports_function_calling": False}, None, True, False, id="model-disabled"), + pytest.param({"supports_function_calling": False}, True, True, False, id="model-overrides-provider"), + pytest.param(None, False, True, False, id="provider-disabled"), + pytest.param({"supports_function_calling": True}, False, True, True, id="model-enabled"), + ], +) +async def test_tools_reach_upstream_unless_explicitly_disabled( + monkeypatch: pytest.MonkeyPatch, + model_info: dict[str, bool | None] | None, + provider_capability: bool | None, + drop_params: bool, + expected_tools: bool, +) -> None: + monkeypatch.setattr(litellm, "disable_aiohttp_transport", True) + model: Final = "z-ai/issue-48145-model" + provider: Final = JSONProviderRegistry.get("nano-gpt") + assert provider is not None + monkeypatch.setattr( + provider, + "provider_info", + {} if provider_capability is None else {"supports_function_calling": provider_capability}, + ) + if model_info is not None: + litellm.register_model( + {f"nano-gpt/{model}": {"litellm_provider": "nano-gpt", "mode": "chat", **model_info}}, + persist_across_reloads=False, + add_to_provider_model_sets=False, + ) + tool: Final = {"type": "function", "function": {"name": "get_weather", "parameters": {"type": "object"}}} + with respx.mock() as upstream: + route: Final = upstream.post("https://upstream.example/v1/chat/completions").respond( + 200, + json={ + "id": "chatcmpl-tools", + "object": "chat.completion", + "created": 1, + "model": model, + "choices": [{"index": 0, "message": {"role": "assistant", "content": "ok"}, "finish_reason": "stop"}], + }, + ) + await litellm.acompletion( + model=f"nano-gpt/{model}", + messages=[{"role": "user", "content": "Weather in Paris?"}], + api_base="https://upstream.example/v1", + api_key="local-placeholder", + tools=[tool], + tool_choice="auto", + drop_params=drop_params, + ) + body: Final = json.loads(route.calls.last.request.content) + assert ("tools" in body) is expected_tools + assert body.get("tool_choice") == ("auto" if expected_tools else None) + if expected_tools: + assert body["tools"] == [tool] diff --git a/tests/unit/llms/openai_like/test_json_providers.py b/tests/unit/llms/openai_like/test_json_providers.py index 9dbdaa28a76..a445be80904 100644 --- a/tests/unit/llms/openai_like/test_json_providers.py +++ b/tests/unit/llms/openai_like/test_json_providers.py @@ -4,7 +4,8 @@ Tests for JSON-based provider configuration system. import os import sys -from unittest.mock import patch + +from litellm import register_model try: import pytest @@ -105,9 +106,12 @@ class TestJSONProviderLoader: 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") + register_model( + {"publicai/some-model-without-fc": {"supports_function_calling": False}}, + persist_across_reloads=False, + add_to_provider_model_sets=False, + ) + supported = config.get_supported_openai_params("some-model-without-fc") tool_params = [ "tools", @@ -135,9 +139,12 @@ class TestJSONProviderLoader: 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") + register_model( + {"publicai/some-model-with-fc": {"supports_function_calling": True}}, + persist_across_reloads=False, + add_to_provider_model_sets=False, + ) + supported = config.get_supported_openai_params("some-model-with-fc") assert "tools" in supported assert "tool_choice" in supported