Skip to content

UI (Teams Page) - Support filtering by team id + team name #10324

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Apr 26, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 17 additions & 7 deletions litellm/model_prices_and_context_window_backup.json
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,8 @@
"supports_vision": true,
"supports_prompt_caching": true,
"supports_system_messages": true,
"supports_tool_choice": true
"supports_tool_choice": true,
"deprecation_date": "2025-07-14"
},
"gpt-4o-audio-preview": {
"max_tokens": 16384,
Expand Down Expand Up @@ -1509,6 +1510,8 @@
},
"gpt-4o-transcribe": {
"mode": "audio_transcription",
"max_input_tokens": 16000,
"max_output_tokens": 2000,
"input_cost_per_token": 0.0000025,
"input_cost_per_audio_token": 0.000006,
"output_cost_per_token": 0.00001,
Expand All @@ -1517,6 +1520,8 @@
},
"gpt-4o-mini-transcribe": {
"mode": "audio_transcription",
"max_input_tokens": 16000,
"max_output_tokens": 2000,
"input_cost_per_token": 0.00000125,
"input_cost_per_audio_token": 0.000003,
"output_cost_per_token": 0.000005,
Expand Down Expand Up @@ -2439,7 +2444,8 @@
"supports_response_schema": true,
"supports_vision": true,
"supports_prompt_caching": true,
"supports_tool_choice": true
"supports_tool_choice": true,
"deprecation_date": "2025-08-20"
},
"azure/us/gpt-4o-2024-08-06": {
"max_tokens": 16384,
Expand Down Expand Up @@ -2479,13 +2485,15 @@
"max_output_tokens": 16384,
"input_cost_per_token": 0.0000025,
"output_cost_per_token": 0.000010,
"cache_read_input_token_cost": 0.00000125,
"litellm_provider": "azure",
"mode": "chat",
"supports_function_calling": true,
"supports_parallel_function_calling": true,
"supports_response_schema": true,
"supports_vision": true,
"supports_tool_choice": true
"supports_tool_choice": true,
"deprecation_date": "2025-12-20"
},
"azure/global-standard/gpt-4o-mini": {
"max_tokens": 16384,
Expand Down Expand Up @@ -5349,14 +5357,14 @@
"input_cost_per_image": 0,
"input_cost_per_video_per_second": 0,
"input_cost_per_audio_per_second": 0,
"input_cost_per_token": 0,
"input_cost_per_token": 0.00000015,
"input_cost_per_character": 0,
"input_cost_per_token_above_128k_tokens": 0,
"input_cost_per_character_above_128k_tokens": 0,
"input_cost_per_image_above_128k_tokens": 0,
"input_cost_per_video_per_second_above_128k_tokens": 0,
"input_cost_per_audio_per_second_above_128k_tokens": 0,
"output_cost_per_token": 0,
"output_cost_per_token": 0.0000006,
"output_cost_per_character": 0,
"output_cost_per_token_above_128k_tokens": 0,
"output_cost_per_character_above_128k_tokens": 0,
Expand Down Expand Up @@ -5395,7 +5403,8 @@
"supports_tool_choice": true,
"supported_modalities": ["text", "image", "audio", "video"],
"supported_output_modalities": ["text", "image"],
"source": "https://cloud.google.com/vertex-ai/generative-ai/pricing"
"source": "https://cloud.google.com/vertex-ai/generative-ai/pricing",
"deprecation_date": "2026-02-05"
},
"gemini-2.0-flash-thinking-exp": {
"max_tokens": 8192,
Expand Down Expand Up @@ -5599,7 +5608,8 @@
"supported_modalities": ["text", "image", "audio", "video"],
"supported_output_modalities": ["text"],
"source": "https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#gemini-2.0-flash",
"supports_tool_choice": true
"supports_tool_choice": true,
"deprecation_date": "2026-02-25"
},
"gemini-2.5-pro-preview-03-25": {
"max_tokens": 65536,
Expand Down
2 changes: 2 additions & 0 deletions litellm/proxy/_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,7 @@ class LiteLLMRoutes(enum.Enum):
"/key/health",
"/team/info",
"/team/list",
"/v2/team/list",
"/organization/list",
"/team/available",
"/user/info",
Expand Down Expand Up @@ -370,6 +371,7 @@ class LiteLLMRoutes(enum.Enum):
"/team/update",
"/team/delete",
"/team/list",
"/v2/team/list",
"/team/info",
"/team/block",
"/team/unblock",
Expand Down
147 changes: 146 additions & 1 deletion litellm/proxy/management_endpoints/team_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
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
Expand Down Expand Up @@ -85,6 +85,7 @@
)
from litellm.types.proxy.management_endpoints.team_endpoints import (
GetTeamMemberPermissionsResponse,
TeamListResponse,
UpdateTeamMemberPermissionsRequest,
)

Expand Down Expand Up @@ -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)]
)
Expand Down
12 changes: 12 additions & 0 deletions litellm/types/proxy/management_endpoints/team_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

from pydantic import BaseModel

from litellm.proxy._types import LiteLLM_TeamTable


class GetTeamMemberPermissionsRequest(BaseModel):
"""Request to get the team member permissions for a team"""
Expand Down Expand Up @@ -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
83 changes: 83 additions & 0 deletions ui/litellm-dashboard/src/components/networking.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<TeamListResponse> => {
/**
* 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
Expand All @@ -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) {
Expand Down
Loading
Loading