diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/models/useModels.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/models/useModels.ts index a5fbc433ea3..a9f7c54698a 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/hooks/models/useModels.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/models/useModels.ts @@ -38,6 +38,7 @@ export const useModelsInfo = ( sortBy?: string, sortOrder?: string, excludeAutoRouters: boolean = false, + modelName?: string, ) => { const { accessToken, userId, userRole } = useAuthorized(); return useQuery({ @@ -48,6 +49,7 @@ export const useModelsInfo = ( page, size, ...(search && { search }), + ...(modelName && { modelName }), ...(modelId && { modelId }), ...(teamId && { teamId }), ...(sortBy && { sortBy }), @@ -70,6 +72,7 @@ export const useModelsInfo = ( sortBy, sortOrder, excludeAutoRouters, + modelName, ), enabled: Boolean(accessToken && userId && userRole), }); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AllModelsTab.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AllModelsTab.test.tsx index 29163108442..65faa85e29e 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AllModelsTab.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AllModelsTab.test.tsx @@ -33,6 +33,7 @@ interface ModelsInfoArgs { teamId?: string; sortBy?: string; sortOrder?: string; + modelName?: string; } const modelsInfoCalls: ModelsInfoArgs[] = []; @@ -47,12 +48,14 @@ type UseModelsInfoArgs = [ teamId?: string, sortBy?: string, sortOrder?: string, + excludeAutoRouters?: boolean, + modelName?: string, ]; vi.mock("../../hooks/models/useModels", () => ({ useModelsInfo: (...args: UseModelsInfoArgs) => { - const [page, size, search, , teamId, sortBy, sortOrder] = args; - const call: ModelsInfoArgs = { page, size, search, teamId, sortBy, sortOrder }; + const [page, size, search, , teamId, sortBy, sortOrder, , modelName] = args; + const call: ModelsInfoArgs = { page, size, search, teamId, sortBy, sortOrder, modelName }; modelsInfoCalls.push(call); return { ...modelsInfoResult, refetch: mockRefetch }; }, @@ -260,25 +263,26 @@ describe("AllModelsTab", () => { expect(within(table).queryByText("gpt-4")).not.toBeInTheDocument(); }); - it("queries the server for the selected model group so deployments beyond the first page are found", () => { + it("asks the server for the exact selected model group so deployments beyond the first page are found", () => { render(); - expect(lastModelsInfoCall().search).toBe("claude-opus"); - }); - - it.each(["all", "wildcard"])("does not seed the server search from the %s pseudo group", (group) => { - render(); - + expect(lastModelsInfoCall().modelName).toBe("claude-opus"); expect(lastModelsInfoCall().search).toBeUndefined(); }); - it("lets a typed search override the selected model group in the server query", async () => { - const user = userEvent.setup(); + it.each(["all", "wildcard"])("sends no exact model name for the %s pseudo group", (group) => { + render(); + + expect(lastModelsInfoCall().modelName).toBeUndefined(); + }); + + it("keeps the exact model group alongside a typed search", async () => { render(); - await user.type(screen.getByPlaceholderText("Search model names…"), "gpt"); + fireEvent.change(screen.getByPlaceholderText("Search model names…"), { target: { value: "opus" } }); - await waitFor(() => expect(lastModelsInfoCall().search).toBe("gpt")); + await waitFor(() => expect(lastModelsInfoCall().search).toBe("opus")); + expect(lastModelsInfoCall().modelName).toBe("claude-opus"); }); it("resets search, filters, team and sorting from the drawer reset button", async () => { diff --git a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AllModelsTab.tsx b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AllModelsTab.tsx index a710940e659..be2cf22d71a 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AllModelsTab.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AllModelsTab.tsx @@ -85,7 +85,7 @@ const AllModelsTab = ({ Boolean(selectedModelGroup) && selectedModelGroup !== ALL_MODEL_GROUPS_VALUE && selectedModelGroup !== WILDCARD_MODEL_GROUP_VALUE; - const searchForQuery = debouncedSearch || (isConcreteModelGroup ? selectedModelGroup ?? undefined : undefined); + const modelNameForQuery = isConcreteModelGroup ? selectedModelGroup ?? undefined : undefined; const sortBy = useMemo(() => { if (sorting.length === 0) return undefined; @@ -105,7 +105,7 @@ const AllModelsTab = ({ } = useModelsInfo( pagination.pageIndex + 1, pagination.pageSize, - searchForQuery, + debouncedSearch || undefined, undefined, teamIdForQuery, sortBy, @@ -113,6 +113,7 @@ const AllModelsTab = ({ // Auto-routers are routing constructs, not deployments; the sibling Auto-Routers tab // lists and manages them. Excluded server-side so total_count stays honest. true, + modelNameForQuery, ); const isLoading = isLoadingModelsInfo || isLoadingModelCostMap; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/detailNavigation.ts b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/detailNavigation.ts index 16d031c52c1..e83a81a53cf 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/detailNavigation.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/detailNavigation.ts @@ -47,7 +47,6 @@ export interface ModelGroupFilterRouting { setModelGroup: (modelGroup: string | null) => void; } -/** `?model_group=` backs the All Models group filter so other pages can deep-link to one group. */ export function useModelGroupFilterRouting(): ModelGroupFilterRouting { const [modelGroup, setParam] = useQueryState("model_group", parseAsString); diff --git a/ui/litellm-dashboard/src/components/networking.test.ts b/ui/litellm-dashboard/src/components/networking.test.ts index cd22935a66f..71296bc1dfd 100644 --- a/ui/litellm-dashboard/src/components/networking.test.ts +++ b/ui/litellm-dashboard/src/components/networking.test.ts @@ -104,6 +104,45 @@ describe("loginCall - storeLoginToken integration", () => { }); }); +describe("modelInfoCall", () => { + let currentFetch: typeof global.fetch; + + beforeEach(() => { + currentFetch = global.fetch; + }); + + afterEach(() => { + global.fetch = currentFetch; + }); + + it("sends the exact model name as the model query param and leaves search alone", async () => { + const mockFetch = vi.fn().mockResolvedValue({ ok: true, json: vi.fn().mockResolvedValue({ data: [] }) } as any); + global.fetch = mockFetch as any; + + await Networking.modelInfoCall( + "token", + "user", + "Admin", + 2, + 25, + undefined, + undefined, + undefined, + undefined, + undefined, + true, + "gpt-4", + ); + + const parsed = new URL(mockFetch.mock.calls[0][0] as string, "http://example.com"); + expect(parsed.pathname).toBe("/v2/model/info"); + expect(parsed.searchParams.get("model")).toBe("gpt-4"); + expect(parsed.searchParams.has("search")).toBe(false); + expect(parsed.searchParams.get("page")).toBe("2"); + expect(parsed.searchParams.get("exclude_auto_routers")).toBe("true"); + }); +}); + describe("daily activity helpers", () => { const startTime = new Date("2025-02-12T00:00:00.000Z"); const endTime = new Date("2025-02-19T00:00:00.000Z"); diff --git a/ui/litellm-dashboard/src/components/networking.tsx b/ui/litellm-dashboard/src/components/networking.tsx index f7368f3957d..c95d57dec3f 100644 --- a/ui/litellm-dashboard/src/components/networking.tsx +++ b/ui/litellm-dashboard/src/components/networking.tsx @@ -1656,6 +1656,7 @@ export const modelInfoCall = async ( sortBy?: string, sortOrder?: string, excludeAutoRouters?: boolean, + modelName?: string, ) => { /** * Get all models on proxy @@ -1669,6 +1670,9 @@ export const modelInfoCall = async ( if (search && search.trim()) { params.append("search", search.trim()); } + if (modelName && modelName.trim()) { + params.append("model", modelName.trim()); + } if (modelId && modelId.trim()) { params.append("modelId", modelId.trim()); } diff --git a/ui/litellm-dashboard/src/utils/entityLinks.ts b/ui/litellm-dashboard/src/utils/entityLinks.ts index ced9bfb5395..ad257ec7969 100644 --- a/ui/litellm-dashboard/src/utils/entityLinks.ts +++ b/ui/litellm-dashboard/src/utils/entityLinks.ts @@ -22,7 +22,6 @@ export function orgDetailHref(orgId: string): string { return `${migratedHref("organizations")}?org=${encodeURIComponent(orgId)}`; } -/** Models page filtered to one model group; undefined for grant sentinels that name no deployment. */ export function modelGroupHref(modelGroup: string): string | undefined { if (MODEL_GRANT_SENTINELS.has(modelGroup)) return undefined; return `${migratedHref("models-and-endpoints")}?model_group=${encodeURIComponent(modelGroup)}`;