diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index a81f4f6c2a..63c1a99cb1 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -331,6 +331,7 @@ class LiteLLMRoutes(enum.Enum): "/key/health", "/team/info", "/team/list", + "/v2/team/list", "/organization/list", "/team/available", "/user/info", @@ -370,6 +371,7 @@ class LiteLLMRoutes(enum.Enum): "/team/update", "/team/delete", "/team/list", + "/v2/team/list", "/team/info", "/team/block", "/team/unblock", diff --git a/litellm/proxy/management_endpoints/team_endpoints.py b/litellm/proxy/management_endpoints/team_endpoints.py index cd4ae97ac7..81b2072eb4 100644 --- a/litellm/proxy/management_endpoints/team_endpoints.py +++ b/litellm/proxy/management_endpoints/team_endpoints.py @@ -14,7 +14,7 @@ import json import traceback import uuid from datetime import datetime, timedelta, timezone -from typing import List, Optional, Tuple, Union, cast +from typing import Any, Dict, List, Optional, Tuple, Union, cast import fastapi from fastapi import APIRouter, Depends, Header, HTTPException, Request, status @@ -85,6 +85,7 @@ from litellm.types.proxy.management_endpoints.common_daily_activity import ( ) from litellm.types.proxy.management_endpoints.team_endpoints import ( GetTeamMemberPermissionsResponse, + TeamListResponse, UpdateTeamMemberPermissionsRequest, ) @@ -1553,6 +1554,150 @@ async def list_available_teams( return available_teams_correct_type +@router.get( + "/v2/team/list", + tags=["team management"], + response_model=TeamListResponse, + dependencies=[Depends(user_api_key_auth)], +) +async def list_team_v2( + http_request: Request, + user_id: Optional[str] = fastapi.Query( + default=None, description="Only return teams which this 'user_id' belongs to" + ), + organization_id: Optional[str] = fastapi.Query( + default=None, + description="Only return teams which this 'organization_id' belongs to", + ), + team_id: Optional[str] = fastapi.Query( + default=None, description="Only return teams which this 'team_id' belongs to" + ), + team_alias: Optional[str] = fastapi.Query( + default=None, + description="Only return teams which this 'team_alias' belongs to. Supports partial matching.", + ), + page: int = fastapi.Query( + default=1, description="Page number for pagination", ge=1 + ), + page_size: int = fastapi.Query( + default=10, description="Number of teams per page", ge=1, le=100 + ), + sort_by: Optional[str] = fastapi.Query( + default=None, + description="Column to sort by (e.g. 'team_id', 'team_alias', 'created_at')", + ), + sort_order: str = fastapi.Query( + default="asc", description="Sort order ('asc' or 'desc')" + ), + user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), +): + """ + Get a paginated list of teams with filtering and sorting options. + + Parameters: + user_id: Optional[str] + Only return teams which this user belongs to + organization_id: Optional[str] + Only return teams which belong to this organization + team_id: Optional[str] + Filter teams by exact team_id match + team_alias: Optional[str] + Filter teams by partial team_alias match + page: int + The page number to return + page_size: int + The number of items per page + sort_by: Optional[str] + Column to sort by (e.g. 'team_id', 'team_alias', 'created_at') + sort_order: str + Sort order ('asc' or 'desc') + """ + from litellm.proxy.proxy_server import prisma_client + + if prisma_client is None: + raise HTTPException( + status_code=500, + detail={"error": f"No db connected. prisma client={prisma_client}"}, + ) + + if user_id is None and user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN: + user_id = user_api_key_dict.user_id + + # Calculate skip and take for pagination + skip = (page - 1) * page_size + + # Build where conditions based on provided parameters + where_conditions: Dict[str, Any] = {} + + if team_id: + where_conditions["team_id"] = team_id + + if team_alias: + where_conditions["team_alias"] = { + "contains": team_alias, + "mode": "insensitive", # Case-insensitive search + } + + if organization_id: + where_conditions["organization_id"] = organization_id + + if user_id: + try: + user_object = await prisma_client.db.litellm_usertable.find_unique( + where={"user_id": user_id} + ) + except Exception: + raise HTTPException( + status_code=404, + detail={"error": f"User not found, passed user_id={user_id}"}, + ) + if user_object is None: + raise HTTPException( + status_code=404, + detail={"error": f"User not found, passed user_id={user_id}"}, + ) + user_object_correct_type = LiteLLM_UserTable(**user_object.model_dump()) + # Find teams where this user is a member by checking members_with_roles array + if team_id is None: + where_conditions["team_id"] = {"in": user_object_correct_type.teams} + elif team_id in user_object_correct_type.teams: + where_conditions["team_id"] = team_id + else: + raise HTTPException( + status_code=404, + detail={"error": f"User is not a member of team_id={team_id}"}, + ) + + # Build order_by conditions + valid_sort_columns = ["team_id", "team_alias", "created_at"] + order_by = None + if sort_by and sort_by in valid_sort_columns: + if sort_order.lower() not in ["asc", "desc"]: + sort_order = "asc" + order_by = {sort_by: sort_order.lower()} + + # Get teams with pagination + teams = await prisma_client.db.litellm_teamtable.find_many( + where=where_conditions, + skip=skip, + take=page_size, + order=order_by if order_by else {"created_at": "desc"}, # Default sort + ) + # Get total count for pagination + total_count = await prisma_client.db.litellm_teamtable.count(where=where_conditions) + + # Calculate total pages + total_pages = -(-total_count // page_size) # Ceiling division + + return { + "teams": [team.model_dump() for team in teams] if teams else [], + "total": total_count, + "page": page, + "page_size": page_size, + "total_pages": total_pages, + } + + @router.get( "/team/list", tags=["team management"], dependencies=[Depends(user_api_key_auth)] ) diff --git a/litellm/types/proxy/management_endpoints/team_endpoints.py b/litellm/types/proxy/management_endpoints/team_endpoints.py index faa3d383dc..ebeadd47c3 100644 --- a/litellm/types/proxy/management_endpoints/team_endpoints.py +++ b/litellm/types/proxy/management_endpoints/team_endpoints.py @@ -2,6 +2,8 @@ from typing import List, Optional from pydantic import BaseModel +from litellm.proxy._types import LiteLLM_TeamTable + class GetTeamMemberPermissionsRequest(BaseModel): """Request to get the team member permissions for a team""" @@ -33,3 +35,13 @@ class UpdateTeamMemberPermissionsRequest(BaseModel): team_id: str team_member_permissions: List[str] + + +class TeamListResponse(BaseModel): + """Response to get the list of teams""" + + teams: List[LiteLLM_TeamTable] + total: int + page: int + page_size: int + total_pages: int diff --git a/ui/litellm-dashboard/src/components/networking.tsx b/ui/litellm-dashboard/src/components/networking.tsx index 63a4d6f101..6a343293ad 100644 --- a/ui/litellm-dashboard/src/components/networking.tsx +++ b/ui/litellm-dashboard/src/components/networking.tsx @@ -848,10 +848,85 @@ export const teamInfoCall = async ( } }; +type TeamListResponse = { + teams: Team[]; + total: number; + page: number; + page_size: number; + total_pages: number; +}; + +export const v2TeamListCall = async ( + accessToken: String, + organizationID: string | null, + userID: String | null = null, + teamID: string | null = null, + team_alias: string | null = null, + page: number = 1, + page_size: number = 10, + sort_by: string | null = null, + sort_order: 'asc' | 'desc' | null = null, +): Promise => { + /** + * Get list of teams with filtering and sorting options + */ + try { + let url = proxyBaseUrl ? `${proxyBaseUrl}/v2/team/list` : `/v2/team/list`; + console.log("in teamInfoCall"); + const queryParams = new URLSearchParams(); + + if (userID) { + queryParams.append('user_id', userID.toString()); + } + + if (organizationID) { + queryParams.append('organization_id', organizationID.toString()); + } + + if (teamID) { + queryParams.append('team_id', teamID.toString()); + } + + if (team_alias) { + queryParams.append('team_alias', team_alias.toString()); + } + + const queryString = queryParams.toString(); + if (queryString) { + url += `?${queryString}`; + } + + const response = await fetch(url, { + method: "GET", + headers: { + [globalLitellmHeaderName]: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + const errorData = await response.text(); + handleError(errorData); + throw new Error("Network response was not ok"); + } + + const data = await response.json(); + console.log("/v2/team/list API Response:", data); + return data; + // Handle success - you might want to update some state or UI based on the created key + } catch (error) { + console.error("Failed to create key:", error); + throw error; + } +}; + + export const teamListCall = async ( accessToken: String, organizationID: string | null, userID: String | null = null, + teamID: string | null = null, + team_alias: string | null = null, ) => { /** * Get all available teams on proxy @@ -868,6 +943,14 @@ export const teamListCall = async ( if (organizationID) { queryParams.append('organization_id', organizationID.toString()); } + + if (teamID) { + queryParams.append('team_id', teamID.toString()); + } + + if (team_alias) { + queryParams.append('team_alias', team_alias.toString()); + } const queryString = queryParams.toString(); if (queryString) { diff --git a/ui/litellm-dashboard/src/components/teams.tsx b/ui/litellm-dashboard/src/components/teams.tsx index 15ae9da377..3abbaee48a 100644 --- a/ui/litellm-dashboard/src/components/teams.tsx +++ b/ui/litellm-dashboard/src/components/teams.tsx @@ -71,6 +71,14 @@ interface TeamProps { organizations: Organization[] | null; } +interface FilterState { + team_id: string; + team_alias: string; + organization_id: string; + sort_by: string; + sort_order: 'asc' | 'desc'; +} + interface EditTeamModalProps { visible: boolean; onCancel: () => void; @@ -84,7 +92,7 @@ import { teamMemberUpdateCall, Member, modelAvailableCall, - teamListCall + v2TeamListCall } from "./networking"; import { updateExistingKeys } from "@/utils/dataUtils"; @@ -119,6 +127,14 @@ const Teams: React.FC = ({ const [lastRefreshed, setLastRefreshed] = useState(""); const [currentOrg, setCurrentOrg] = useState(null); const [currentOrgForCreateTeam, setCurrentOrgForCreateTeam] = useState(null); + const [showFilters, setShowFilters] = useState(false); + const [filters, setFilters] = useState({ + team_id: "", + team_alias: "", + organization_id: "", + sort_by: "created_at", + sort_order: "desc" + }); useEffect(() => { console.log(`inside useeffect - ${lastRefreshed}`) @@ -318,6 +334,71 @@ const Teams: React.FC = ({ setLastRefreshed(currentDate.toLocaleString()); }; + const handleFilterChange = (key: keyof FilterState, value: string) => { + const newFilters = { ...filters, [key]: value }; + setFilters(newFilters); + // Call teamListCall with the new filters + if (accessToken) { + v2TeamListCall( + accessToken, + newFilters.organization_id || null, + null, + newFilters.team_id || null, + newFilters.team_alias || null + ).then((response) => { + if (response && response.teams) { + setTeams(response.teams); + } + }).catch((error) => { + console.error("Error fetching teams:", error); + }); + } + }; + + const handleSortChange = (sortBy: string, sortOrder: 'asc' | 'desc') => { + const newFilters = { + ...filters, + sort_by: sortBy, + sort_order: sortOrder + }; + setFilters(newFilters); + // Call teamListCall with the new sort parameters + if (accessToken) { + v2TeamListCall( + accessToken, + filters.organization_id || null, + null, + filters.team_id || null, + filters.team_alias || null + ).then((response) => { + if (response && response.teams) { + setTeams(response.teams); + } + }).catch((error) => { + console.error("Error fetching teams:", error); + }); + } + }; + + const handleFilterReset = () => { + setFilters({ + team_id: "", + team_alias: "", + organization_id: "", + sort_by: "created_at", + sort_order: "desc" + }); + // Reset teams list + if (accessToken) { + v2TeamListCall(accessToken, null, userID || null, null, null).then((response) => { + if (response && response.teams) { + setTeams(response.teams); + } + }).catch((error) => { + console.error("Error fetching teams:", error); + }); + } + }; return (
@@ -377,6 +458,125 @@ const Teams: React.FC = ({ +
+
+ {/* Search and Filter Controls */} +
+ {/* Team Alias Search */} +
+ handleFilterChange('team_alias', e.target.value)} + /> + + + +
+ + {/* Filter Button */} + + + {/* Reset Filters Button */} + +
+ + {/* Additional Filters */} + {showFilters && ( +
+ {/* Team ID Search */} +
+ handleFilterChange('team_id', e.target.value)} + /> + + + +
+ + {/* Organization Dropdown */} +
+ +
+
+ )} +
+