diff --git a/litellm/proxy/health_endpoints/_health_endpoints.py b/litellm/proxy/health_endpoints/_health_endpoints.py index 6ef8bbc4006..dbc56c0e2e2 100644 --- a/litellm/proxy/health_endpoints/_health_endpoints.py +++ b/litellm/proxy/health_endpoints/_health_endpoints.py @@ -24,6 +24,7 @@ from litellm.proxy._types import ( LitellmUserRoles, ProxyErrorTypes, ProxyException, + SpecialModelNames, UserAPIKeyAuth, WebhookEvent, ) @@ -1052,7 +1053,14 @@ async def health_endpoint( # response but NOT in the background-cache /health response. This is # surfaced via the "warnings" field below so operators can fix the # missing model_info.id rather than guess at the discrepancy. - if len(user_api_key_dict.models) > 0: + # Keys granted SpecialModelNames.all_proxy_models carry the literal + # "all-proxy-models" entry, which matches no real model_name; treat + # them as unrestricted instead of filtering the list down to nothing. + restrict_to_allowed_models = ( + len(user_api_key_dict.models) > 0 + and SpecialModelNames.all_proxy_models.value not in user_api_key_dict.models + ) + if restrict_to_allowed_models: allowed_models = set(user_api_key_dict.models) _llm_model_list = [ m for m in _llm_model_list if m.get("model_name") in allowed_models @@ -1065,7 +1073,7 @@ async def health_endpoint( # other healthy model would still report healthy_count > 0 and # the targeted-503 path would never fire. targeted_ids = _resolve_targeted_model_ids(_llm_model_list, model, model_id) - if len(user_api_key_dict.models) > 0: + if restrict_to_allowed_models: allowed_model_ids = { (m.get("model_info") or {}).get("id") for m in _llm_model_list diff --git a/tests/test_litellm/proxy/health_endpoints/test_health_endpoints.py b/tests/test_litellm/proxy/health_endpoints/test_health_endpoints.py index 64c57ab90e3..3ce8f007a6a 100644 --- a/tests/test_litellm/proxy/health_endpoints/test_health_endpoints.py +++ b/tests/test_litellm/proxy/health_endpoints/test_health_endpoints.py @@ -1142,6 +1142,71 @@ async def test_health_endpoint_filters_model_list_by_user_access(): }, f"health_endpoint did not scope model_list to caller access: {returned_names}" +@pytest.mark.asyncio +async def test_health_endpoint_keeps_full_model_list_for_all_proxy_models(): + """ + A key granted all model permissions carries the literal + "all-proxy-models" entry in user_api_key_dict.models. It matches no real + model_name, so the access filter must be skipped entirely; otherwise the + model list filters down to nothing and /health reports 0/0 counts. + """ + from litellm.proxy._types import SpecialModelNames, UserAPIKeyAuth + from litellm.proxy.health_endpoints._health_endpoints import health_endpoint + + full_model_list = [ + { + "model_name": "model-a", + "litellm_params": {"model": "openai/gpt-4o"}, + "model_info": {"id": "id-a"}, + }, + { + "model_name": "model-b", + "litellm_params": {"model": "openai/gpt-4o"}, + "model_info": {"id": "id-b"}, + }, + ] + + user_api_key_dict = UserAPIKeyAuth( + api_key="hashed-test-key", + models=[SpecialModelNames.all_proxy_models.value], + ) + + captured: dict = {} + + async def fake_perform(**kwargs): + captured["model_list"] = kwargs["model_list"] + return { + "healthy_endpoints": [], + "unhealthy_endpoints": [], + "healthy_count": 0, + "unhealthy_count": 0, + } + + with ( + patch("litellm.proxy.proxy_server.llm_model_list", full_model_list), + patch("litellm.proxy.proxy_server.llm_router", None), + patch("litellm.proxy.proxy_server.prisma_client", None), + patch("litellm.proxy.proxy_server.use_background_health_checks", False), + patch("litellm.proxy.proxy_server.user_model", None), + patch("litellm.proxy.proxy_server.health_check_results", {}), + patch("litellm.proxy.proxy_server.health_check_details", True), + patch("litellm.proxy.proxy_server.health_check_concurrency", 1), + patch( + "litellm.proxy.health_endpoints._health_endpoints._perform_health_check_and_save", + side_effect=fake_perform, + ), + ): + from fastapi import Response + + await health_endpoint(response=Response(), user_api_key_dict=user_api_key_dict) + + returned_names = {m["model_name"] for m in captured["model_list"]} + assert returned_names == { + "model-a", + "model-b", + }, f"all-proxy-models key should health-check every model: {returned_names}" + + @pytest.mark.asyncio async def test_health_endpoint_filters_background_cache_by_user_access(): """