fix(proxy): let the exact model= filter match team BYOK public names

Team-scoped deployments keep the internal model_name_{team_id}_{uuid} routing key and expose the public name in model_info.team_public_model_name. The dashboard links team model chips with the public name, so the exact filter now matches either name via the existing helper.
This commit is contained in:
ryan-crabbe-berri 2026-08-29 10:52:15 -07:00
parent 9beb5ead4d
commit 08118e6246
2 changed files with 56 additions and 3 deletions

View file

@ -16,7 +16,7 @@ import threading
import time
import traceback
import warnings
from collections.abc import AsyncGenerator, AsyncIterator, Callable, Mapping, MutableMapping, Sequence
from collections.abc import AsyncGenerator, AsyncIterator, Callable, Collection, Mapping, MutableMapping, Sequence
from datetime import datetime, timedelta, timezone
from types import MappingProxyType, UnionType
from typing import (
@ -13484,7 +13484,7 @@ async def model_info_v2(
all_models += [user_model]
if model is not None:
all_models = [m for m in all_models if m["model_name"] == model]
all_models = [m for m in all_models if _deployment_matches_allowed_model_names(m, frozenset((model,)))]
# Apply search filter if provided
all_models, search_total_count = await _apply_search_filter_to_models(
@ -14011,7 +14011,7 @@ async def model_metrics_exceptions(
return {"data": response, "exception_types": list(exception_types)}
def _deployment_matches_allowed_model_names(model: dict[str, JsonValue], allowed_model_names: set[str]) -> bool:
def _deployment_matches_allowed_model_names(model: dict[str, JsonValue], allowed_model_names: Collection[str]) -> bool:
"""Match a router deployment against allowed public model names.
Team-scoped rows store an internal routing key in ``model_name``; callers

View file

@ -154,6 +154,59 @@ async def test_model_info_v2_translates_team_model_name(monkeypatch):
assert "model_name_team-abc-123_4a6b8" not in names
@pytest.mark.asyncio
async def test_model_info_v2_exact_model_filter_matches_team_public_name(monkeypatch):
"""`/v2/model/info?model=<public name>` must keep the team-scoped row whose
`model_name` is the internal routing key: the dashboard links team model
chips with the public name, and the exact filter ran before translation."""
global_row = {
"model_name": "gpt-4o",
"litellm_params": {"model": "gpt-4o"},
"model_info": {"id": "normal-id-1", "db_model": False},
}
router = MagicMock()
router.model_list = [_team_row(), global_row]
monkeypatch.setattr(ps, "llm_router", router)
monkeypatch.setattr(ps, "user_model", None)
monkeypatch.setattr(ps, "prisma_client", MagicMock())
monkeypatch.setattr(ps.proxy_config, "get_config", AsyncMock(return_value={}))
monkeypatch.setattr(
ps,
"_apply_search_filter_to_models",
AsyncMock(side_effect=lambda all_models, **kw: (all_models, len(all_models))),
)
monkeypatch.setattr(
ps, "_enrich_model_info_with_litellm_data", lambda model, **kw: model
)
import litellm.proxy.agent_endpoints.model_list_helpers as mlh
monkeypatch.setattr(
mlh,
"append_agents_to_model_info",
AsyncMock(side_effect=lambda models, **kw: models),
)
admin = UserAPIKeyAuth(user_id="u", user_role=LitellmUserRoles.PROXY_ADMIN)
resp = await ps.model_info_v2(
user_api_key_dict=admin,
model="team-claude-sonnet",
user_models_only=False,
include_team_models=False,
debug=False,
page=1,
size=50,
search=None,
modelId=None,
teamId=None,
sortBy=None,
sortOrder="asc",
)
assert [m["model_name"] for m in resp["data"]] == ["team-claude-sonnet"]
assert resp["total_count"] == 1
@pytest.mark.asyncio
async def test_model_info_v1_list_path_translates_team_model_name(monkeypatch):
"""/v1/model/info list path (no litellm_model_id) must include team-scoped