mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-06 02:48:13 +00:00
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:
parent
3eaad4fb82
commit
b9d86129b5
3 changed files with 47 additions and 7 deletions
|
|
@ -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, ...]:
|
||||
|
|
|
|||
|
|
@ -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}"
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue