diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 3b36ea5afce..315b1d9fcc4 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -12258,6 +12258,25 @@ async def _deployment_hidden_by_listing_callbacks(deployment: Deployment, user_a return listed_name in await _names_hidden_by_listing_callbacks(user_api_key_dict, (listed_name,)) +def _model_list_provider(model_id: str, router: Router | None, team_id: str | None) -> str: + if router is None: + return "openai" + model_group: Final = router.routable_model_group(model_id) + deployment: Final = router.get_deployment_by_model_group_name(model_group) or next( + (Deployment.model_validate(row) for row in router.wildcard_route_deployments(model_group, team_id)), None + ) + if deployment is None: + return "openai" + try: + return litellm.get_llm_provider( + model=deployment.litellm_params.model, + custom_llm_provider=deployment.litellm_params.custom_llm_provider, + api_base=deployment.litellm_params.api_base, + )[1] + except litellm.BadRequestError: + return "openai" + + @router.get("/v1/models", dependencies=[Depends(user_api_key_auth)], tags=["model management"]) @router.get( "/models", dependencies=[Depends(user_api_key_auth)], tags=["model management"] @@ -12427,7 +12446,7 @@ async def model_list( for response_id, lookup_id in admin_entries: model_info = create_model_info_response( model_id=lookup_id, - provider="openai", + provider=_model_list_provider(lookup_id, llm_router, team_id or user_api_key_dict.team_id), include_metadata=include_metadata or False, fallback_type=fallback_type, llm_router=llm_router, @@ -12498,7 +12517,7 @@ async def model_list( for response_id, lookup_id in entries: model_info = create_model_info_response( model_id=lookup_id, - provider="openai", + provider=_model_list_provider(lookup_id, llm_router, team_id or user_api_key_dict.team_id), include_metadata=include_metadata or False, fallback_type=fallback_type, llm_router=llm_router, diff --git a/tests/unit/proxy/proxy_server/test_routes_models.py b/tests/unit/proxy/proxy_server/test_routes_models.py index 7be30c61f0c..d53ae077f69 100644 --- a/tests/unit/proxy/proxy_server/test_routes_models.py +++ b/tests/unit/proxy/proxy_server/test_routes_models.py @@ -74,7 +74,7 @@ def patched_models(monkeypatch): monkeypatch.setattr( litellm, "get_llm_provider", - lambda model: (model, "openai", None, None), + lambda model, **kwargs: (model, "openai", None, None), ) return router diff --git a/tests/unit/proxy/proxy_server/test_team_model_name_translation.py b/tests/unit/proxy/proxy_server/test_team_model_name_translation.py index 679959b5728..745dbceb682 100644 --- a/tests/unit/proxy/proxy_server/test_team_model_name_translation.py +++ b/tests/unit/proxy/proxy_server/test_team_model_name_translation.py @@ -28,7 +28,7 @@ from litellm.proxy.proxy_server import ( _get_proxy_model_info, _translate_model_name_for_response, ) -from litellm.types.router import DeploymentModelListingInfo +from litellm.types.router import Deployment, DeploymentModelListingInfo def _team_row() -> dict: @@ -880,6 +880,7 @@ async def test_v1_models_translates_team_model_for_access_group_key(monkeypatch) }, } router = MagicMock() + router.get_deployment_by_model_group_name.return_value = Deployment.model_validate(team_dep) router.get_model_names.return_value = ["model_name_teamX_uuid9"] router.get_model_access_groups.return_value = {"grp-a": ["model_name_teamX_uuid9"]} router.get_fully_blocked_model_names.return_value = set() @@ -922,6 +923,7 @@ async def test_v1_models_keeps_internal_names_when_public_name_flag_disabled( }, } router = MagicMock() + router.get_deployment_by_model_group_name.return_value = Deployment.model_validate(team_dep) router.get_model_names.return_value = ["model_name_teamX_uuid9"] router.get_model_access_groups.return_value = {"grp-a": ["model_name_teamX_uuid9"]} router.get_fully_blocked_model_names.return_value = set() @@ -957,6 +959,7 @@ async def test_v1_models_translates_team_model_with_metadata(monkeypatch): }, } router = MagicMock() + router.get_deployment_by_model_group_name.return_value = Deployment.model_validate(team_dep) router.get_model_names.return_value = ["model_name_teamX_uuid9"] router.get_model_access_groups.return_value = {"grp-a": ["model_name_teamX_uuid9"]} router.get_fully_blocked_model_names.return_value = set() @@ -979,7 +982,7 @@ async def test_v1_models_translates_team_model_with_metadata(monkeypatch): "id": "tushar-gpt-4.1", "object": "model", "created": 1677610602, - "owned_by": "openai", + "owned_by": team_dep["litellm_params"]["model"].split("/")[0], "metadata": {"fallbacks": []}, } ] @@ -1003,6 +1006,7 @@ async def test_v1_models_metadata_fallbacks_use_internal_routing_key(monkeypatch }, } router = MagicMock() + router.get_deployment_by_model_group_name.return_value = Deployment.model_validate(team_dep) router.get_model_names.return_value = ["model_name_teamX_uuid9"] router.get_model_access_groups.return_value = {"grp-a": ["model_name_teamX_uuid9"]} router.get_fully_blocked_model_names.return_value = set() @@ -1027,7 +1031,7 @@ async def test_v1_models_metadata_fallbacks_use_internal_routing_key(monkeypatch "id": "tushar-gpt-4.1", "object": "model", "created": 1677610602, - "owned_by": "openai", + "owned_by": team_dep["litellm_params"]["model"].split("/")[0], "metadata": {"fallbacks": ["gpt-4o-backup"]}, } ] @@ -1060,6 +1064,7 @@ async def test_v1_models_metadata_does_not_leak_other_team_fallbacks(monkeypatch }, } router = MagicMock() + router.get_deployment_by_model_group_name.return_value = Deployment.model_validate(team_x) router.get_model_names.return_value = ["model_name_teamX_uuid9"] router.get_model_access_groups.return_value = {"grp-a": ["model_name_teamX_uuid9"]} router.get_fully_blocked_model_names.return_value = set() @@ -1086,7 +1091,7 @@ async def test_v1_models_metadata_does_not_leak_other_team_fallbacks(monkeypatch "id": "tushar-gpt-4.1", "object": "model", "created": 1677610602, - "owned_by": "openai", + "owned_by": team_x["litellm_params"]["model"].split("/")[0], "metadata": {"fallbacks": ["teamX-backup"]}, } ] @@ -1108,6 +1113,7 @@ async def test_v1_models_team_alias_inherits_token_limits_and_chat_mode(monkeypa }, } router = MagicMock() + router.get_deployment_by_model_group_name.return_value = Deployment.model_validate(team_dep) router.get_model_names.return_value = ["model_name_teamX_terra_uuid"] router.get_model_access_groups.return_value = {"grp-a": ["model_name_teamX_terra_uuid"]} router.get_fully_blocked_model_names.return_value = set() @@ -1133,7 +1139,7 @@ async def test_v1_models_team_alias_inherits_token_limits_and_chat_mode(monkeypa "id": "GPT Terra", "object": "model", "created": 1677610602, - "owned_by": "openai", + "owned_by": team_dep["litellm_params"]["model"].split("/")[0], "mode": "chat", "max_input_tokens": 876000, "max_output_tokens": 128000, @@ -1156,6 +1162,7 @@ async def test_v1_models_team_image_alias_inherits_image_generation_mode(monkeyp }, } router = MagicMock() + router.get_deployment_by_model_group_name.return_value = Deployment.model_validate(team_dep) router.get_model_names.return_value = ["model_name_teamX_image_uuid"] router.get_model_access_groups.return_value = {"grp-a": ["model_name_teamX_image_uuid"]} router.get_fully_blocked_model_names.return_value = set() diff --git a/tests/unit/proxy/test_model_list_healthy_only.py b/tests/unit/proxy/test_model_list_healthy_only.py index 2652fbb30ef..808a58f1748 100644 --- a/tests/unit/proxy/test_model_list_healthy_only.py +++ b/tests/unit/proxy/test_model_list_healthy_only.py @@ -24,6 +24,7 @@ def patched_model_list(monkeypatch): from litellm.proxy import utils as proxy_utils router = MagicMock() + router.get_deployment_by_model_group_name.return_value = None router.get_fully_blocked_model_names = MagicMock(return_value=set()) router.async_get_fully_unhealthy_model_names = AsyncMock( return_value={"claude-sonnet"} diff --git a/tests/unit/proxy/test_proxy_server.py b/tests/unit/proxy/test_proxy_server.py index a26a64c6fb0..8bb5b7e5ebd 100644 --- a/tests/unit/proxy/test_proxy_server.py +++ b/tests/unit/proxy/test_proxy_server.py @@ -3857,3 +3857,73 @@ async def test_model_list_return_wildcard_routes_defaults_to_general_setting( assert "gpt-6.1-sol" in listed, listed assert ("openai/*" in listed) is wildcard_listed, listed + + +@pytest.mark.parametrize("scope", [None, "expand"]) +@pytest.mark.parametrize("provider", ["azure", "anthropic", "openai"]) +@pytest.mark.asyncio +async def test_model_list_provider_matches_retrieval( + monkeypatch: pytest.MonkeyPatch, scope: str | None, provider: str +) -> None: + router: Final = litellm.Router( + model_list=[ + { + "model_name": "hosted-model", + "litellm_params": { + "model": f"{provider}/deployment-model", + "api_key": "sk-fake", + "api_base": "https://provider.example.com", + }, + }, + { + "model_name": "prompt-model", + "litellm_params": {"model": "gitlab/test-prompt"}, + }, + ], + model_group_alias={"friendly-name": "hosted-model"}, + ) + monkeypatch.setattr(litellm.proxy.proxy_server, "llm_router", router) + monkeypatch.setattr(litellm.proxy.proxy_server, "llm_model_list", router.model_list) + monkeypatch.setattr(litellm.proxy.proxy_server, "prisma_client", None) + monkeypatch.setattr(litellm.proxy.proxy_server, "user_model", None) + monkeypatch.setattr(litellm.proxy.proxy_server, "general_settings", {}) + caller: Final = UserAPIKeyAuth(api_key="sk-test", user_role=LitellmUserRoles.PROXY_ADMIN) + + listing: Final = await litellm.proxy.proxy_server.model_list(user_api_key_dict=caller, scope=scope) + + owners: Final = {row["id"]: row["owned_by"] for row in listing["data"]} + assert owners == {"hosted-model": provider, "friendly-name": provider, "prompt-model": "openai"} + for model_id in ("hosted-model", "friendly-name"): + retrieved: Final = await litellm.proxy.proxy_server.model_info(model_id=model_id, user_api_key_dict=caller) + assert owners[model_id] == retrieved["owned_by"] + + +@pytest.mark.parametrize("scope", [None, "expand"]) +@pytest.mark.parametrize("prefix", ["anthropic", "public"]) +@pytest.mark.asyncio +async def test_model_list_expanded_names_use_wildcard_provider( + monkeypatch: pytest.MonkeyPatch, scope: str | None, prefix: str +) -> None: + exact_name: Final = f"{prefix}/configured-model" + router: Final = litellm.Router( + model_list=[ + {"model_name": f"{prefix}/*", "litellm_params": {"model": "anthropic/*", "api_key": "sk-fake"}}, + {"model_name": exact_name, "litellm_params": {"model": "openai/configured-model", "api_key": "sk-fake"}}, + ] + ) + monkeypatch.setattr(litellm.proxy.proxy_server, "llm_router", router) + monkeypatch.setattr(litellm.proxy.proxy_server, "llm_model_list", router.model_list) + monkeypatch.setattr(litellm.proxy.proxy_server, "prisma_client", None) + monkeypatch.setattr(litellm.proxy.proxy_server, "user_model", None) + monkeypatch.setattr(litellm.proxy.proxy_server, "general_settings", {}) + + listing: Final = await litellm.proxy.proxy_server.model_list( + user_api_key_dict=UserAPIKeyAuth(api_key="sk-test", user_role=LitellmUserRoles.PROXY_ADMIN), + scope=scope, + ) + + owners: Final = {row["id"]: row["owned_by"] for row in listing["data"]} + expanded_owners: Final = {owner for name, owner in owners.items() if name != exact_name} + assert expanded_owners == {"anthropic"} + assert all(name.startswith(f"{prefix}/") and "*" not in name for name in owners) + assert owners[exact_name] == "openai"