mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-20 00:11:50 +00:00
Merge commit '957af3023594810253a0abf06cf51348bfa43a4c' into litellm_team_byok_models_fix_uuid_v2
This commit is contained in:
commit
7c78646de3
2 changed files with 169 additions and 0 deletions
|
|
@ -8343,6 +8343,12 @@ async def model_list(
|
|||
if hidden_names:
|
||||
all_models = [m for m in all_models if m not in hidden_names]
|
||||
|
||||
# Opt-in (default off): surface the public name for team-scoped rows.
|
||||
# Off by default so /v1/models model ids stay backward-compatible for
|
||||
# callers that scripted against the internal routing name.
|
||||
if general_settings.get("use_team_public_model_name", False):
|
||||
all_models = _translate_model_names_for_listing(all_models, llm_router)
|
||||
|
||||
# Build response data with all proxy models
|
||||
model_data = []
|
||||
for model in all_models:
|
||||
|
|
@ -8380,6 +8386,12 @@ async def model_list(
|
|||
if hidden_names:
|
||||
all_models = [m for m in all_models if m not in hidden_names]
|
||||
|
||||
# Opt-in (default off): surface the public name for team-scoped rows.
|
||||
# Off by default so /v1/models model ids stay backward-compatible for
|
||||
# callers that scripted against the internal routing name.
|
||||
if general_settings.get("use_team_public_model_name", False):
|
||||
all_models = _translate_model_names_for_listing(all_models, llm_router)
|
||||
|
||||
# Build response data
|
||||
model_data = []
|
||||
for model in all_models:
|
||||
|
|
@ -12589,6 +12601,34 @@ def _translate_model_name_for_response(model: dict) -> dict:
|
|||
return {**model, "model_name": team_public}
|
||||
|
||||
|
||||
def _translate_model_names_for_listing(model_names: List[str], llm_router) -> List[str]:
|
||||
"""Swap internal team routing keys for their public names in list-style
|
||||
responses (e.g. `/v1/models`, `/models`).
|
||||
|
||||
`/v1/models` builds from bare model-name strings produced by access-group
|
||||
expansion (`get_model_access_groups`), which surfaces the internal routing
|
||||
key `model_name_{team_id}_{uuid}` for team-scoped (BYOK) deployments. This
|
||||
is a presentation-layer swap only -- access-group/auth semantics are
|
||||
unchanged (see issue #28382). Sibling deployments collapse to one public
|
||||
name, so the result is de-duplicated while preserving order.
|
||||
"""
|
||||
if llm_router is None:
|
||||
return model_names
|
||||
internal_to_public: Dict[str, str] = {}
|
||||
for m in getattr(llm_router, "model_list", None) or []:
|
||||
model_info = m.get("model_info") or {}
|
||||
if not isinstance(model_info, dict):
|
||||
continue
|
||||
team_id = model_info.get("team_id")
|
||||
team_public = model_info.get("team_public_model_name")
|
||||
name = m.get("model_name") or ""
|
||||
if team_id and team_public and name.startswith(f"model_name_{team_id}_"):
|
||||
internal_to_public[name] = team_public
|
||||
if not internal_to_public:
|
||||
return model_names
|
||||
return list(dict.fromkeys(internal_to_public.get(n, n) for n in model_names))
|
||||
|
||||
|
||||
def _get_proxy_model_info(model: dict) -> dict:
|
||||
# provided model_info in config.yaml
|
||||
model_info = model.get("model_info", {})
|
||||
|
|
|
|||
|
|
@ -593,3 +593,132 @@ async def test_model_info_v1_litellm_model_id_team_id_applies_team_filter(monkey
|
|||
team_filter.assert_awaited_once()
|
||||
assert team_filter.await_args.kwargs["team_id"] == "other-team"
|
||||
assert team_filter.await_args.kwargs["all_models"] == [team_row]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_v1_models_translates_team_model_for_access_group_key(monkeypatch):
|
||||
"""Regression (#28382 sibling leak): a virtual key whose model access group
|
||||
resolves to a team BYOK deployment must list the PUBLIC name in /v1/models,
|
||||
not the internal routing key model_name_{team_id}_{uuid}.
|
||||
|
||||
The /model/info read-path fix did not cover /v1/models, which builds from
|
||||
bare model-name strings via access-group expansion.
|
||||
"""
|
||||
team_dep = {
|
||||
"model_name": "model_name_teamX_uuid9",
|
||||
"litellm_params": {"model": "azure/gpt-4.1"},
|
||||
"model_info": {
|
||||
"id": "id1",
|
||||
"team_id": "teamX",
|
||||
"team_public_model_name": "tushar-gpt-4.1",
|
||||
"access_groups": ["grp-a"],
|
||||
},
|
||||
}
|
||||
router = MagicMock()
|
||||
router.get_model_names.return_value = ["model_name_teamX_uuid9"]
|
||||
router.get_model_access_groups.return_value = {"grp-a": ["model_name_teamX_uuid9"]}
|
||||
router.get_fully_blocked_model_names.return_value = set()
|
||||
router.model_list = [team_dep]
|
||||
router.get_model_list.return_value = [team_dep]
|
||||
|
||||
monkeypatch.setattr(ps, "llm_router", router)
|
||||
monkeypatch.setattr(ps, "user_model", None)
|
||||
# opt-in flag ON -> listing surfaces public names
|
||||
monkeypatch.setattr(ps, "general_settings", {"use_team_public_model_name": True})
|
||||
|
||||
# virtual key granted access via the access group (no team membership)
|
||||
key = UserAPIKeyAuth(
|
||||
user_id="u", api_key="sk-test", models=["grp-a"], team_models=[]
|
||||
)
|
||||
resp = await ps.model_list(user_api_key_dict=key)
|
||||
|
||||
ids = [d["id"] for d in resp["data"]]
|
||||
assert "tushar-gpt-4.1" in ids
|
||||
assert "model_name_teamX_uuid9" not in ids
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_v1_models_keeps_internal_names_when_flag_off(monkeypatch):
|
||||
"""Default (flag off): /v1/models stays backward-compatible and lists the
|
||||
internal routing name, so consumers that scripted against those ids are not
|
||||
broken. Translation is opt-in via
|
||||
general_settings['use_team_public_model_name'].
|
||||
"""
|
||||
team_dep = {
|
||||
"model_name": "model_name_teamX_uuid9",
|
||||
"litellm_params": {"model": "azure/gpt-4.1"},
|
||||
"model_info": {
|
||||
"id": "id1",
|
||||
"team_id": "teamX",
|
||||
"team_public_model_name": "tushar-gpt-4.1",
|
||||
"access_groups": ["grp-a"],
|
||||
},
|
||||
}
|
||||
router = MagicMock()
|
||||
router.get_model_names.return_value = ["model_name_teamX_uuid9"]
|
||||
router.get_model_access_groups.return_value = {"grp-a": ["model_name_teamX_uuid9"]}
|
||||
router.get_fully_blocked_model_names.return_value = set()
|
||||
router.model_list = [team_dep]
|
||||
router.get_model_list.return_value = [team_dep]
|
||||
|
||||
monkeypatch.setattr(ps, "llm_router", router)
|
||||
monkeypatch.setattr(ps, "user_model", None)
|
||||
monkeypatch.setattr(ps, "general_settings", {}) # flag absent -> default off
|
||||
|
||||
key = UserAPIKeyAuth(
|
||||
user_id="u", api_key="sk-test", models=["grp-a"], team_models=[]
|
||||
)
|
||||
resp = await ps.model_list(user_api_key_dict=key)
|
||||
|
||||
ids = [d["id"] for d in resp["data"]]
|
||||
assert "model_name_teamX_uuid9" in ids # internal id preserved (backward-compat)
|
||||
assert "tushar-gpt-4.1" not in ids
|
||||
|
||||
|
||||
def test_translate_model_names_for_listing_swaps_and_dedupes():
|
||||
"""Internal team routing keys -> public name; sibling deployments sharing a
|
||||
public name collapse to one entry (order preserved); globals untouched."""
|
||||
from litellm.proxy.proxy_server import _translate_model_names_for_listing
|
||||
|
||||
router = MagicMock()
|
||||
router.model_list = [
|
||||
{
|
||||
"model_name": "model_name_teamX_uuidA",
|
||||
"model_info": {
|
||||
"team_id": "teamX",
|
||||
"team_public_model_name": "tushar-gpt-4.1",
|
||||
},
|
||||
},
|
||||
{
|
||||
"model_name": "model_name_teamX_uuidB", # sibling: same public name
|
||||
"model_info": {
|
||||
"team_id": "teamX",
|
||||
"team_public_model_name": "tushar-gpt-4.1",
|
||||
},
|
||||
},
|
||||
{"model_name": "gpt-4o", "model_info": {"db_model": False}},
|
||||
]
|
||||
|
||||
out = _translate_model_names_for_listing(
|
||||
["model_name_teamX_uuidA", "model_name_teamX_uuidB", "gpt-4o"], router
|
||||
)
|
||||
assert out == ["tushar-gpt-4.1", "gpt-4o"]
|
||||
|
||||
|
||||
def test_translate_model_names_for_listing_leaves_unmapped_names():
|
||||
"""Names with no team mapping (globals, access-group keys) pass through."""
|
||||
from litellm.proxy.proxy_server import _translate_model_names_for_listing
|
||||
|
||||
router = MagicMock()
|
||||
router.model_list = [{"model_name": "gpt-4o", "model_info": {"db_model": False}}]
|
||||
assert _translate_model_names_for_listing(["gpt-4o", "beta-group"], router) == [
|
||||
"gpt-4o",
|
||||
"beta-group",
|
||||
]
|
||||
|
||||
|
||||
def test_translate_model_names_for_listing_none_router():
|
||||
"""No router -> return the input list unchanged."""
|
||||
from litellm.proxy.proxy_server import _translate_model_names_for_listing
|
||||
|
||||
assert _translate_model_names_for_listing(["a", "b"], None) == ["a", "b"]
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue