diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index 8faf36df4c..6b581957e9 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -3805,6 +3805,10 @@ class OrganizationMemberUpdateResponse(MemberUpdateResponse): class TeamInfoResponseObjectTeamTable(LiteLLM_TeamTable): team_member_budget_table: Optional[LiteLLM_BudgetTable] = None + # Resources inherited from access groups (separate from direct assignments) + access_group_models: Optional[List[str]] = None + access_group_mcp_server_ids: Optional[List[str]] = None + access_group_agent_ids: Optional[List[str]] = None class TeamInfoResponseObject(TypedDict): diff --git a/litellm/proxy/management_endpoints/team_endpoints.py b/litellm/proxy/management_endpoints/team_endpoints.py index 3643373be6..6b4cf72fc1 100644 --- a/litellm/proxy/management_endpoints/team_endpoints.py +++ b/litellm/proxy/management_endpoints/team_endpoints.py @@ -62,6 +62,9 @@ from litellm.proxy._types import ( UserAPIKeyAuth, ) from litellm.proxy.auth.auth_checks import ( + _get_agent_ids_from_access_groups, + _get_mcp_server_ids_from_access_groups, + _get_models_from_access_groups, allowed_route_check_inside_route, can_org_access_model, get_org_object, @@ -3042,6 +3045,14 @@ async def team_info( team_info_response_object=_team_info, ) + # Resolve resources inherited from access groups + resolved = await _resolve_access_group_resources( + access_group_ids=_team_info.access_group_ids, + ) + _team_info.access_group_models = resolved["access_group_models"] + _team_info.access_group_mcp_server_ids = resolved["access_group_mcp_server_ids"] + _team_info.access_group_agent_ids = resolved["access_group_agent_ids"] + response_object = TeamInfoResponseObject( team_id=team_id, team_info=_team_info, @@ -3332,6 +3343,36 @@ async def _build_team_list_where_conditions( return where_conditions +async def _resolve_access_group_resources( + access_group_ids: Optional[List[str]], +) -> Dict[str, List[str]]: + """ + Resolve resources inherited from access groups. + + Returns only the access-group-sourced resources (not direct assignments). + Keeps them separate so callers can distinguish where each resource comes from. + """ + empty: Dict[str, List[str]] = { + "access_group_models": [], + "access_group_mcp_server_ids": [], + "access_group_agent_ids": [], + } + if not access_group_ids: + return empty + + return { + "access_group_models": await _get_models_from_access_groups( + access_group_ids=access_group_ids, + ), + "access_group_mcp_server_ids": await _get_mcp_server_ids_from_access_groups( + access_group_ids=access_group_ids, + ), + "access_group_agent_ids": await _get_agent_ids_from_access_groups( + access_group_ids=access_group_ids, + ), + } + + def _convert_teams_to_response_models( teams: list, use_deleted_table: bool, @@ -3558,6 +3599,19 @@ async def list_team_v2( # Convert Prisma models to response models with members_count team_list = _convert_teams_to_response_models(teams, use_deleted_table) + # Resolve resources inherited from access groups for each team + if not use_deleted_table: + for team_item in team_list: + if isinstance(team_item, TeamListItem): + resolved = await _resolve_access_group_resources( + access_group_ids=team_item.access_group_ids, + ) + team_item.access_group_models = resolved["access_group_models"] + team_item.access_group_mcp_server_ids = resolved[ + "access_group_mcp_server_ids" + ] + team_item.access_group_agent_ids = resolved["access_group_agent_ids"] + return { "teams": team_list, "total": total_count, diff --git a/litellm/types/proxy/management_endpoints/team_endpoints.py b/litellm/types/proxy/management_endpoints/team_endpoints.py index 5055a65783..2455eb495d 100644 --- a/litellm/types/proxy/management_endpoints/team_endpoints.py +++ b/litellm/types/proxy/management_endpoints/team_endpoints.py @@ -47,6 +47,10 @@ class TeamListItem(LiteLLM_TeamTable): """A team item in the paginated list response, enriched with computed fields.""" members_count: int = 0 + # Resources inherited from access groups (separate from direct assignments) + access_group_models: Optional[List[str]] = None + access_group_mcp_server_ids: Optional[List[str]] = None + access_group_agent_ids: Optional[List[str]] = None class TeamListResponse(BaseModel): diff --git a/ui/litellm-dashboard/src/app/(dashboard)/teams/components/TeamsTable/ModelsCell.tsx b/ui/litellm-dashboard/src/app/(dashboard)/teams/components/TeamsTable/ModelsCell.tsx index 5cabe4c4a8..03a0a80bc7 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/teams/components/TeamsTable/ModelsCell.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/teams/components/TeamsTable/ModelsCell.tsx @@ -1,16 +1,54 @@ import { Badge, Icon, TableCell, Text } from "@tremor/react"; import { ChevronDownIcon, ChevronRightIcon } from "@heroicons/react/outline"; import { getModelDisplayName } from "@/components/key_team_helpers/fetch_available_models_team_key"; -import React, { useState } from "react"; +import React, { useMemo, useState } from "react"; import { Team } from "@/components/key_team_helpers/key_list"; interface ModelsCellProps { team: Team; } +interface ModelEntry { + name: string; + source: "direct" | "access_group"; +} + const ModelsCell = ({ team }: ModelsCellProps) => { const [expandedAccordion, setExpandedAccordion] = useState(false); + const modelEntries: ModelEntry[] = useMemo(() => { + const entries: ModelEntry[] = (team.models || []).map((m) => ({ + name: m, + source: "direct" as const, + })); + for (const m of team.access_group_models || []) { + entries.push({ name: m, source: "access_group" }); + } + return entries; + }, [team.models, team.access_group_models]); + + const renderBadge = (entry: ModelEntry, index: number) => { + if (entry.name === "all-proxy-models") { + return ( + + All Proxy Models + + ); + } + const displayName = getModelDisplayName(entry.name); + const truncated = displayName.length > 30 ? `${displayName.slice(0, 30)}...` : displayName; + return ( + + {truncated} + + ); + }; + return ( { whiteSpace: "pre-wrap", overflow: "hidden", }} - className={team.models.length > 3 ? "px-0" : ""} + className={modelEntries.length > 3 ? "px-0" : ""} >
- {Array.isArray(team.models) ? ( + {modelEntries.length === 0 ? ( + + All Proxy Models + + ) : (
- {team.models.length === 0 ? ( - - All Proxy Models - - ) : ( - <> -
- {team.models.length > 3 && ( -
- { - setExpandedAccordion((prev) => !prev); - }} - /> -
- )} -
- {team.models.slice(0, 3).map((model: string, index: number) => - model === "all-proxy-models" ? ( - - All Proxy Models - - ) : ( - - - {model.length > 30 - ? `${getModelDisplayName(model).slice(0, 30)}...` - : getModelDisplayName(model)} - - - ), - )} - {team.models.length > 3 && !expandedAccordion && ( - - - +{team.models.length - 3} {team.models.length - 3 === 1 ? "more model" : "more models"} - - - )} - {expandedAccordion && ( -
- {team.models.slice(3).map((model: string, index: number) => - model === "all-proxy-models" ? ( - - All Proxy Models - - ) : ( - - - {model.length > 30 - ? `${getModelDisplayName(model).slice(0, 30)}...` - : getModelDisplayName(model)} - - - ), - )} -
- )} -
+
+ {modelEntries.length > 3 && ( +
+ { + setExpandedAccordion((prev) => !prev); + }} + />
- - )} + )} +
+ {modelEntries.slice(0, 3).map((entry, index) => renderBadge(entry, index))} + {modelEntries.length > 3 && !expandedAccordion && ( + + + +{modelEntries.length - 3} {modelEntries.length - 3 === 1 ? "more model" : "more models"} + + + )} + {expandedAccordion && ( +
+ {modelEntries.slice(3).map((entry, index) => renderBadge(entry, index + 3))} +
+ )} +
+
- ) : null} + )}
); diff --git a/ui/litellm-dashboard/src/components/key_team_helpers/key_list.tsx b/ui/litellm-dashboard/src/components/key_team_helpers/key_list.tsx index a681e438cd..04b9a5c996 100644 --- a/ui/litellm-dashboard/src/components/key_team_helpers/key_list.tsx +++ b/ui/litellm-dashboard/src/components/key_team_helpers/key_list.tsx @@ -15,6 +15,10 @@ export interface Team { keys: KeyResponse[]; members_with_roles: Member[]; spend: number; + access_group_ids?: string[]; + access_group_models?: string[]; + access_group_mcp_server_ids?: string[]; + access_group_agent_ids?: string[]; } export interface KeyResponse { diff --git a/ui/litellm-dashboard/src/components/team/TeamInfo.tsx b/ui/litellm-dashboard/src/components/team/TeamInfo.tsx index a4c7ae2bbb..79a55cf30f 100644 --- a/ui/litellm-dashboard/src/components/team/TeamInfo.tsx +++ b/ui/litellm-dashboard/src/components/team/TeamInfo.tsx @@ -655,16 +655,31 @@ const TeamInfoView: React.FC = ({ Models
- {info.models.length === 0 ? ( + {info.models.length === 0 && !(info.access_group_models?.length) ? ( All proxy models ) : ( - info.models.map((model, index) => ( - - {model} - - )) + <> + {info.models.map((model: string, index: number) => ( + + {model} + + ))} + {(info.access_group_models || []).map((model: string, index: number) => ( + + {model} + + ))} + )}
+ {info.access_group_models && info.access_group_models.length > 0 && ( +
+ + Direct + From access group + +
+ )}