fix(proxy): reserve team-public model names and group aliases from the catalog

get_model_names() is not the set of routable names. Given no team id it drops
team-scoped deployments along with their public name, and despite its docstring
it never returns model_group_alias keys. An advertised entry could therefore
claim either kind of name and disclose it to a caller who cannot list the
model behind it.

Read team_public_model_names and model_group_alias off the router instead, and
record on the helper why get_model_names() alone will not do.

Also record the test callback's observations in a tuple rather than growing a
list.
This commit is contained in:
ajitsharmas2007 2026-09-26 22:22:54 +05:30
parent 3eaad4fb82
commit b9d86129b5
3 changed files with 47 additions and 7 deletions

View file

@ -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, ...]:

View file

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

View file

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