From 32f31889a9a2f79b62bf3bfef8e7dbc642d5b251 Mon Sep 17 00:00:00 2001 From: Lucas Song Date: Fri, 17 Apr 2026 22:17:12 -0700 Subject: [PATCH] =?UTF-8?q?fix(proxy-ui):=20PR=20review=20=E2=80=94=20key?= =?UTF-8?q?=20models=20API=20clarity=20and=20RQ=20cache=20keys?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - model_checks: simplify empty_models; rename resolved_total_count to resolved_config_entry_count; document pattern vs matched counts - key_management: OpenAPI doc bullets for new response fields - useGetKeyModels: include userId ?? accessToken in query keys - networking: drop no-op try/catch in fetchKeyModelCall; update KeyModelResponse - Tests: mocks/assertions for renamed field Made-with: Cursor --- litellm/proxy/auth/model_checks.py | 8 ++- .../key_management_endpoints.py | 2 + .../auth/test_model_checks_key_display.py | 2 +- .../key_team_helpers/KeyModelList.test.tsx | 12 ++-- .../src/components/networking.test.ts | 2 +- .../src/components/networking.tsx | 61 +++++++++---------- .../templates/key_info_view.test.tsx | 2 +- .../src/hooks/keys/useGetKeyModels.test.tsx | 8 ++- .../src/hooks/keys/useGetKeyModels.ts | 7 ++- 9 files changed, 55 insertions(+), 49 deletions(-) diff --git a/litellm/proxy/auth/model_checks.py b/litellm/proxy/auth/model_checks.py index 9f1c91faae7..70c1bc572bf 100644 --- a/litellm/proxy/auth/model_checks.py +++ b/litellm/proxy/auth/model_checks.py @@ -534,7 +534,8 @@ def build_key_resolved_model_display_sections( only to avoid repeating the full flat list as "Other models". """ sections: List[KeyResolvedModelDisplaySection] = [] - empty_models: List[str] = [] if compact else [] + # Always [] — compact still toggles each section's `models=` via `empty_models if compact else ...` below. + empty_models: List[str] = [] display_set = set(display_models) @@ -603,6 +604,9 @@ def prepare_key_models_response_payload( Access-group sections list concrete model names: wildcards in router group metadata are expanded against models allowed for this key (resolved ∩ router names). + + ``resolved_config_entry_count`` is len(resolved) (pattern/sentinel entries); ``matched_count`` is + the number of concrete router names after expansion and search; they differ when wildcards are used. """ base_concrete = _concrete_models_allowed_by_resolved(resolved, all_router_model_names) @@ -638,7 +642,7 @@ def prepare_key_models_response_payload( return { "model_display_sections": model_display_sections, "source": source, - "resolved_total_count": len(resolved), + "resolved_config_entry_count": len(resolved), "matched_count": matched_count, "models_truncated": models_truncated, "all_team_models_without_team": all_team_models_without_team, diff --git a/litellm/proxy/management_endpoints/key_management_endpoints.py b/litellm/proxy/management_endpoints/key_management_endpoints.py index e1e2e834115..2a6482046d0 100644 --- a/litellm/proxy/management_endpoints/key_management_endpoints.py +++ b/litellm/proxy/management_endpoints/key_management_endpoints.py @@ -2828,6 +2828,8 @@ async def key_resolved_models_fn( - **search**: Filters the resolved list before truncation and sectioning. - **compact**: Metadata-only payload (empty `models` arrays in each section) for fast initial load. - **all_team_models_without_team**: True when the key uses `all-team-models` but has no `team_id` (assign a team in key settings). + - **resolved_config_entry_count**: Length of the post-sentinel-resolution pattern list (not the same as concrete `matched_count` when wildcards are present). + - **matched_count**: Number of concrete router model names after expansion and optional search (before display truncation). """ from litellm.proxy.proxy_server import llm_router, prisma_client diff --git a/tests/test_litellm/proxy/auth/test_model_checks_key_display.py b/tests/test_litellm/proxy/auth/test_model_checks_key_display.py index ca9ff3843bc..57946182f3b 100644 --- a/tests/test_litellm/proxy/auth/test_model_checks_key_display.py +++ b/tests/test_litellm/proxy/auth/test_model_checks_key_display.py @@ -254,7 +254,7 @@ def test_prepare_payload_search_and_truncation(): compact=False, all_router_model_names=big, ) - assert out["resolved_total_count"] == len(big) + assert out["resolved_config_entry_count"] == len(big) assert out["matched_count"] == len(big) assert out["models_truncated"] is True ung = out["model_display_sections"][0] diff --git a/ui/litellm-dashboard/src/components/key_team_helpers/KeyModelList.test.tsx b/ui/litellm-dashboard/src/components/key_team_helpers/KeyModelList.test.tsx index 908ff04c800..227e246424a 100644 --- a/ui/litellm-dashboard/src/components/key_team_helpers/KeyModelList.test.tsx +++ b/ui/litellm-dashboard/src/components/key_team_helpers/KeyModelList.test.tsx @@ -27,7 +27,7 @@ describe("KeyModelList", () => { { title: "grp-a", section_kind: "access_group", models: ["m1"] }, ], source: "all-proxy-models", - resolved_total_count: 2, + resolved_config_entry_count: 2, matched_count: 2, models_truncated: false, all_team_models_without_team: false, @@ -62,7 +62,7 @@ describe("KeyModelList", () => { { title: "All team models", section_kind: "all_team_models", models: ["a"] }, ], source: "all-team-models", - resolved_total_count: 1, + resolved_config_entry_count: 1, matched_count: 1, models_truncated: false, all_team_models_without_team: true, @@ -92,7 +92,7 @@ describe("KeyModelList", () => { { title: "Other models", section_kind: "ungrouped", models: ["x"] }, ], source: "no-default-models", - resolved_total_count: 1, + resolved_config_entry_count: 1, matched_count: 1, models_truncated: false, all_team_models_without_team: false, @@ -124,7 +124,7 @@ describe("KeyModelList", () => { { title: "Other models", section_kind: "ungrouped", models: ["gpt-4"] }, ], source: "no-default-models", - resolved_total_count: 1, + resolved_config_entry_count: 1, matched_count: 1, models_truncated: false, all_team_models_without_team: false, @@ -152,7 +152,7 @@ describe("KeyModelList", () => { data: { model_display_sections: [], source: "no-default-models", - resolved_total_count: 5, + resolved_config_entry_count: 5, matched_count: 0, models_truncated: false, all_team_models_without_team: false, @@ -184,7 +184,7 @@ describe("KeyModelList", () => { { title: "grp-b", section_kind: "access_group", models: ["m2", "m1"] }, ], source: "all-proxy-models", - resolved_total_count: 2, + resolved_config_entry_count: 2, matched_count: 2, models_truncated: false, all_team_models_without_team: false, diff --git a/ui/litellm-dashboard/src/components/networking.test.ts b/ui/litellm-dashboard/src/components/networking.test.ts index 6c2166d9367..5863417f629 100644 --- a/ui/litellm-dashboard/src/components/networking.test.ts +++ b/ui/litellm-dashboard/src/components/networking.test.ts @@ -423,7 +423,7 @@ describe("fetchKeyModelCall", () => { json: vi.fn().mockResolvedValue({ model_display_sections: [], source: "no-default-models", - resolved_total_count: 0, + resolved_config_entry_count: 0, matched_count: 0, models_truncated: false, all_team_models_without_team: false, diff --git a/ui/litellm-dashboard/src/components/networking.tsx b/ui/litellm-dashboard/src/components/networking.tsx index f33a87559dc..50074599acb 100644 --- a/ui/litellm-dashboard/src/components/networking.tsx +++ b/ui/litellm-dashboard/src/components/networking.tsx @@ -3323,7 +3323,8 @@ export interface KeyModelDisplaySection { export interface KeyModelResponse { model_display_sections: KeyModelDisplaySection[]; source: string; - resolved_total_count: number; + /** Number of entries in the post–sentinel-resolution list (`resolved`), not concrete model cardinality. */ + resolved_config_entry_count: number; matched_count: number; models_truncated: boolean; all_team_models_without_team: boolean; @@ -3339,38 +3340,34 @@ export const fetchKeyModelCall = async ( key_id: string, options?: FetchKeyModelCallOptions ): Promise => { - try { - const params = new URLSearchParams(); - if (options?.search !== undefined && options.search.trim() !== "") { - params.set("search", options.search.trim()); - } - if (options?.compact === true) { - params.set("compact", "true"); - } - const qs = params.toString(); - const base = proxyBaseUrl ? `${proxyBaseUrl}/key/${key_id}/models` : `/key/${key_id}/models`; - let url = qs ? `${base}?${qs}` : base; - - const response = await fetch(url, { - method: "GET", - headers: { - [globalLitellmHeaderName]: `Bearer ${accessToken}`, - "Content-Type": "application/json", - }, - }); - - if (!response.ok) { - const errorData = await response.json(); - const errorMessage = deriveErrorMessage(errorData); - handleError(errorMessage); - throw new Error(errorMessage); - } - - const data = await response.json(); - return data; - } catch (error) { - throw error; + const params = new URLSearchParams(); + if (options?.search !== undefined && options.search.trim() !== "") { + params.set("search", options.search.trim()); } + if (options?.compact === true) { + params.set("compact", "true"); + } + const qs = params.toString(); + const base = proxyBaseUrl ? `${proxyBaseUrl}/key/${key_id}/models` : `/key/${key_id}/models`; + const url = qs ? `${base}?${qs}` : base; + + const response = await fetch(url, { + method: "GET", + headers: { + [globalLitellmHeaderName]: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + const errorData = await response.json(); + const errorMessage = deriveErrorMessage(errorData); + handleError(errorMessage); + throw new Error(errorMessage); + } + + const data = await response.json(); + return data; }; export const userDailyActivityAggregatedCall = async (accessToken: string, startTime: Date, endTime: Date, userId: string | null = null) => { diff --git a/ui/litellm-dashboard/src/components/templates/key_info_view.test.tsx b/ui/litellm-dashboard/src/components/templates/key_info_view.test.tsx index b2d7a0f5e23..a41b6b97bd7 100644 --- a/ui/litellm-dashboard/src/components/templates/key_info_view.test.tsx +++ b/ui/litellm-dashboard/src/components/templates/key_info_view.test.tsx @@ -36,7 +36,7 @@ vi.mock("../networking", () => ({ }, ], source: "default", - resolved_total_count: 1, + resolved_config_entry_count: 1, matched_count: 1, models_truncated: false, all_team_models_without_team: false, diff --git a/ui/litellm-dashboard/src/hooks/keys/useGetKeyModels.test.tsx b/ui/litellm-dashboard/src/hooks/keys/useGetKeyModels.test.tsx index 80a6e5f98a5..a696ab2eadd 100644 --- a/ui/litellm-dashboard/src/hooks/keys/useGetKeyModels.test.tsx +++ b/ui/litellm-dashboard/src/hooks/keys/useGetKeyModels.test.tsx @@ -13,13 +13,14 @@ vi.mock("@/components/networking", () => ({ vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({ default: vi.fn(() => ({ accessToken: "test-token-456", + userId: "test-user-id", })), })); const emptyKeyModelResponse = { model_display_sections: [], source: "no-default-models", - resolved_total_count: 0, + resolved_config_entry_count: 0, matched_count: 0, models_truncated: false, all_team_models_without_team: false, @@ -43,13 +44,14 @@ describe("useGetKeyModels", () => { const useAuthorizedModule = await import("@/app/(dashboard)/hooks/useAuthorized"); vi.mocked(useAuthorizedModule.default).mockReturnValue({ accessToken: "test-token-456", + userId: "test-user-id", } as any); }); it("should load default full model list without compact", async () => { vi.mocked(networking.fetchKeyModelCall).mockResolvedValue({ ...emptyKeyModelResponse, - resolved_total_count: 3, + resolved_config_entry_count: 3, model_display_sections: [ { title: "Other models", section_kind: "ungrouped", models: ["a", "b", "c"] }, ], @@ -62,6 +64,6 @@ describe("useGetKeyModels", () => { await waitFor(() => expect(result.current.defaultModelsQuery.isSuccess).toBe(true)); expect(networking.fetchKeyModelCall).toHaveBeenCalledWith("test-token-456", "test-key-id"); - expect(result.current.defaultModelsQuery.data?.resolved_total_count).toBe(3); + expect(result.current.defaultModelsQuery.data?.resolved_config_entry_count).toBe(3); }); }); diff --git a/ui/litellm-dashboard/src/hooks/keys/useGetKeyModels.ts b/ui/litellm-dashboard/src/hooks/keys/useGetKeyModels.ts index 2a2c2754e27..5a38e0b7f78 100644 --- a/ui/litellm-dashboard/src/hooks/keys/useGetKeyModels.ts +++ b/ui/litellm-dashboard/src/hooks/keys/useGetKeyModels.ts @@ -8,7 +8,8 @@ import useAuthorized from '@/app/(dashboard)/hooks/useAuthorized'; const SEARCH_DEBOUNCE_MS = 450; export const useGetKeyModels = (key_id: string) => { - const { accessToken } = useAuthorized(); + const { accessToken, userId } = useAuthorized(); + const cachePrincipal = userId ?? accessToken ?? ''; const [searchInput, setSearchInputState] = useState(''); const [debouncedSearch, setDebouncedSearch] = useDebouncedState('', { wait: SEARCH_DEBOUNCE_MS, @@ -28,7 +29,7 @@ export const useGetKeyModels = (key_id: string) => { const isSearchDebouncing = searchInput.trim() !== trimmedDebounced; const defaultModelsQuery = useQuery({ - queryKey: ['keyModelsDefault', key_id], + queryKey: ['keyModelsDefault', key_id, cachePrincipal], queryFn: () => { if (!accessToken) throw new Error('Access Token required'); return fetchKeyModelCall(accessToken, key_id); @@ -37,7 +38,7 @@ export const useGetKeyModels = (key_id: string) => { }); const searchQuery = useQuery({ - queryKey: ['keyModelsSearch', key_id, trimmedDebounced], + queryKey: ['keyModelsSearch', key_id, trimmedDebounced, cachePrincipal], queryFn: () => { if (!accessToken) throw new Error('Access Token required'); return fetchKeyModelCall(accessToken, key_id, { search: trimmedDebounced });