fix(health): treat all-proxy-models keys as unrestricted in /health

A key granted all model permissions stores the literal
"all-proxy-models" marker in its models list. The /health access
filter compared that marker against real model_names, so the model
list filtered down to nothing and the WebUI health check returned
healthy_count=0, unhealthy_count=0 with HTTP 503. Skip the filter
(both the live path and the background-cache model_id scoping) when
the marker is present, matching how auth_checks treats
SpecialModelNames.all_proxy_models.

Fixes #29744.
This commit is contained in:
Filippo Mattia Menghi 2026-06-10 09:29:11 +02:00
parent e15b37a18e
commit 040417a8e9
2 changed files with 75 additions and 2 deletions

View file

@ -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

View file

@ -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():
"""