From 47ff0d1d863426a3390dfb30bd871b98bf28b9a1 Mon Sep 17 00:00:00 2001 From: Alejandro Tapia Date: Tue, 3 Mar 2026 16:03:25 -0800 Subject: [PATCH] Fixed: 50 sequential API calls on mount is excessive --- .../key_team_helpers/filter_helpers.ts | 109 +++++++++++------- 1 file changed, 68 insertions(+), 41 deletions(-) diff --git a/ui/litellm-dashboard/src/components/key_team_helpers/filter_helpers.ts b/ui/litellm-dashboard/src/components/key_team_helpers/filter_helpers.ts index a1602fc3aea..539ac172569 100644 --- a/ui/litellm-dashboard/src/components/key_team_helpers/filter_helpers.ts +++ b/ui/litellm-dashboard/src/components/key_team_helpers/filter_helpers.ts @@ -9,12 +9,35 @@ export interface TeamFilterOptions { } const FILTER_OPTIONS_PAGE_SIZE = 100; // API max per page -const MAX_PAGES = 50; // Cap at 5000 keys to avoid unbounded fetches +const MAX_PAGES = 10; // Cap at 1000 keys; filter completeness beyond ~500 has diminishing returns + +const processKeysIntoOptions = ( + keys: Array>, + keyAliases: Set, + organizationIds: Set, + userMap: Map, +) => { + for (const key of keys) { + const alias = key?.key_alias; + if (alias && typeof alias === "string") { + keyAliases.add(alias.trim()); + } + const orgId = key?.organization_id; + if (orgId && typeof orgId === "string") { + organizationIds.add(orgId.trim()); + } + const userId = key?.user_id; + if (userId && typeof userId === "string") { + const email = (key?.user as { user_email?: string })?.user_email || userId; + userMap.set(userId, email); + } + } +}; /** - * Fetches filter options (key aliases, org IDs, user IDs) from all team keys. - * Paginates through pages to build complete dropdowns. Capped at 50 pages - * (5000 keys) to limit load for very large teams. + * Fetches filter options (key aliases, org IDs, user IDs) from team keys. + * Fetches page 1 first to get totalPages, then batches remaining pages with + * Promise.all. Capped at 10 pages (1000 keys) */ export const fetchTeamFilterOptions = async ( accessToken: string | null, @@ -29,46 +52,50 @@ export const fetchTeamFilterOptions = async ( const organizationIds = new Set(); const userMap = new Map(); - let page = 1; - let totalPages = 1; + // First request: get page 1 and totalPages + const firstResponse = await keyListCall( + accessToken, + null, + teamId, + null, + null, + null, + 1, + FILTER_OPTIONS_PAGE_SIZE, + null, + null, + "user", + null, + ); - do { - const response = await keyListCall( - accessToken, - null, - teamId, - null, - null, - null, - page, - FILTER_OPTIONS_PAGE_SIZE, - null, - null, - "user", - null, + const firstKeys = firstResponse?.keys || []; + const totalPages = firstResponse?.total_pages ?? 1; + processKeysIntoOptions(firstKeys, keyAliases, organizationIds, userMap); + + // Batch fetch remaining pages (2 through min(totalPages, MAX_PAGES)) in parallel + const pagesToFetch = Math.min(totalPages, MAX_PAGES) - 1; + if (pagesToFetch > 0) { + const pagePromises = Array.from({ length: pagesToFetch }, (_, i) => + keyListCall( + accessToken, + null, + teamId, + null, + null, + null, + i + 2, + FILTER_OPTIONS_PAGE_SIZE, + null, + null, + "user", + null, + ), ); - - const keys = response?.keys || []; - totalPages = response?.total_pages ?? 1; - - for (const key of keys) { - const alias = key?.key_alias; - if (alias && typeof alias === "string") { - keyAliases.add(alias.trim()); - } - const orgId = key?.organization_id; - if (orgId && typeof orgId === "string") { - organizationIds.add(orgId.trim()); - } - const userId = key?.user_id; - if (userId && typeof userId === "string") { - const email = key?.user?.user_email || userId; - userMap.set(userId, email); - } + const responses = await Promise.all(pagePromises); + for (const response of responses) { + processKeysIntoOptions(response?.keys || [], keyAliases, organizationIds, userMap); } - - page++; - } while (page <= totalPages && page <= MAX_PAGES); + } return { keyAliases: Array.from(keyAliases).sort(),