diff --git a/litellm/proxy/common_utils/advertised_models.py b/litellm/proxy/common_utils/advertised_models.py index d7c14bda446..815fdf76a8a 100644 --- a/litellm/proxy/common_utils/advertised_models.py +++ b/litellm/proxy/common_utils/advertised_models.py @@ -81,13 +81,22 @@ def _listing_row(entry: AdvertisedModel) -> ModelInfoResponse: def _reserved_ids(listed_ids: Collection[str], llm_router: Router | None) -> frozenset[str]: """Ids a catalog entry may not claim. - Every name the router knows counts, not just the ids this caller can see, so - an entry cannot re-expose a deployment that team scoping, a pause or a health - filter had already removed from their listing. + Every routable name counts, not just the ids this caller can see, so an entry + cannot re-expose a deployment that team scoping, a pause or a health filter + had already removed from their listing. + + `get_model_names()` alone is not that set: with no team id it drops + team-scoped deployments, and despite its docstring it never returns + `model_group_alias` keys, so both are unioned in explicitly. """ if llm_router is None: return frozenset(listed_ids) - return frozenset(listed_ids).union(llm_router.get_model_names(), llm_router.get_model_access_groups()) + return frozenset(listed_ids).union( + llm_router.get_model_names(), + llm_router.get_model_access_groups(), + llm_router.team_public_model_names, + llm_router.model_group_alias or (), + ) def _first_per_id(entries: tuple[AdvertisedModel, ...]) -> tuple[AdvertisedModel, ...]: diff --git a/tests/test_litellm/proxy/common_utils/test_advertised_models.py b/tests/test_litellm/proxy/common_utils/test_advertised_models.py index 88fe501d9b8..eda8e98e20d 100644 --- a/tests/test_litellm/proxy/common_utils/test_advertised_models.py +++ b/tests/test_litellm/proxy/common_utils/test_advertised_models.py @@ -98,11 +98,20 @@ def test_configured_entries_are_typed_and_keep_their_order(): class _RouterStub: """Minimal stand-in for the bits of Router this module reads.""" - def __init__(self, names: tuple[str, ...], groups: tuple[str, ...] = ()) -> None: + def __init__( + self, + names: tuple[str, ...], + groups: tuple[str, ...] = (), + team_public: frozenset[str] = frozenset(), + aliases: tuple[str, ...] = (), + ) -> None: self._names: Final = names self._groups: Final = groups + self.team_public_model_names: Final = team_public + self.model_group_alias: Final = {alias: "some-target" for alias in aliases} def get_model_names(self) -> list[str]: + """Mirrors the real method: team-scoped names and aliases are absent here.""" return list(self._names) def get_model_access_groups(self) -> dict[str, list[str]]: @@ -154,3 +163,25 @@ def test_every_row_is_marked_catalog_only(): True, True, ], f"every catalog row must be marked so clients can tell it from a routable model, got {rows}" + + +def test_entry_naming_a_team_scoped_public_name_is_dropped(): + """`get_model_names()` omits team-scoped deployments when given no team id.""" + rows: Final = advertised_model_rows( + _settings({"id": "team-public-gpt", "owned_by": "impostor"}), + [], + _RouterStub((), team_public=frozenset({"team-public-gpt"})), + ) + + assert rows == (), f"a team's public model name must stay reserved, got {rows}" + + +def test_entry_naming_a_model_group_alias_is_dropped(): + """`get_model_names()` does not return alias keys despite its docstring.""" + rows: Final = advertised_model_rows( + _settings({"id": "gpt-alias", "owned_by": "impostor"}), + [], + _RouterStub((), aliases=("gpt-alias",)), + ) + + assert rows == (), f"a routable alias must stay reserved, got {rows}" diff --git a/tests/test_litellm/proxy/test_model_list_advertised_models.py b/tests/test_litellm/proxy/test_model_list_advertised_models.py index f106c587146..5e58b7f591b 100644 --- a/tests/test_litellm/proxy/test_model_list_advertised_models.py +++ b/tests/test_litellm/proxy/test_model_list_advertised_models.py @@ -174,10 +174,10 @@ class _HidingGate(CustomLogger): def __init__(self, hidden: frozenset[str]) -> None: super().__init__() self.hidden = hidden - self.seen: list[tuple[str, ...]] = [] + self.seen: tuple[tuple[str, ...], ...] = () async def async_filter_listed_models(self, user_api_key_dict, model_names): - self.seen.append(tuple(model_names)) + self.seen = (*self.seen, tuple(model_names)) return [name for name in model_names if name not in self.hidden]