fix(health): resolve all-team-models sentinel to the team allowlist

Same failure shape as the all-proxy-models case: a key carrying the
literal "all-team-models" entry matches no real model_name, so the
/health access filter would zero out the model list. Resolve the
sentinel to the key's team models when team_id is set, matching
get_key_models in model_checks.py. Without a team_id the sentinel
stays unresolved and matches nothing, denying rather than widening
access, mirroring _resolve_key_models_for_auth_check.
This commit is contained in:
Filippo Mattia Menghi 2026-06-10 10:20:03 +02:00
parent 040417a8e9
commit b04b23357e
2 changed files with 81 additions and 3 deletions

View file

@ -1056,12 +1056,23 @@ async def health_endpoint(
# 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.
# Keys granted SpecialModelNames.all_team_models inherit the parent
# team's allowlist (same semantics as get_key_models in
# model_checks.py). Without a team_id the sentinel cannot resolve and
# stays in the list, matching nothing; denied rather than
# unrestricted, mirroring _resolve_key_models_for_auth_check.
accessible_models = list(user_api_key_dict.models)
if (
SpecialModelNames.all_team_models.value in accessible_models
and user_api_key_dict.team_id is not None
):
accessible_models = list(user_api_key_dict.team_models)
restrict_to_allowed_models = (
len(user_api_key_dict.models) > 0
and SpecialModelNames.all_proxy_models.value not in user_api_key_dict.models
len(accessible_models) > 0
and SpecialModelNames.all_proxy_models.value not in accessible_models
)
if restrict_to_allowed_models:
allowed_models = set(user_api_key_dict.models)
allowed_models = set(accessible_models)
_llm_model_list = [
m for m in _llm_model_list if m.get("model_name") in allowed_models
]

View file

@ -1207,6 +1207,73 @@ async def test_health_endpoint_keeps_full_model_list_for_all_proxy_models():
}, f"all-proxy-models key should health-check every model: {returned_names}"
@pytest.mark.asyncio
async def test_health_endpoint_resolves_all_team_models_to_team_allowlist():
"""
A key granted "all-team-models" carries the literal sentinel in
user_api_key_dict.models, which matches no real model_name. With a
team_id the sentinel must resolve to the team's allowlist (same
semantics as get_key_models); otherwise the filter would zero out the
model list just like the all-proxy-models case.
"""
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_team_models.value],
team_id="team-1",
team_models=["model-b"],
)
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-b"
}, f"all-team-models key should health-check the team's models: {returned_names}"
@pytest.mark.asyncio
async def test_health_endpoint_filters_background_cache_by_user_access():
"""