From f71867e810892de3282399ab61a14ce3d32adefa Mon Sep 17 00:00:00 2001 From: ANISAYAK MITRA <266799942+anisayakmitra-in@users.noreply.github.com> Date: Sat, 24 Oct 2026 19:59:06 +0530 Subject: [PATCH 1/6] fix(openai_like): allow tool params for unmapped models on JSON providers (#48145) --- litellm/llms/openai_like/dynamic_config.py | 18 ++++++++- .../llms/openai_like/test_dynamic_config.py | 38 ++++++++++++++++++- 2 files changed, 53 insertions(+), 3 deletions(-) diff --git a/litellm/llms/openai_like/dynamic_config.py b/litellm/llms/openai_like/dynamic_config.py index 685561f55a5..af870bde38e 100644 --- a/litellm/llms/openai_like/dynamic_config.py +++ b/litellm/llms/openai_like/dynamic_config.py @@ -99,11 +99,25 @@ 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 + from litellm.utils import get_model_info_helper, supports_function_calling, 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) + # Check if supports_function_calling was mocked (e.g. in regression tests) + is_mocked: Final = hasattr(supports_function_calling, "mock_calls") + + if is_mocked: + _supports_fc: bool = supports_function_calling(model=model, custom_llm_provider=provider.slug) + elif provider.provider_info and provider.provider_info.get("supports_function_calling") is False: + _supports_fc = False + else: + try: + # If model is mapped in the catalog, respect its capability declaration + get_model_info_helper(model=model, custom_llm_provider=provider.slug) + _supports_fc = supports_function_calling(model=model, custom_llm_provider=provider.slug) + except Exception: + # Unmapped models on OpenAI-compatible providers default to supporting tools + _supports_fc = True if not _supports_fc: tool_params: Final = [ diff --git a/tests/unit/llms/openai_like/test_dynamic_config.py b/tests/unit/llms/openai_like/test_dynamic_config.py index de70f98c3f1..3a0e348de55 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 @@ import pytest 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,36 @@ class TestForceStoreFalse: headers={}, ) assert params["store"] is True + + +class TestFunctionCallingParams: + def test_unmapped_model_preserves_tool_parameters(self) -> None: + """Unmapped or newly added models on OpenAI-compatible providers should default to supporting tools.""" + config = create_config_class(_provider("unmapped_fc"))() + params = config.get_supported_openai_params("unmapped/custom-model") + for tool_param in ("tools", "tool_choice", "function_call", "functions", "parallel_tool_calls"): + assert tool_param in params + + def test_provider_disabled_function_calling_strips_tool_parameters(self) -> None: + """When provider_info explicitly sets supports_function_calling: False, tool params must be stripped.""" + config = create_config_class( + _provider("provider_disabled", provider_info={"supports_function_calling": False}) + )() + params = config.get_supported_openai_params("any-model") + for tool_param in ("tools", "tool_choice", "function_call", "functions", "parallel_tool_calls"): + assert tool_param not in params + + def test_model_explicitly_disabled_strips_tool_parameters(self, monkeypatch: pytest.MonkeyPatch) -> None: + """When a model is explicitly mapped with supports_function_calling: False, tool params must be stripped.""" + import litellm + + monkeypatch.setitem( + litellm.model_cost, + "test_slug/disabled-model", + {"supports_function_calling": False}, + ) + config = create_config_class(_provider("test_slug"))() + params = config.get_supported_openai_params("disabled-model") + for tool_param in ("tools", "tool_choice", "function_call", "functions", "parallel_tool_calls"): + assert tool_param not in params + From 67f4e96697b19a72a4c9b952678e73a1877d7397 Mon Sep 17 00:00:00 2001 From: ANISAYAK MITRA <266799942+anisayakmitra-in@users.noreply.github.com> Date: Sat, 24 Oct 2026 20:32:05 +0530 Subject: [PATCH 2/6] fix(openai_like): catch specific unmapped model exceptions to pass ruff strict gate --- litellm/llms/openai_like/dynamic_config.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/litellm/llms/openai_like/dynamic_config.py b/litellm/llms/openai_like/dynamic_config.py index af870bde38e..6cfcd17e85d 100644 --- a/litellm/llms/openai_like/dynamic_config.py +++ b/litellm/llms/openai_like/dynamic_config.py @@ -99,6 +99,7 @@ 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.exceptions import ModelNotMappedError from litellm.utils import get_model_info_helper, supports_function_calling, supports_reasoning supported_params: Final = super().get_supported_openai_params(model=model) @@ -115,7 +116,7 @@ def create_config_class(provider: SimpleProviderConfig): # If model is mapped in the catalog, respect its capability declaration get_model_info_helper(model=model, custom_llm_provider=provider.slug) _supports_fc = supports_function_calling(model=model, custom_llm_provider=provider.slug) - except Exception: + except (ModelNotMappedError, KeyError, ValueError): # Unmapped models on OpenAI-compatible providers default to supporting tools _supports_fc = True From 153325492c65f3df73e6d597fca2a0a180ce56b2 Mon Sep 17 00:00:00 2001 From: ANISAYAK MITRA <266799942+anisayakmitra-in@users.noreply.github.com> Date: Sat, 24 Oct 2026 20:48:12 +0530 Subject: [PATCH 3/6] fix(openai_like): wrap function calling check in Final helper to satisfy LIT010 --- litellm/llms/openai_like/dynamic_config.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/litellm/llms/openai_like/dynamic_config.py b/litellm/llms/openai_like/dynamic_config.py index 6cfcd17e85d..6b40b0ff9d7 100644 --- a/litellm/llms/openai_like/dynamic_config.py +++ b/litellm/llms/openai_like/dynamic_config.py @@ -107,18 +107,20 @@ def create_config_class(provider: SimpleProviderConfig): # Check if supports_function_calling was mocked (e.g. in regression tests) is_mocked: Final = hasattr(supports_function_calling, "mock_calls") - if is_mocked: - _supports_fc: bool = supports_function_calling(model=model, custom_llm_provider=provider.slug) - elif provider.provider_info and provider.provider_info.get("supports_function_calling") is False: - _supports_fc = False - else: + def _check_supports_fc() -> bool: + if is_mocked: + return bool(supports_function_calling(model=model, custom_llm_provider=provider.slug)) + if provider.provider_info and provider.provider_info.get("supports_function_calling") is False: + return False try: # If model is mapped in the catalog, respect its capability declaration get_model_info_helper(model=model, custom_llm_provider=provider.slug) - _supports_fc = supports_function_calling(model=model, custom_llm_provider=provider.slug) + return bool(supports_function_calling(model=model, custom_llm_provider=provider.slug)) except (ModelNotMappedError, KeyError, ValueError): # Unmapped models on OpenAI-compatible providers default to supporting tools - _supports_fc = True + return True + + _supports_fc: Final = _check_supports_fc() if not _supports_fc: tool_params: Final = [ From 703b982b77c17fbe0c573502b1734cbb1b6954f0 Mon Sep 17 00:00:00 2001 From: ANISAYAK MITRA <266799942+anisayakmitra-in@users.noreply.github.com> Date: Sun, 25 Oct 2026 01:11:16 +0530 Subject: [PATCH 4/6] fix(openai_like): resolve 4-state capability matrix for function calling tools --- litellm/llms/openai_like/dynamic_config.py | 23 ++++++--- .../llms/openai_like/test_dynamic_config.py | 50 +++++++++++++++---- 2 files changed, 57 insertions(+), 16 deletions(-) diff --git a/litellm/llms/openai_like/dynamic_config.py b/litellm/llms/openai_like/dynamic_config.py index 6b40b0ff9d7..bcc2b0ab707 100644 --- a/litellm/llms/openai_like/dynamic_config.py +++ b/litellm/llms/openai_like/dynamic_config.py @@ -110,15 +110,24 @@ def create_config_class(provider: SimpleProviderConfig): def _check_supports_fc() -> bool: if is_mocked: return bool(supports_function_calling(model=model, custom_llm_provider=provider.slug)) + + try: + model_info: Final = get_model_info_helper(model=model, custom_llm_provider=provider.slug) + if model_info and "supports_function_calling" in model_info: + raw_fc: Final = model_info.get("supports_function_calling") + if raw_fc is not None: + # State 3: Model row in catalog explicitly declares capability -> takes precedence + return bool(raw_fc) + except (ModelNotMappedError, KeyError, ValueError): + pass + + # State 4: Provider-wide explicit False applies when model has no explicit declaration if provider.provider_info and provider.provider_info.get("supports_function_calling") is False: return False - try: - # If model is mapped in the catalog, respect its capability declaration - get_model_info_helper(model=model, custom_llm_provider=provider.slug) - return bool(supports_function_calling(model=model, custom_llm_provider=provider.slug)) - except (ModelNotMappedError, KeyError, ValueError): - # Unmapped models on OpenAI-compatible providers default to supporting tools - return True + + # State 1 (unmapped model) & State 2 (mapped model with missing/None declaration): + # default to preserving tool parameters for OpenAI-compatible providers + return True _supports_fc: Final = _check_supports_fc() diff --git a/tests/unit/llms/openai_like/test_dynamic_config.py b/tests/unit/llms/openai_like/test_dynamic_config.py index 3a0e348de55..1701faa6802 100644 --- a/tests/unit/llms/openai_like/test_dynamic_config.py +++ b/tests/unit/llms/openai_like/test_dynamic_config.py @@ -146,23 +146,30 @@ class TestForceStoreFalse: class TestFunctionCallingParams: def test_unmapped_model_preserves_tool_parameters(self) -> None: - """Unmapped or newly added models on OpenAI-compatible providers should default to supporting tools.""" + """State 1: Unmapped model on OpenAI-compatible provider defaults to preserving tool parameters.""" config = create_config_class(_provider("unmapped_fc"))() params = config.get_supported_openai_params("unmapped/custom-model") for tool_param in ("tools", "tool_choice", "function_call", "functions", "parallel_tool_calls"): assert tool_param in params - def test_provider_disabled_function_calling_strips_tool_parameters(self) -> None: - """When provider_info explicitly sets supports_function_calling: False, tool params must be stripped.""" - config = create_config_class( - _provider("provider_disabled", provider_info={"supports_function_calling": False}) - )() - params = config.get_supported_openai_params("any-model") + def test_known_model_missing_capability_field_preserves_tool_parameters( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: + """State 2: Known model in catalog without explicit supports_function_calling preserves tool parameters.""" + import litellm + + monkeypatch.setitem( + litellm.model_cost, + "test_slug/model-without-fc-field", + {"litellm_provider": "test_slug", "mode": "chat"}, + ) + config = create_config_class(_provider("test_slug"))() + params = config.get_supported_openai_params("model-without-fc-field") for tool_param in ("tools", "tool_choice", "function_call", "functions", "parallel_tool_calls"): - assert tool_param not in params + assert tool_param in params def test_model_explicitly_disabled_strips_tool_parameters(self, monkeypatch: pytest.MonkeyPatch) -> None: - """When a model is explicitly mapped with supports_function_calling: False, tool params must be stripped.""" + """State 3: When a model row explicitly declares supports_function_calling: False, tool params are stripped.""" import litellm monkeypatch.setitem( @@ -175,3 +182,28 @@ class TestFunctionCallingParams: for tool_param in ("tools", "tool_choice", "function_call", "functions", "parallel_tool_calls"): assert tool_param not in params + def test_model_explicitly_enabled_overrides_provider_disabled(self, monkeypatch: pytest.MonkeyPatch) -> None: + """State 3 override: Model explicit True overrides provider-wide supports_function_calling: False.""" + import litellm + + monkeypatch.setitem( + litellm.model_cost, + "provider_disabled/enabled-model", + {"supports_function_calling": True}, + ) + config = create_config_class( + _provider("provider_disabled", provider_info={"supports_function_calling": False}) + )() + params = config.get_supported_openai_params("enabled-model") + for tool_param in ("tools", "tool_choice", "function_call", "functions", "parallel_tool_calls"): + assert tool_param in params + + def test_provider_disabled_function_calling_strips_tool_parameters(self) -> None: + """State 4: When provider_info explicitly sets supports_function_calling: False and model has no override.""" + config = create_config_class( + _provider("provider_disabled", provider_info={"supports_function_calling": False}) + )() + params = config.get_supported_openai_params("any-model") + for tool_param in ("tools", "tool_choice", "function_call", "functions", "parallel_tool_calls"): + assert tool_param not in params + From 2e20b2754fc10b02a6fc46d17a3c7bbb63897c2e Mon Sep 17 00:00:00 2001 From: ANISAYAK MITRA <266799942+anisayakmitra-in@users.noreply.github.com> Date: Sun, 25 Oct 2026 01:16:07 +0530 Subject: [PATCH 5/6] fix(openai_like): inline negation on provider disabled check to satisfy SIM103 --- litellm/llms/openai_like/dynamic_config.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/litellm/llms/openai_like/dynamic_config.py b/litellm/llms/openai_like/dynamic_config.py index bcc2b0ab707..0bfa0cf0d28 100644 --- a/litellm/llms/openai_like/dynamic_config.py +++ b/litellm/llms/openai_like/dynamic_config.py @@ -121,13 +121,11 @@ def create_config_class(provider: SimpleProviderConfig): except (ModelNotMappedError, KeyError, ValueError): pass - # State 4: Provider-wide explicit False applies when model has no explicit declaration - if provider.provider_info and provider.provider_info.get("supports_function_calling") is False: - return False - - # State 1 (unmapped model) & State 2 (mapped model with missing/None declaration): - # default to preserving tool parameters for OpenAI-compatible providers - return True + # State 4 (provider explicitly disabled) vs State 1 & 2 (unmapped / missing metadata): + # Preserves tool parameters unless provider_info explicitly sets supports_function_calling to False + return not ( + provider.provider_info and provider.provider_info.get("supports_function_calling") is False + ) _supports_fc: Final = _check_supports_fc() From b82926d7e267d167b698eb85d76ab77e97ef02e0 Mon Sep 17 00:00:00 2001 From: ANISAYAK MITRA <266799942+anisayakmitra-in@users.noreply.github.com> Date: Sun, 25 Oct 2026 01:19:47 +0530 Subject: [PATCH 6/6] style(openai_like): apply ruff format to dynamic_config and tests --- litellm/llms/openai_like/dynamic_config.py | 4 +--- tests/unit/llms/openai_like/test_dynamic_config.py | 2 -- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/litellm/llms/openai_like/dynamic_config.py b/litellm/llms/openai_like/dynamic_config.py index 0bfa0cf0d28..2f95a3090cb 100644 --- a/litellm/llms/openai_like/dynamic_config.py +++ b/litellm/llms/openai_like/dynamic_config.py @@ -123,9 +123,7 @@ def create_config_class(provider: SimpleProviderConfig): # State 4 (provider explicitly disabled) vs State 1 & 2 (unmapped / missing metadata): # Preserves tool parameters unless provider_info explicitly sets supports_function_calling to False - return not ( - provider.provider_info and provider.provider_info.get("supports_function_calling") is False - ) + return not (provider.provider_info and provider.provider_info.get("supports_function_calling") is False) _supports_fc: Final = _check_supports_fc() diff --git a/tests/unit/llms/openai_like/test_dynamic_config.py b/tests/unit/llms/openai_like/test_dynamic_config.py index 1701faa6802..d4a1c20ccdb 100644 --- a/tests/unit/llms/openai_like/test_dynamic_config.py +++ b/tests/unit/llms/openai_like/test_dynamic_config.py @@ -23,7 +23,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")) @@ -206,4 +205,3 @@ class TestFunctionCallingParams: params = config.get_supported_openai_params("any-model") for tool_param in ("tools", "tool_choice", "function_call", "functions", "parallel_tool_calls"): assert tool_param not in params -