mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-14 23:21:35 +00:00
test(proxy): cover team model listing metadata
This commit is contained in:
parent
deb1bef859
commit
df00250b37
2 changed files with 95 additions and 19 deletions
|
|
@ -15,6 +15,7 @@ import threading
|
|||
import time
|
||||
import traceback
|
||||
import warnings
|
||||
from collections.abc import Mapping, Sequence
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
|
|
@ -8345,8 +8346,10 @@ async def model_list(
|
|||
|
||||
# Surface the public name for team-scoped rows by default. Operators
|
||||
# that need legacy internal routing keys can explicitly disable this.
|
||||
if general_settings.get("use_team_public_model_name", True):
|
||||
all_models = _translate_model_names_for_listing(all_models, llm_router)
|
||||
if _should_use_team_public_model_name():
|
||||
all_models = _translate_model_names_for_listing(
|
||||
all_models, _get_team_model_deployments_for_listing(llm_router)
|
||||
)
|
||||
|
||||
# Build response data with all proxy models
|
||||
model_data = []
|
||||
|
|
@ -8387,8 +8390,10 @@ async def model_list(
|
|||
|
||||
# Surface the public name for team-scoped rows by default. Operators that
|
||||
# need legacy internal routing keys can explicitly disable this.
|
||||
if general_settings.get("use_team_public_model_name", True):
|
||||
all_models = _translate_model_names_for_listing(all_models, llm_router)
|
||||
if _should_use_team_public_model_name():
|
||||
all_models = _translate_model_names_for_listing(
|
||||
all_models, _get_team_model_deployments_for_listing(llm_router)
|
||||
)
|
||||
|
||||
# Build response data
|
||||
model_data = []
|
||||
|
|
@ -12599,7 +12604,24 @@ 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]:
|
||||
def _should_use_team_public_model_name() -> bool:
|
||||
settings = cast(dict[str, object], general_settings) # any-ok: legacy settings
|
||||
use_public_name = settings.get("use_team_public_model_name", True)
|
||||
return use_public_name is not False
|
||||
|
||||
|
||||
def _get_team_model_deployments_for_listing(
|
||||
llm_router: Router | None,
|
||||
) -> Sequence[Mapping[str, object]]:
|
||||
if llm_router is None:
|
||||
return ()
|
||||
return cast(Sequence[Mapping[str, object]], llm_router.get_model_list() or ())
|
||||
|
||||
|
||||
def _translate_model_names_for_listing(
|
||||
model_names: list[str],
|
||||
model_deployments: Sequence[Mapping[str, object]],
|
||||
) -> list[str]:
|
||||
"""Swap internal team routing keys for their public names in list-style
|
||||
responses (e.g. `/v1/models`, `/models`).
|
||||
|
||||
|
|
@ -12610,21 +12632,35 @@ def _translate_model_names_for_listing(model_names: List[str], llm_router) -> Li
|
|||
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:
|
||||
if not model_deployments:
|
||||
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):
|
||||
internal_to_public: dict[str, str] = {}
|
||||
for model in model_deployments:
|
||||
model_info_raw = model.get("model_info")
|
||||
if not isinstance(model_info_raw, Mapping):
|
||||
continue
|
||||
model_info = cast(Mapping[str, object], model_info_raw) # any-ok: checked
|
||||
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}_"):
|
||||
name = model.get("model_name")
|
||||
if (
|
||||
isinstance(team_id, str)
|
||||
and isinstance(team_public, str)
|
||||
and isinstance(name, str)
|
||||
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))
|
||||
translated_names: list[str] = []
|
||||
seen_names: set[str] = set()
|
||||
for model_name in model_names:
|
||||
translated_name = internal_to_public.get(model_name, model_name)
|
||||
if translated_name in seen_names:
|
||||
continue
|
||||
seen_names.add(translated_name)
|
||||
translated_names.append(translated_name)
|
||||
return translated_names
|
||||
|
||||
|
||||
def _get_proxy_model_info(model: dict) -> dict:
|
||||
|
|
|
|||
|
|
@ -676,6 +676,46 @@ async def test_v1_models_keeps_internal_names_when_public_name_flag_disabled(
|
|||
assert "tushar-gpt-4.1" not in ids
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_v1_models_translates_team_model_with_metadata(monkeypatch):
|
||||
"""include_metadata=true must build metadata for the public model id."""
|
||||
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", {})
|
||||
|
||||
key = UserAPIKeyAuth(
|
||||
user_id="u", api_key="sk-test", models=["grp-a"], team_models=[]
|
||||
)
|
||||
resp = await ps.model_list(user_api_key_dict=key, include_metadata=True)
|
||||
|
||||
assert resp["data"] == [
|
||||
{
|
||||
"id": "tushar-gpt-4.1",
|
||||
"object": "model",
|
||||
"created": 1677610602,
|
||||
"owned_by": "openai",
|
||||
"metadata": {"fallbacks": []},
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
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."""
|
||||
|
|
@ -701,7 +741,8 @@ def test_translate_model_names_for_listing_swaps_and_dedupes():
|
|||
]
|
||||
|
||||
out = _translate_model_names_for_listing(
|
||||
["model_name_teamX_uuidA", "model_name_teamX_uuidB", "gpt-4o"], router
|
||||
["model_name_teamX_uuidA", "model_name_teamX_uuidB", "gpt-4o"],
|
||||
router.model_list,
|
||||
)
|
||||
assert out == ["tushar-gpt-4.1", "gpt-4o"]
|
||||
|
||||
|
|
@ -712,14 +753,13 @@ def test_translate_model_names_for_listing_leaves_unmapped_names():
|
|||
|
||||
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",
|
||||
]
|
||||
assert _translate_model_names_for_listing(
|
||||
["gpt-4o", "beta-group"], router.model_list
|
||||
) == ["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"]
|
||||
assert _translate_model_names_for_listing(["a", "b"], ()) == ["a", "b"]
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue