Skip to content

Commit 5cd20d2

Browse files
authored
(UI) allow adding model aliases for teams (#8471)
* update team info endpoint * clean up model alias * fix model alias * fix model alias card * clean up naming on docs * fix model alias card * fix _model_in_team_aliases * fix key_model_access_denied * test_can_key_call_model_with_aliases * fix test_aview_spend_per_user
1 parent ac14cfc commit 5cd20d2

6 files changed

Lines changed: 347 additions & 2 deletions

File tree

litellm/proxy/auth/auth_checks.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ async def common_checks(
101101
team_object=team_object,
102102
model=_model,
103103
llm_router=llm_router,
104+
team_model_aliases=valid_token.team_model_aliases if valid_token else None,
104105
)
105106

106107
## 2.1 If user can call model (if personal key)
@@ -968,6 +969,7 @@ async def _can_object_call_model(
968969
model: str,
969970
llm_router: Optional[Router],
970971
models: List[str],
972+
team_model_aliases: Optional[Dict[str, str]] = None,
971973
) -> Literal[True]:
972974
"""
973975
Checks if token can call a given model
@@ -1002,6 +1004,9 @@ async def _can_object_call_model(
10021004

10031005
verbose_proxy_logger.debug(f"model: {model}; allowed_models: {filtered_models}")
10041006

1007+
if _model_in_team_aliases(model=model, team_model_aliases=team_model_aliases):
1008+
return True
1009+
10051010
if _model_matches_any_wildcard_pattern_in_list(
10061011
model=model, allowed_model_list=filtered_models
10071012
):
@@ -1026,6 +1031,26 @@ async def _can_object_call_model(
10261031
return True
10271032

10281033

1034+
def _model_in_team_aliases(
1035+
model: str, team_model_aliases: Optional[Dict[str, str]] = None
1036+
) -> bool:
1037+
"""
1038+
Returns True if `model` being accessed is an alias of a team model
1039+
1040+
- `model=gpt-4o`
1041+
- `team_model_aliases={"gpt-4o": "gpt-4o-team-1"}`
1042+
- returns True
1043+
1044+
- `model=gp-4o`
1045+
- `team_model_aliases={"o-3": "o3-preview"}`
1046+
- returns False
1047+
"""
1048+
if team_model_aliases:
1049+
if model in team_model_aliases:
1050+
return True
1051+
return False
1052+
1053+
10291054
async def can_key_call_model(
10301055
model: str,
10311056
llm_model_list: Optional[list],
@@ -1045,6 +1070,7 @@ async def can_key_call_model(
10451070
model=model,
10461071
llm_router=llm_router,
10471072
models=valid_token.models,
1073+
team_model_aliases=valid_token.team_model_aliases,
10481074
)
10491075

10501076

@@ -1217,6 +1243,7 @@ def _team_model_access_check(
12171243
model: Optional[str],
12181244
team_object: Optional[LiteLLM_TeamTable],
12191245
llm_router: Optional[Router],
1246+
team_model_aliases: Optional[Dict[str, str]] = None,
12201247
):
12211248
"""
12221249
Access check for team models
@@ -1244,6 +1271,8 @@ def _team_model_access_check(
12441271
pass
12451272
elif model and "*" in model:
12461273
pass
1274+
elif _model_in_team_aliases(model=model, team_model_aliases=team_model_aliases):
1275+
pass
12471276
elif _model_matches_any_wildcard_pattern_in_list(
12481277
model=model, allowed_model_list=team_object.models
12491278
):

litellm/proxy/management_endpoints/team_endpoints.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1516,7 +1516,11 @@ async def list_team(
15161516
detail={"error": CommonProxyErrors.db_not_connected_error.value},
15171517
)
15181518

1519-
response = await prisma_client.db.litellm_teamtable.find_many()
1519+
response = await prisma_client.db.litellm_teamtable.find_many(
1520+
include={
1521+
"litellm_model_table": True,
1522+
}
1523+
)
15201524

15211525
filtered_response = []
15221526
if user_id:

tests/proxy_admin_ui_tests/test_key_management.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1022,3 +1022,82 @@ async def test_key_generate_always_db_team(mock_get_team_object):
10221022

10231023
mock_get_team_object.assert_called_once()
10241024
assert mock_get_team_object.call_args.kwargs["check_db_only"] == True
1025+
1026+
1027+
@pytest.mark.asyncio
1028+
@pytest.mark.parametrize(
1029+
"requested_model, should_pass",
1030+
[
1031+
("gpt-4o", True), # Should pass - exact match in aliases
1032+
("gpt-4o-team1", True), # Should pass - team has access to this deployment
1033+
("gpt-4o-mini", False), # Should fail - not in aliases
1034+
("o-3", False), # Should fail - not in aliases
1035+
],
1036+
)
1037+
async def test_team_model_alias(prisma_client, requested_model, should_pass):
1038+
"""
1039+
Test team model alias functionality:
1040+
1. Create team with model alias = `{gpt-4o: gpt-4o-team1}`
1041+
2. Generate key for that team with model = `gpt-4o`
1042+
3. Verify chat completion request works with aliased model = `gpt-4o`
1043+
"""
1044+
litellm.set_verbose = True
1045+
setattr(litellm.proxy.proxy_server, "prisma_client", prisma_client)
1046+
setattr(litellm.proxy.proxy_server, "master_key", "sk-1234")
1047+
await litellm.proxy.proxy_server.prisma_client.connect()
1048+
1049+
# Create team with model alias
1050+
team_id = f"test_team_{uuid.uuid4()}"
1051+
await new_team(
1052+
data=NewTeamRequest(
1053+
team_id=team_id,
1054+
team_alias=f"test_team_alias_{uuid.uuid4()}",
1055+
models=["gpt-4o-team1"],
1056+
model_aliases={"gpt-4o": "gpt-4o-team1"},
1057+
),
1058+
http_request=Request(scope={"type": "http"}),
1059+
user_api_key_dict=UserAPIKeyAuth(
1060+
user_role=LitellmUserRoles.PROXY_ADMIN, api_key="sk-1234", user_id="admin"
1061+
),
1062+
)
1063+
1064+
# Generate key for the team
1065+
new_key = await generate_key_fn(
1066+
data=GenerateKeyRequest(
1067+
team_id=team_id,
1068+
models=["gpt-4o-team1"],
1069+
),
1070+
user_api_key_dict=UserAPIKeyAuth(
1071+
user_role=LitellmUserRoles.PROXY_ADMIN, api_key="sk-1234", user_id="admin"
1072+
),
1073+
)
1074+
1075+
generated_key = new_key.key
1076+
1077+
# Test chat completion request
1078+
request = Request(scope={"type": "http"})
1079+
request._url = URL(url="/chat/completions")
1080+
1081+
async def return_body():
1082+
return_string = f'{{"model": "{requested_model}"}}'
1083+
return return_string.encode()
1084+
1085+
request.body = return_body
1086+
1087+
if should_pass:
1088+
# Verify the key works with the aliased model
1089+
result = await user_api_key_auth(
1090+
request=request, api_key=f"Bearer {generated_key}"
1091+
)
1092+
1093+
assert result.models == [
1094+
"gpt-4o-team1"
1095+
], "Expected model list to contain aliased model"
1096+
assert result.team_model_aliases == {
1097+
"gpt-4o": "gpt-4o-team1"
1098+
}, "Expected model aliases to be present"
1099+
else:
1100+
# Verify the key fails with non-aliased models
1101+
with pytest.raises(Exception) as exc_info:
1102+
await user_api_key_auth(request=request, api_key=f"Bearer {generated_key}")
1103+
assert exc_info.value.type == ProxyErrorTypes.key_model_access_denied

tests/proxy_unit_tests/test_auth_checks.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -633,3 +633,52 @@ async def test_get_fuzzy_user_object():
633633
mock_prisma.db.litellm_usertable.find_unique.assert_called_with(
634634
where={"sso_user_id": "sso_123"}, include={"organization_memberships": True}
635635
)
636+
637+
638+
@pytest.mark.parametrize(
639+
"model, alias_map, expect_to_work",
640+
[
641+
("gpt-4", {"gpt-4": "gpt-4-team1"}, True), # model matches alias value
642+
("gpt-5", {"gpt-4": "gpt-4-team1"}, False),
643+
],
644+
)
645+
@pytest.mark.asyncio
646+
async def test_can_key_call_model_with_aliases(model, alias_map, expect_to_work):
647+
"""
648+
Test if can_key_call_model correctly handles model aliases in the token
649+
"""
650+
from litellm.proxy.auth.auth_checks import can_key_call_model
651+
652+
llm_model_list = [
653+
{
654+
"model_name": "gpt-4-team1",
655+
"litellm_params": {
656+
"model": "gpt-4",
657+
"api_key": "test-api-key",
658+
},
659+
}
660+
]
661+
router = litellm.Router(model_list=llm_model_list)
662+
663+
user_api_key_object = UserAPIKeyAuth(
664+
models=[
665+
"gpt-4-team1",
666+
],
667+
team_model_aliases=alias_map,
668+
)
669+
670+
if expect_to_work:
671+
await can_key_call_model(
672+
model=model,
673+
llm_model_list=llm_model_list,
674+
valid_token=user_api_key_object,
675+
llm_router=router,
676+
)
677+
else:
678+
with pytest.raises(Exception) as e:
679+
await can_key_call_model(
680+
model=model,
681+
llm_model_list=llm_model_list,
682+
valid_token=user_api_key_object,
683+
llm_router=router,
684+
)

0 commit comments

Comments
 (0)