diff --git a/litellm/proxy/auth/model_checks.py b/litellm/proxy/auth/model_checks.py index bf76f99db69..dad1a6a7109 100644 --- a/litellm/proxy/auth/model_checks.py +++ b/litellm/proxy/auth/model_checks.py @@ -4,10 +4,12 @@ from typing import Dict, List, Optional, Set import litellm from litellm._logging import verbose_proxy_logger +from litellm.llms.openai_like.json_loader import JSONProviderRegistry from litellm.proxy._types import SpecialModelNames, UserAPIKeyAuth from litellm.router import Router from litellm.router_utils.fallback_event_handlers import get_fallback_model_group from litellm.types.router import LiteLLM_Params +from litellm.types.utils import LlmProvidersSet from litellm.utils import get_valid_models @@ -64,6 +66,82 @@ def _get_models_from_access_groups( return all_models +def _is_known_provider_qualified_model(model: str) -> bool: + """ + Return True for provider-qualified model identifiers such as: + - openai/gpt-4o-mini + - bedrock/us.amazon.nova-micro-v1:0 + - openrouter/auto + + This intentionally allows provider-prefixed models even when the exact + model name is not yet in LiteLLM's static model list, since those routes + may still be valid for pass-through / BYOK flows. + """ + if "/" not in model: + return False + + provider_prefix = model.split("/", 1)[0] + return provider_prefix in LlmProvidersSet or JSONProviderRegistry.exists( + provider_prefix + ) + + +def _should_include_model_in_complete_list( + model: str, + proxy_model_list: List[str], + model_access_groups: Dict[str, List[str]], +) -> bool: + """ + Filter out unresolved strings from the final model list returned by + /v1/models and related endpoints. + + Keep: + - configured proxy model groups + - configured access group names + - known base model IDs (e.g. gpt-4o-mini) + - known provider-qualified routes (e.g. openai/gpt-4o-mini, bedrock/*) + + Drop: + - arbitrary strings that don't resolve to a proxy model, access group, or + a recognized LiteLLM/provider model route. + """ + if model in ( + SpecialModelNames.all_proxy_models.value, + SpecialModelNames.all_team_models.value, + ): + return False + + if model in proxy_model_list or model in model_access_groups: + return True + + if model in litellm.model_list_set: + return True + + if model == "*": + return True + + if _is_known_provider_qualified_model(model): + return True + + return False + + +def _filter_complete_model_list( + models: List[str], + proxy_model_list: List[str], + model_access_groups: Dict[str, List[str]], +) -> List[str]: + return [ + model + for model in models + if _should_include_model_in_complete_list( + model=model, + proxy_model_list=proxy_model_list, + model_access_groups=model_access_groups, + ) + ] + + async def get_mcp_server_ids( user_api_key_dict: UserAPIKeyAuth, ) -> List[str]: @@ -211,6 +289,12 @@ def get_complete_model_list( valid_models = get_valid_models() append_unique(valid_models) + unique_models = _filter_complete_model_list( + models=unique_models, + proxy_model_list=proxy_model_list, + model_access_groups=model_access_groups, + ) + if only_model_access_groups: model_access_groups_to_return: List[str] = [] for model in unique_models: diff --git a/tests/test_litellm/proxy/auth/test_model_checks.py b/tests/test_litellm/proxy/auth/test_model_checks.py index c43621d7f71..222631b6aae 100644 --- a/tests/test_litellm/proxy/auth/test_model_checks.py +++ b/tests/test_litellm/proxy/auth/test_model_checks.py @@ -37,7 +37,10 @@ def test_get_team_models_all_proxy_models_includes_access_groups(): } result = get_team_models( - team_models, proxy_model_list, model_access_groups, include_model_access_groups=True + team_models, + proxy_model_list, + model_access_groups, + include_model_access_groups=True, ) assert "group-a" in result assert "group-b" in result @@ -61,7 +64,10 @@ def test_get_team_models_all_proxy_models_without_include_flag(): } result = get_team_models( - team_models, proxy_model_list, model_access_groups, include_model_access_groups=False + team_models, + proxy_model_list, + model_access_groups, + include_model_access_groups=False, ) assert "group-a" not in result assert "group-b" not in result @@ -159,43 +165,66 @@ def test_get_key_models_does_not_mutate_input(): "key_models,team_models,proxy_model_list,model_list,expected", [ ( - ["anthropic/claude-3-haiku-20240307", "anthropic/claude-3-5-haiku-20241022"], + [ + "anthropic/claude-3-haiku-20240307", + "anthropic/claude-3-5-haiku-20241022", + ], [], [], [{"model_name": "anthropic/*", "litellm_params": {"model": "anthropic/*"}}], - ["anthropic/claude-3-haiku-20240307", "anthropic/claude-3-5-haiku-20241022"] + [ + "anthropic/claude-3-haiku-20240307", + "anthropic/claude-3-5-haiku-20241022", + ], ), ( [], - ["anthropic/claude-3-haiku-20240307", "anthropic/claude-3-5-haiku-20241022"], + [ + "anthropic/claude-3-haiku-20240307", + "anthropic/claude-3-5-haiku-20241022", + ], [], [{"model_name": "anthropic/*", "litellm_params": {"model": "anthropic/*"}}], - ["anthropic/claude-3-haiku-20240307", "anthropic/claude-3-5-haiku-20241022"] + [ + "anthropic/claude-3-haiku-20240307", + "anthropic/claude-3-5-haiku-20241022", + ], ), ( [], [], - ["anthropic/claude-3-haiku-20240307", "anthropic/claude-3-5-haiku-20241022"], + [ + "anthropic/claude-3-haiku-20240307", + "anthropic/claude-3-5-haiku-20241022", + ], [{"model_name": "anthropic/*", "litellm_params": {"model": "anthropic/*"}}], - ["anthropic/claude-3-haiku-20240307", "anthropic/claude-3-5-haiku-20241022"] + [ + "anthropic/claude-3-haiku-20240307", + "anthropic/claude-3-5-haiku-20241022", + ], ), ], ) -def test_get_complete_model_list_order(key_models, team_models, proxy_model_list, model_list, expected): +def test_get_complete_model_list_order( + key_models, team_models, proxy_model_list, model_list, expected +): """ Test that get_complete_model_list preserves order """ from litellm.proxy.auth.model_checks import get_complete_model_list from litellm import Router - assert get_complete_model_list( - proxy_model_list=proxy_model_list, - key_models=key_models, - team_models=team_models, - user_model=None, - infer_model_from_keys=False, - llm_router=Router(model_list=model_list), - ) == expected + assert ( + get_complete_model_list( + proxy_model_list=proxy_model_list, + key_models=key_models, + team_models=team_models, + user_model=None, + infer_model_from_keys=False, + llm_router=Router(model_list=model_list), + ) + == expected + ) def test_get_complete_model_list_byok_wildcard_expansion(): @@ -220,3 +249,66 @@ def test_get_complete_model_list_byok_wildcard_expansion(): assert len(result) > 0 assert all(m.startswith("openai/") for m in result) assert "openai/*" not in result + + +def test_get_complete_model_list_filters_unknown_non_model_strings(): + """ + If a key contains an arbitrary string that is neither: + - a configured proxy model + - a configured access group + - a known LiteLLM model id + - nor a recognized provider-qualified route + + it should not leak into the final /v1/models response. + """ + from litellm.proxy.auth.model_checks import get_complete_model_list + + result = get_complete_model_list( + key_models=["team-sales-api"], + team_models=[], + proxy_model_list=["gpt-4o-mini"], + user_model=None, + infer_model_from_keys=False, + model_access_groups={}, + ) + + assert "team-sales-api" not in result + assert result == [] + + +def test_get_complete_model_list_keeps_known_base_model_ids(): + """ + Exact model IDs can be valid even when they are not configured as proxy + model groups, so known LiteLLM model ids should remain in the final list. + """ + from litellm.proxy.auth.model_checks import get_complete_model_list + + result = get_complete_model_list( + key_models=["gpt-4o-mini"], + team_models=[], + proxy_model_list=[], + user_model=None, + infer_model_from_keys=False, + model_access_groups={}, + ) + + assert result == ["gpt-4o-mini"] + + +def test_get_complete_model_list_keeps_provider_qualified_models(): + """ + Provider-qualified model identifiers should survive filtering even if the + exact model name is newer than LiteLLM's baked-in model list. + """ + from litellm.proxy.auth.model_checks import get_complete_model_list + + result = get_complete_model_list( + key_models=["bedrock/very_new_model"], + team_models=[], + proxy_model_list=[], + user_model=None, + infer_model_from_keys=False, + model_access_groups={}, + ) + + assert result == ["bedrock/very_new_model"]