diff --git a/litellm/proxy/health_endpoints/_health_endpoints.py b/litellm/proxy/health_endpoints/_health_endpoints.py index b8c56037206..bf0200acfa9 100644 --- a/litellm/proxy/health_endpoints/_health_endpoints.py +++ b/litellm/proxy/health_endpoints/_health_endpoints.py @@ -878,11 +878,17 @@ def _health_accessible_model_names( def _caller_may_probe_deployment( - deployment: Mapping[str, object], allowed_models: frozenset[str], llm_router: Router | None, team_id: str | None + deployment: Mapping[str, object], + allowed_models: frozenset[str] | None, + llm_router: Router | None, + team_id: str | None, + caller_is_admin: bool, ) -> bool: """Same deployment visibility rule as routing: another team's deployment is never in scope, team-less callers included.""" - if not Router._deployment_usable_by_team(deployment, team_id): + if not caller_is_admin and not Router._deployment_usable_by_team(deployment, team_id): return False + if allowed_models is None: + return True if llm_router is None: return deployment.get("model_name") in allowed_models model: Final = dict(deployment) @@ -1120,12 +1126,12 @@ async def health_endpoint( detail={"error": "Model list not initialized"}, ) allowed_models: Final = _health_accessible_model_names(user_api_key_dict, llm_router) - restrict_to_allowed_models: Final = allowed_models is not None + restrict_to_allowed_models: Final = not is_admin or allowed_models is not None _llm_model_list: Final = [ m for m in copy.deepcopy(llm_model_list) - if allowed_models is None - or _caller_may_probe_deployment(m, allowed_models, llm_router, user_api_key_dict.team_id) + if not restrict_to_allowed_models + or _caller_may_probe_deployment(m, allowed_models, llm_router, user_api_key_dict.team_id, is_admin) ] targeted_ids: Final = _resolve_targeted_model_ids(_llm_model_list, model, model_id) if restrict_to_allowed_models and targeted_ids is not None and not targeted_ids: 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 c03284b72b6..bbcae2fb68f 100644 --- a/tests/test_litellm/proxy/health_endpoints/test_health_endpoints.py +++ b/tests/test_litellm/proxy/health_endpoints/test_health_endpoints.py @@ -1836,6 +1836,8 @@ async def test_health_endpoint_treats_no_team_all_team_models_as_unrestricted(): user_api_key_dict=UserAPIKeyAuth( api_key="hashed-test-key", models=[SpecialModelNames.all_team_models.value], team_id=None ), + model=None, + model_id=None, ) assert {m["model_name"] for m in captured["model_list"]} == {"bedrock-nova", "gpt-5.4-mini"} @@ -2844,6 +2846,57 @@ async def test_health_endpoint_hides_team_deployments_from_a_key_with_no_team(): assert probed == {"id-bedrock"} +@pytest.mark.asyncio +@pytest.mark.parametrize( + ("team_id", "expected_ids"), + [(None, {"id-bedrock"}), ("team-a", {"id-bedrock"}), ("team-b", {"id-bedrock", "id-team-b"})], +) +async def test_health_endpoint_keeps_an_unrestricted_non_admin_key_to_its_own_team(team_id, expected_ids): + """ + A key with no model restriction is still bound by routing's team rule: + it may probe global deployments and its own team's, never another team's. + """ + probed = await _live_probed_model_ids( + _TEAM_MODEL_LIST, + UserAPIKeyAuth(api_key="hashed-test-key", models=[], team_id=team_id), + ) + + assert probed == expected_ids + + +@pytest.mark.asyncio +async def test_health_endpoint_lets_a_proxy_admin_probe_every_teams_deployment(): + probed = await _live_probed_model_ids( + _TEAM_MODEL_LIST, + UserAPIKeyAuth(api_key="hashed-test-key", models=[], user_role=LitellmUserRoles.PROXY_ADMIN), + ) + + assert probed == {"id-bedrock", "id-team-b"} + + +@pytest.mark.asyncio +async def test_health_endpoint_keeps_an_unrestricted_non_admin_key_to_its_own_team_on_background_cache_path(): + from fastapi import Response + + from litellm.proxy.health_endpoints._health_endpoints import health_endpoint + + with _proxy_health_globals( + _TEAM_MODEL_LIST, + _router_for(_TEAM_MODEL_LIST), + use_background_health_checks=True, + health_check_results=_TEAM_CACHED_RESULTS, + ): + result = await health_endpoint( + response=Response(), + user_api_key_dict=UserAPIKeyAuth(api_key="hashed-test-key", models=[], team_id="team-a"), + model=None, + model_id=None, + ) + + assert [ep["model_id"] for ep in result["healthy_endpoints"]] == ["id-bedrock"] + assert result["healthy_count"] == 1 + + @pytest.mark.asyncio async def test_health_endpoint_shows_a_teams_own_deployment_by_its_public_name(): """