Show user all models they can call (Across teams) on UI (#11948)

* fix(proxy_cli.py): check for module not found error on proxy import

Fixes https://github.com/BerriAI/litellm/issues/11836

* feat(proxy_server.py): utility function to get all models across all teams user is in

Allows user to see all team models on UI

* feat(proxy_server.py): return models accessible via team id's in `/v2/model/info`

Allows UI to tell user which team they can use to access model

* feat(columsn.tsx): initial PR to add 'accessible via Team ID's on model hub

allows user to know what teams they can access a model through

* Revert "feat(columsn.tsx): initial PR to add 'accessible via Team ID's on model hub"

This reverts commit f844c79383.

* fix(proxy_server.py): backend model info endpoint improvements

* UI Improvements for Default User access (#11952)

* feat(ui/): add a 'current team' and 'view' filters to the models page

allow user to see what all models they have access to within a specific team

* feat: working ui for seeing models in teams

* fix(model_dashboard.tsx): make current team filter more prominent

* style(model_dashboard.tsx): add a helpful note telling user how to create a model for the team they've selected

* style(model_dashboard.tsx): only show helpful note when current view is team, not for global

* fix(team_dropdown.tsx): allow searching by team id on create key modal

* feat(create_key_button.tsx): add helpful message when team selection is required

* fix: fix linting checks

* fix: fix ui linting error

* docs(team_endpoints.py): document new param
This commit is contained in:
Krish Dholakia 2025-06-21 15:29:25 -07:00 • committed by GitHub
parent 6c17e856d1
commit 2404d3fb1b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 521 additions and 65 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -2,6 +2,6 @@
3:I[78401,["665","static/chunks/3014691f-b7b79b78e27792f3.js","990","static/chunks/13b76428-ebdf3012af0e4489.js","402","static/chunks/402-d418d0a7bf158a22.js","313","static/chunks/313-fe5a1ed341fdff45.js","899","static/chunks/899-dd9d2ed8efc1b4ac.js","339","static/chunks/339-9ad819586efb0d68.js","250","static/chunks/250-46ca089b25e2e50d.js","699","static/chunks/699-2e0b76ba9cd1d301.js","931","static/chunks/app/page-cbefa9dd90304eaf.js"],"default",1]
4:I[4707,[],""]
5:I[36423,[],""]
0:["H40ScCE0aF9WfwOsAFKzy",[[["",{"children":["__PAGE__",{}]},"$undefined","$undefined",true],["",{"children":["__PAGE__",{},[["$L1",["$","$L2",null,{"props":{"params":{},"searchParams":{}},"Component":"$3"}],null],null],null]},[[[["$","link","0",{"rel":"stylesheet","href":"/litellm-asset-prefix/_next/static/css/31b7f215e119031e.css","precedence":"next","crossOrigin":"$undefined"}],["$","link","1",{"rel":"stylesheet","href":"/litellm-asset-prefix/_next/static/css/27aa365198bdc7c8.css","precedence":"next","crossOrigin":"$undefined"}]],["$","html",null,{"lang":"en","children":["$","body",null,{"className":"__className_b0dd8a","children":["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[]}]}]}]],null],null],["$L6",null]]]]
0:["H40ScCE0aF9WfwOsAFKzy",[[["",{"children":["__PAGE__",{}]},"$undefined","$undefined",true],["",{"children":["__PAGE__",{},[["$L1",["$","$L2",null,{"props":{"params":{},"searchParams":{}},"Component":"$3"}],null],null],null]},[[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/31b7f215e119031e.css","precedence":"next","crossOrigin":"$undefined"}],["$","link","1",{"rel":"stylesheet","href":"/_next/static/css/27aa365198bdc7c8.css","precedence":"next","crossOrigin":"$undefined"}]],["$","html",null,{"lang":"en","children":["$","body",null,{"className":"__className_b0dd8a","children":["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[]}]}]}]],null],null],["$L6",null]]]]
6:[["$","meta","0",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","meta","1",{"charSet":"utf-8"}],["$","title","2",{"children":"LiteLLM Dashboard"}],["$","meta","3",{"name":"description","content":"LiteLLM Proxy Admin UI"}],["$","link","4",{"rel":"icon","href":"/favicon.ico","type":"image/x-icon","sizes":"16x16"}],["$","link","5",{"rel":"icon","href":"./favicon.ico"}],["$","meta","6",{"name":"next-size-adjust"}]]
1:null

View file

@ -2,6 +2,6 @@
3:I[52829,["402","static/chunks/402-d418d0a7bf158a22.js","313","static/chunks/313-fe5a1ed341fdff45.js","250","static/chunks/250-46ca089b25e2e50d.js","699","static/chunks/699-2e0b76ba9cd1d301.js","418","static/chunks/app/model_hub/page-ce40c5a05f3174ca.js"],"default",1]
4:I[4707,[],""]
5:I[36423,[],""]
0:["H40ScCE0aF9WfwOsAFKzy",[[["",{"children":["model_hub",{"children":["__PAGE__",{}]}]},"$undefined","$undefined",true],["",{"children":["model_hub",{"children":["__PAGE__",{},[["$L1",["$","$L2",null,{"props":{"params":{},"searchParams":{}},"Component":"$3"}],null],null],null]},[null,["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children","model_hub","children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined"}]],null]},[[[["$","link","0",{"rel":"stylesheet","href":"/litellm-asset-prefix/_next/static/css/31b7f215e119031e.css","precedence":"next","crossOrigin":"$undefined"}],["$","link","1",{"rel":"stylesheet","href":"/litellm-asset-prefix/_next/static/css/27aa365198bdc7c8.css","precedence":"next","crossOrigin":"$undefined"}]],["$","html",null,{"lang":"en","children":["$","body",null,{"className":"__className_b0dd8a","children":["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[]}]}]}]],null],null],["$L6",null]]]]
0:["H40ScCE0aF9WfwOsAFKzy",[[["",{"children":["model_hub",{"children":["__PAGE__",{}]}]},"$undefined","$undefined",true],["",{"children":["model_hub",{"children":["__PAGE__",{},[["$L1",["$","$L2",null,{"props":{"params":{},"searchParams":{}},"Component":"$3"}],null],null],null]},[null,["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children","model_hub","children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined"}]],null]},[[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/31b7f215e119031e.css","precedence":"next","crossOrigin":"$undefined"}],["$","link","1",{"rel":"stylesheet","href":"/_next/static/css/27aa365198bdc7c8.css","precedence":"next","crossOrigin":"$undefined"}]],["$","html",null,{"lang":"en","children":["$","body",null,{"className":"__className_b0dd8a","children":["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[]}]}]}]],null],null],["$L6",null]]]]
6:[["$","meta","0",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","meta","1",{"charSet":"utf-8"}],["$","title","2",{"children":"LiteLLM Dashboard"}],["$","meta","3",{"name":"description","content":"LiteLLM Proxy Admin UI"}],["$","link","4",{"rel":"icon","href":"/favicon.ico","type":"image/x-icon","sizes":"16x16"}],["$","link","5",{"rel":"icon","href":"./favicon.ico"}],["$","meta","6",{"name":"next-size-adjust"}]]
1:null

View file

@ -2,6 +2,6 @@
3:I[12011,["665","static/chunks/3014691f-b7b79b78e27792f3.js","402","static/chunks/402-d418d0a7bf158a22.js","899","static/chunks/899-dd9d2ed8efc1b4ac.js","250","static/chunks/250-46ca089b25e2e50d.js","461","static/chunks/app/onboarding/page-e05c770288debeda.js"],"default",1]
4:I[4707,[],""]
5:I[36423,[],""]
0:["H40ScCE0aF9WfwOsAFKzy",[[["",{"children":["onboarding",{"children":["__PAGE__",{}]}]},"$undefined","$undefined",true],["",{"children":["onboarding",{"children":["__PAGE__",{},[["$L1",["$","$L2",null,{"props":{"params":{},"searchParams":{}},"Component":"$3"}],null],null],null]},[null,["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children","onboarding","children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined"}]],null]},[[[["$","link","0",{"rel":"stylesheet","href":"/litellm-asset-prefix/_next/static/css/31b7f215e119031e.css","precedence":"next","crossOrigin":"$undefined"}],["$","link","1",{"rel":"stylesheet","href":"/litellm-asset-prefix/_next/static/css/27aa365198bdc7c8.css","precedence":"next","crossOrigin":"$undefined"}]],["$","html",null,{"lang":"en","children":["$","body",null,{"className":"__className_b0dd8a","children":["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[]}]}]}]],null],null],["$L6",null]]]]
0:["H40ScCE0aF9WfwOsAFKzy",[[["",{"children":["onboarding",{"children":["__PAGE__",{}]}]},"$undefined","$undefined",true],["",{"children":["onboarding",{"children":["__PAGE__",{},[["$L1",["$","$L2",null,{"props":{"params":{},"searchParams":{}},"Component":"$3"}],null],null],null]},[null,["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children","onboarding","children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined"}]],null]},[[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/31b7f215e119031e.css","precedence":"next","crossOrigin":"$undefined"}],["$","link","1",{"rel":"stylesheet","href":"/_next/static/css/27aa365198bdc7c8.css","precedence":"next","crossOrigin":"$undefined"}]],["$","html",null,{"lang":"en","children":["$","body",null,{"className":"__className_b0dd8a","children":["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[]}]}]}]],null],null],["$L6",null]]]]
6:[["$","meta","0",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","meta","1",{"charSet":"utf-8"}],["$","title","2",{"children":"LiteLLM Dashboard"}],["$","meta","3",{"name":"description","content":"LiteLLM Proxy Admin UI"}],["$","link","4",{"rel":"icon","href":"/favicon.ico","type":"image/x-icon","sizes":"16x16"}],["$","link","5",{"rel":"icon","href":"./favicon.ico"}],["$","meta","6",{"name":"next-size-adjust"}]]
1:null

View file

@ -5,13 +5,14 @@ import os
import random
import subprocess
import sys
import urllib.parse
import urllib.parse as urlparse
from typing import TYPE_CHECKING, Any, Optional, Union
import click
import httpx
from dotenv import load_dotenv
import urllib.parse
if TYPE_CHECKING:
from fastapi import FastAPI
else:
@ -539,6 +540,10 @@ def run_server( # noqa: PLR0915
app,
save_worker_config,
)
except ModuleNotFoundError as e:
raise ModuleNotFoundError(
f"Missing dependency {e}. Run `pip install 'litellm[proxy]'`"
)
except ImportError as e:
if "litellm[proxy]" in str(e):
# user is missing a proxy dependency, ask them to pip install litellm[proxy]
@ -788,7 +793,9 @@ def run_server( # noqa: PLR0915
# Skip server startup if requested (after all setup is done)
if skip_server_startup:
print("LiteLLM: Setup complete. Skipping server startup as requested.") # noqa
print( # noqa
"LiteLLM: Setup complete. Skipping server startup as requested."
)
return
uvicorn_args = ProxyInitializationHelpers._get_default_unvicorn_init_args(

View file

@ -16,7 +16,9 @@ from typing import (
TYPE_CHECKING,
Any,
List,
Literal,
Optional,
Set,
Tuple,
Union,
cast,
@ -5531,6 +5533,139 @@ async def non_admin_all_models(
return unique_models
async def get_all_team_models(
user_teams: Union[List[str], Literal["*"]],
prisma_client: PrismaClient,
llm_router: Router,
) -> Dict[str, List[str]]:
"""
Get all models across all teams user is in.
1. Get all teams user is in
2. Get all models across all teams
3. Return {"model_id": ["team_id1", "team_id2"]}
"""
team_models: Dict[str, Set[str]] = {}
team_db_objects_typed: List[LiteLLM_TeamTable] = []
if user_teams == "*":
team_db_objects = await prisma_client.db.litellm_teamtable.find_many()
team_db_objects_typed = [
LiteLLM_TeamTable(**team_db_object.model_dump())
for team_db_object in team_db_objects
]
else:
team_db_objects = await prisma_client.db.litellm_teamtable.find_many(
where={"team_id": {"in": user_teams}}
)
team_db_objects_typed = [
LiteLLM_TeamTable(**team_db_object.model_dump())
for team_db_object in team_db_objects
]
for team_object in team_db_objects_typed:
if (
len(team_object.models) == 0 # empty list = all model access
or SpecialModelNames.all_proxy_models.value in team_object.models
):
_model_ids = llm_router.get_model_ids()
if _model_ids is not None:
for model_id in _model_ids:
team_models.setdefault(model_id, set()).add(team_object.team_id)
else:
for model_name in team_object.models:
_models = llm_router.get_model_list(model_name=model_name)
if _models is not None:
for model in _models:
model_id = model.get("model_info", {}).get("id", None)
if model_id is not None:
team_models.setdefault(model_id, set()).add(
team_object.team_id
)
# convert set to list
returned_team_models: Dict[str, List[str]] = {}
for model_id, team_ids in team_models.items():
returned_team_models[model_id] = list(team_ids)
return returned_team_models
def get_direct_access_models(
user_db_object: LiteLLM_UserTable,
llm_router: Router,
) -> List[str]:
"""
Get all models that user has direct access to
"""
direct_access_models: List[str] = []
for model in user_db_object.models:
deployments = llm_router.get_model_list(model_name=model)
if deployments is not None:
for deployment in deployments:
model_id = deployment.get("model_info", {}).get("id", None)
if model_id is not None:
direct_access_models.append(model_id)
return direct_access_models
async def get_all_team_and_direct_access_models(
user_api_key_dict: UserAPIKeyAuth,
prisma_client: PrismaClient,
llm_router: Router,
all_models: List[Dict],
) -> List[Dict]:
"""
Get all models across all teams user is in.
"""
user_teams: Optional[Union[List[str], Literal["*"]]] = None
direct_access_models: List[str] = []
if user_api_key_dict.user_role == LitellmUserRoles.PROXY_ADMIN:
user_teams = "*"
direct_access_models = llm_router.get_model_ids() # has access to all models
else:
user_db_object = await prisma_client.db.litellm_usertable.find_unique(
where={"user_id": user_api_key_dict.user_id}
)
if user_db_object is not None:
user_object = LiteLLM_UserTable(**user_db_object.model_dump())
user_teams = user_object.teams or []
direct_access_models = get_direct_access_models(
user_db_object=user_object,
llm_router=llm_router,
)
## ADD ACCESS_VIA_TEAM_IDS TO ALL MODELS
if user_teams is not None:
team_models = await get_all_team_models(
user_teams=user_teams,
prisma_client=prisma_client,
llm_router=llm_router,
)
for _model in all_models:
model_id = _model.get("model_info", {}).get("id", None)
if model_id is not None:
_model["model_info"]["access_via_team_ids"] = team_models.get(
model_id, []
)
## ADD DIRECT_ACCESS TO RELEVANT MODELS
for _model in all_models:
model_id = _model.get("model_info", {}).get("id", None)
if model_id is not None and model_id in direct_access_models:
_model["model_info"]["direct_access"] = True
## FILTER OUT MODELS THAT ARE NOT IN DIRECT_ACCESS_MODELS OR ACCESS_VIA_TEAM_IDS - only show user models they can call
all_models = [
_model
for _model in all_models
if _model.get("model_info", {}).get("direct_access", False)
or _model.get("model_info", {}).get("access_via_team_ids", [])
]
return all_models
@router.get(
"/v2/model/info",
description="v2 - returns models available to the user based on their API key permissions. Shows model info from config.yaml (except api key and api base). Filter to just user-added models with ?user_models_only=true",
@ -5546,6 +5681,9 @@ async def model_info_v2(
user_models_only: Optional[bool] = fastapi.Query(
False, description="Only return models added by this user"
),
include_team_models: Optional[bool] = fastapi.Query(
False, description="Return all models across all teams user is in."
),
debug: Optional[bool] = False,
):
"""
@ -5561,6 +5699,12 @@ async def model_info_v2(
},
)
if prisma_client is None:
raise HTTPException(
status_code=500,
detail={"error": CommonProxyErrors.db_not_connected_error.value},
)
# Load existing config
await proxy_config.get_config()
all_models = copy.deepcopy(llm_router.model_list)
@ -5569,10 +5713,6 @@ async def model_info_v2(
# if user does not use a config.yaml, https://github.com/BerriAI/litellm/issues/2061
all_models += [user_model]
# check all models user has access to in user_api_key_dict
if len(user_api_key_dict.models) > 0:
pass
if model is not None:
all_models = [m for m in all_models if m["model_name"] == model]
@ -5584,6 +5724,13 @@ async def model_info_v2(
prisma_client=prisma_client,
)
if include_team_models:
all_models = await get_all_team_and_direct_access_models(
user_api_key_dict=user_api_key_dict,
prisma_client=prisma_client,
llm_router=llm_router,
all_models=all_models,
)
# fill in model info based on config.yaml and litellm model_prices_and_context_window.json
for _model in all_models:
# provided model_info in config.yaml
@ -5635,10 +5782,7 @@ async def model_info_v2(
_model["model_info"] = model_info
# don't return the api key / vertex credentials
# don't return the llm credentials
_model["litellm_params"].pop("api_key", None)
_model["litellm_params"].pop("vertex_credentials", None)
_model["litellm_params"].pop("aws_access_key_id", None)
_model["litellm_params"].pop("aws_secret_access_key", None)
_model = remove_sensitive_info_from_deployment(_model)
verbose_proxy_logger.debug("all_models: %s", all_models)
return {"data": all_models}
@ -8024,7 +8168,7 @@ async def delete_callback(
Delete specific logging callback from configuration.
"""
global prisma_client, proxy_config
if prisma_client is None:
raise HTTPException(
status_code=400,
@ -8054,24 +8198,28 @@ async def delete_callback(
# Get current configuration
config = await proxy_config.get_config()
callback_name = data.callback_name.lower()
# Check if callback exists in current configuration
litellm_settings = config.get("litellm_settings", {})
success_callbacks = litellm_settings.get("success_callback", [])
if callback_name not in success_callbacks:
raise HTTPException(
status_code=404,
detail={"error": f"Callback '{callback_name}' not found in active configuration"},
detail={
"error": f"Callback '{callback_name}' not found in active configuration"
},
)
# Remove callback from success_callback list
success_callbacks.remove(callback_name)
config.setdefault("litellm_settings", {})["success_callback"] = success_callbacks
config.setdefault("litellm_settings", {})[
"success_callback"
] = success_callbacks
# Save the updated configuration
await proxy_config.save_config(new_config=config)
# Restart the proxy to apply changes
await proxy_config.add_deployment(
prisma_client=prisma_client, proxy_logging_obj=proxy_logging_obj

View file

@ -267,3 +267,170 @@ def test_embedding_input_array_of_tokens(mock_aembedding, client_no_auth):
assert len(result["data"][0]["embedding"]) > 10 # this usually has len==1536 so
except Exception as e:
pytest.fail(f"LiteLLM Proxy test failed. Exception - {str(e)}")
@pytest.mark.asyncio
async def test_get_all_team_models():
"""
Test get_all_team_models function with both "*" and specific team IDs
"""
from unittest.mock import AsyncMock, MagicMock
from litellm.proxy._types import LiteLLM_TeamTable
from litellm.proxy.proxy_server import get_all_team_models
# Mock team data
mock_team1 = MagicMock()
mock_team1.model_dump.return_value = {
"team_id": "team1",
"models": ["gpt-4", "gpt-3.5-turbo"],
"team_alias": "Team 1",
}
mock_team2 = MagicMock()
mock_team2.model_dump.return_value = {
"team_id": "team2",
"models": ["claude-3", "gpt-4"],
"team_alias": "Team 2",
}
# Mock model data returned by router
mock_models_gpt4 = [
{"model_info": {"id": "gpt-4-model-1"}},
{"model_info": {"id": "gpt-4-model-2"}},
]
mock_models_gpt35 = [
{"model_info": {"id": "gpt-3.5-turbo-model-1"}},
]
mock_models_claude = [
{"model_info": {"id": "claude-3-model-1"}},
]
# Mock prisma client
mock_prisma_client = MagicMock()
mock_db = MagicMock()
mock_litellm_teamtable = MagicMock()
mock_prisma_client.db = mock_db
mock_db.litellm_teamtable = mock_litellm_teamtable
# Make find_many async
mock_litellm_teamtable.find_many = AsyncMock()
# Mock router
mock_router = MagicMock()
def mock_get_model_list(model_name):
if model_name == "gpt-4":
return mock_models_gpt4
elif model_name == "gpt-3.5-turbo":
return mock_models_gpt35
elif model_name == "claude-3":
return mock_models_claude
return None
mock_router.get_model_list.side_effect = mock_get_model_list
# Test Case 1: user_teams = "*" (all teams)
mock_litellm_teamtable.find_many.return_value = [mock_team1, mock_team2]
with patch("litellm.proxy.proxy_server.LiteLLM_TeamTable") as mock_team_table_class:
# Configure the mock class to return proper instances
def mock_team_table_constructor(**kwargs):
mock_instance = MagicMock()
mock_instance.team_id = kwargs["team_id"]
mock_instance.models = kwargs["models"]
return mock_instance
mock_team_table_class.side_effect = mock_team_table_constructor
result = await get_all_team_models(
user_teams="*",
prisma_client=mock_prisma_client,
llm_router=mock_router,
)
# Verify find_many was called without where clause for "*"
mock_litellm_teamtable.find_many.assert_called_with()
# Verify router.get_model_list was called for each model
expected_calls = [
mock.call(model_name="gpt-4"),
mock.call(model_name="gpt-3.5-turbo"),
mock.call(model_name="claude-3"),
mock.call(model_name="gpt-4"), # Called again for team2
]
mock_router.get_model_list.assert_has_calls(expected_calls, any_order=True)
# Test Case 2: user_teams = specific list
mock_litellm_teamtable.reset_mock()
mock_router.reset_mock()
mock_router.get_model_list.side_effect = mock_get_model_list
# Only return team1 for specific team query
mock_litellm_teamtable.find_many.return_value = [mock_team1]
with patch("litellm.proxy.proxy_server.LiteLLM_TeamTable") as mock_team_table_class:
mock_team_table_class.side_effect = mock_team_table_constructor
result = await get_all_team_models(
user_teams=["team1"],
prisma_client=mock_prisma_client,
llm_router=mock_router,
)
# Verify find_many was called with where clause for specific teams
mock_litellm_teamtable.find_many.assert_called_with(
where={"team_id": {"in": ["team1"]}}
)
# Verify router.get_model_list was called only for team1 models
expected_calls = [
mock.call(model_name="gpt-4"),
mock.call(model_name="gpt-3.5-turbo"),
]
mock_router.get_model_list.assert_has_calls(expected_calls, any_order=True)
# Test Case 3: Empty teams list
mock_litellm_teamtable.reset_mock()
mock_router.reset_mock()
mock_litellm_teamtable.find_many.return_value = []
result = await get_all_team_models(
user_teams=[],
prisma_client=mock_prisma_client,
llm_router=mock_router,
)
# Verify find_many was called with empty list
mock_litellm_teamtable.find_many.assert_called_with(where={"team_id": {"in": []}})
# Should return empty list when no teams
assert result == {}
# Test Case 4: Router returns None for some models
mock_litellm_teamtable.reset_mock()
mock_router.reset_mock()
mock_litellm_teamtable.find_many.return_value = [mock_team1]
def mock_get_model_list_with_none(model_name):
if model_name == "gpt-4":
return mock_models_gpt4
# Return None for gpt-3.5-turbo to test None handling
return None
mock_router.get_model_list.side_effect = mock_get_model_list_with_none
with patch("litellm.proxy.proxy_server.LiteLLM_TeamTable") as mock_team_table_class:
mock_team_table_class.side_effect = mock_team_table_constructor
result = await get_all_team_models(
user_teams=["team1"],
prisma_client=mock_prisma_client,
llm_router=mock_router,
)
# Should handle None return gracefully
assert isinstance(result, dict)
print("result: ", result)
assert result == {"gpt-4-model-1": ["team1"], "gpt-4-model-2": ["team1"]}

View file

@ -17,8 +17,16 @@ const TeamDropdown: React.FC<TeamDropdownProps> = ({ teams, value, onChange }) =
onChange={onChange}
filterOption={(input, option) => {
if (!option) return false;
const teamAlias = option.children?.[0]?.props?.children || '';
return teamAlias.toLowerCase().includes(input.toLowerCase());
// Get team data from the option key
const team = teams?.find(t => t.team_id === option.key);
if (!team) return false;
const searchTerm = input.toLowerCase().trim();
const teamAlias = (team.team_alias || '').toLowerCase();
const teamId = (team.team_id || '').toLowerCase();
// Search in both team alias and team ID
return teamAlias.includes(searchTerm) || teamId.includes(searchTerm);
}}
optionFilterProp="children"
>

View file

@ -233,6 +233,10 @@ const CreateKey: React.FC<CreateKeyProps> = ({
fetchPossibleRoles();
}, [accessToken]);
// Check if team selection is required
const isTeamSelectionRequired = modelsToPick.includes('no-default-models');
const isFormDisabled = isTeamSelectionRequired && !selectedCreateKeyTeam;
const handleCreate = async (formValues: Record<string, any>) => {
try {
const newKeyAlias = formValues?.key_alias ?? "";
@ -487,9 +491,19 @@ const CreateKey: React.FC<CreateKeyProps> = ({
</div>
{/* Show message when team selection is required */}
{isFormDisabled && (
<div className="mb-8 p-4 bg-blue-50 border border-blue-200 rounded-md">
<Text className="text-blue-800 text-sm">
Please select a team to continue configuring your API key.
</Text>
</div>
)}
{/* Section 2: Key Details */}
<div className="mb-8">
<Title className="mb-4">Key Details</Title>
{!isFormDisabled && (
<div className="mb-8">
<Title className="mb-4">Key Details</Title>
<Form.Item
label={
<span>
@ -543,10 +557,12 @@ const CreateKey: React.FC<CreateKeyProps> = ({
</Select>
</Form.Item>
</div>
)}
{/* Section 3: Optional Settings */}
<div className="mb-8">
<Accordion className="mt-4 mb-4">
{!isFormDisabled && (
<div className="mb-8">
<Accordion className="mt-4 mb-4">
<AccordionHeader>
<Title className="m-0">Optional Settings</Title>
</AccordionHeader>
@ -815,9 +831,16 @@ const CreateKey: React.FC<CreateKeyProps> = ({
</AccordionBody>
</Accordion>
</div>
)}
<div style={{ textAlign: "right", marginTop: "10px" }}>
<Button2 htmlType="submit">Create Key</Button2>
<Button2
htmlType="submit"
disabled={isFormDisabled}
style={{ opacity: isFormDisabled ? 0.5 : 1 }}
>
Create Key
</Button2>
</div>
</Form>
</Modal>

View file

@ -100,7 +100,7 @@ import {
} from "@heroicons/react/outline";
import DeleteModelButton from "./delete_model_button";
const { Title: Title2, Link } = Typography;
import { UploadOutlined } from "@ant-design/icons";
import { InfoCircleOutlined, UploadOutlined } from "@ant-design/icons";
import type { UploadProps } from "antd";
import { Upload } from "antd";
import TimeToFirstToken from "./model_metrics/time_to_first_token";
@ -260,6 +260,10 @@ const ModelDashboard: React.FC<ModelDashboardProps> = ({
const [selectedTeamFilter, setSelectedTeamFilter] = useState<string | null>(null);
const [selectedModelAccessGroupFilter, setSelectedModelAccessGroupFilter] = useState<string | null>(null);
// Add new state for current team and model view mode
const [currentTeam, setCurrentTeam] = useState<string>('personal'); // 'personal' or team_id
const [modelViewMode, setModelViewMode] = useState<'current_team' | 'all'>('current_team');
const [showColumnDropdown, setShowColumnDropdown] = useState(false);
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
@ -1137,7 +1141,97 @@ const ModelDashboard: React.FC<ModelDashboardProps> = ({
<div className="bg-white rounded-lg shadow">
<div className="border-b px-6 py-4">
<div className="flex flex-col space-y-4">
{/* Search and Filter Controls */}
{/* Current Team Selector - Prominent */}
<div className="flex items-center justify-between bg-gray-50 rounded-lg p-4 border-2 border-gray-200">
<div>
<div className="flex items-center gap-4">
<Text className="text-lg font-semibold text-gray-900">Current Team:</Text>
<Select
className="w-80"
defaultValue="personal"
value={currentTeam}
onValueChange={(value) => setCurrentTeam(value)}
>
<SelectItem value="personal">
<div className="flex items-center gap-2">
<div className="w-2 h-2 bg-blue-500 rounded-full"></div>
<span className="font-medium">Personal</span>
</div>
</SelectItem>
{teams?.filter(team => team.team_id).map((team) => (
<SelectItem
key={team.team_id}
value={team.team_id}
>
<div className="flex items-center gap-2">
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
<span className="font-medium">
{team.team_alias
? `${team.team_alias.slice(0, 30)}...`
: `Team ${team.team_id.slice(0, 30)}...`
}
</span>
</div>
</SelectItem>
))}
</Select>
</div>
{
modelViewMode === "current_team" && (
<div className="flex items-start gap-2 mt-2 bg-gray-50 rounded">
<InfoCircleOutlined className="text-gray-400 mt-0.5 flex-shrink-0 text-xs" />
<div className="text-xs text-gray-500">
{currentTeam === "personal" ? (
<span>
To access these models: Create a Virtual Key without selecting a team on the {" "}
<a href="/?login=success&page=api-keys" className="text-gray-600 hover:text-gray-800 underline">
Virtual Keys page
</a>
</span>
) : (
<span>
To access these models: Create a Virtual Key and select Team as &quot;{currentTeam}&quot; on the {" "}
<a href="/?login=success&page=api-keys" className="text-gray-600 hover:text-gray-800 underline">
Virtual Keys page
</a>
</span>
)}
</div>
</div>
)
}
</div>
{/* Model View Mode Toggle - Also prominent */}
<div className="flex items-center gap-4">
<Text className="text-lg font-semibold text-gray-900">View:</Text>
<Select
className="w-64"
defaultValue="current_team"
value={modelViewMode}
onValueChange={(value) => setModelViewMode(value as 'current_team' | 'all')}
>
<SelectItem value="current_team">
<div className="flex items-center gap-2">
<div className="w-2 h-2 bg-purple-500 rounded-full"></div>
<span className="font-medium">Current Team Models</span>
</div>
</SelectItem>
<SelectItem value="all">
<div className="flex items-center gap-2">
<div className="w-2 h-2 bg-gray-500 rounded-full"></div>
<span className="font-medium">All Available Models</span>
</div>
</SelectItem>
</Select>
</div>
</div>
{/* Other Filters */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
{/* Model Name Filter */}
@ -1161,29 +1255,6 @@ const ModelDashboard: React.FC<ModelDashboardProps> = ({
</Select>
</div>
{/* Team Filter */}
<div className="flex items-center gap-2">
<Text>Filter by Team:</Text>
<Select
className="w-64"
defaultValue="all"
value={selectedTeamFilter ?? "all"}
onValueChange={(value) => setSelectedTeamFilter(value === "all" ? null : value)}
>
<SelectItem value="all">All Teams</SelectItem>
{teams?.filter(team => team.team_id).map((team) => (
<SelectItem
key={team.team_id}
value={team.team_id}
>
{team.team_alias
? `${team.team_alias} (${team.team_id.slice(0, 8)}...)`
: `Team ${team.team_id.slice(0, 8)}...`
}
</SelectItem>
))}
</Select>
</div>
<div className="flex items-center gap-2">
<Text>Filter by Model Access Group:</Text>
<Select
@ -1210,7 +1281,21 @@ const ModelDashboard: React.FC<ModelDashboardProps> = ({
{/* Results Count */}
<div className="flex justify-between items-center">
<Text className="text-sm text-gray-700">
Showing {modelData && modelData.data.length > 0 ? modelData.data.length : 0} results
Showing {modelData && modelData.data.length > 0 ?
modelData.data.filter((model: any) => {
const modelNameMatch = selectedModelGroup === "all" || model.model_name === selectedModelGroup || !selectedModelGroup;
const accessGroupMatch = selectedModelAccessGroupFilter === "all" || model.model_info["access_groups"]?.includes(selectedModelAccessGroupFilter) || !selectedModelAccessGroupFilter;
let teamAccessMatch = true;
if (modelViewMode === 'current_team') {
if (currentTeam === 'personal') {
teamAccessMatch = model.model_info?.direct_access === true;
} else {
teamAccessMatch = model.model_info?.access_via_team_ids?.includes(currentTeam) === true;
}
}
return modelNameMatch && accessGroupMatch && teamAccessMatch;
}).length : 0} results
</Text>
</div>
</div>
@ -1231,11 +1316,28 @@ const ModelDashboard: React.FC<ModelDashboardProps> = ({
setExpandedRows,
)}
data={modelData.data.filter(
(model: any) => (
(selectedModelGroup === "all" || model.model_name === selectedModelGroup || !selectedModelGroup) &&
(selectedTeamFilter === "all" || model.model_info["team_id"] === selectedTeamFilter || !selectedTeamFilter) &&
(selectedModelAccessGroupFilter === "all" || model.model_info["access_groups"]?.includes(selectedModelAccessGroupFilter) || !selectedModelAccessGroupFilter)
)
(model: any) => {
// Model name filter
const modelNameMatch = selectedModelGroup === "all" || model.model_name === selectedModelGroup || !selectedModelGroup;
// Model access group filter
const accessGroupMatch = selectedModelAccessGroupFilter === "all" || model.model_info["access_groups"]?.includes(selectedModelAccessGroupFilter) || !selectedModelAccessGroupFilter;
// Team access filter based on current team and view mode
let teamAccessMatch = true;
if (modelViewMode === 'current_team') {
if (currentTeam === 'personal') {
// Show only models with direct access
teamAccessMatch = model.model_info?.direct_access === true;
} else {
// Show only models accessible by the current team
teamAccessMatch = model.model_info?.access_via_team_ids?.includes(currentTeam) === true;
}
}
// For 'all' mode, show all models (teamAccessMatch remains true)
return modelNameMatch && accessGroupMatch && teamAccessMatch;
}
)}
isLoading={false}
table={tableRef}

View file

@ -1611,11 +1611,12 @@ export const modelInfoCall = async (
try {
console.log("modelInfoCall:", accessToken, userID, userRole);
let url = proxyBaseUrl ? `${proxyBaseUrl}/v2/model/info` : `/v2/model/info`;
if (!all_admin_roles.includes(userRole as string)) {
// only show users models they've added
url += `?user_models_only=true`;
const params = new URLSearchParams();
params.append("include_team_models", "true");
if (params.toString()) {
url += `?${params.toString()}`;
}
//message.info("Requesting model data");
const response = await fetch(url, {
method: "GET",