From 490face7deb6f49278a076cf6980a69cca469ccd Mon Sep 17 00:00:00 2001 From: tin-berri Date: Thu, 27 Aug 2026 11:30:48 -0700 Subject: [PATCH] fix(ui): order the auto-routers table newest first so a new router lands on page one (#38545) /v2/model/info returns llm_router.model_list, which carries no defined order: the DB read has no order_by and an edited deployment is popped and re-appended. The Auto routers table rendered that order verbatim behind a ten-row first page, so on a proxy with more than ten auto routers a router created moments ago was drawn wherever the API happened to return it, in practice last, and read as never created Adopt the ordering the rest of the dashboard already uses, with the two cases this table has and its siblings do not. created_at is enterprise-gated and config.yaml routers never carry one, so seeding created_at desc alone leaves every comparison tied on a non-premium proxy and the fix a no-op. The column now declares sortUndefined last, which table-core applies before the desc flip so undated rows stay last in both directions, and the row emits undefined rather than null so that branch is reachable at all. Name is the secondary key, giving the undated block a defined order too Page size is deliberately unchanged: it exposes the missing order rather than causing it --- .../AutoRouters/AutoRoutersPanel.test.tsx | 55 +++++++++++++++++++ .../AutoRouters/AutoRoutersTable.tsx | 12 ++-- .../AutoRouters/AutoRoutersTableColumns.tsx | 1 + .../components/AutoRouters/autoRouterRows.ts | 5 +- 4 files changed, 66 insertions(+), 7 deletions(-) diff --git a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AutoRouters/AutoRoutersPanel.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AutoRouters/AutoRoutersPanel.test.tsx index 9ec551bc227..8c683f230e0 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AutoRouters/AutoRoutersPanel.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AutoRouters/AutoRoutersPanel.test.tsx @@ -107,6 +107,33 @@ const mockDeploymentsPage = () => { modelInfoCall.mockResolvedValue(pageOf(DEPLOYMENTS)); }; +// Oldest-first, as the proxy returns them, and two more than the ten-row first page holds. +const BULK_ROUTER_NAMES = [ + "router-01-oldest", + ...Array.from({ length: 10 }, (_, i) => `router-${i + 2}`), + "router-12-newest", +]; + +const A_FULL_PAGE_AND_TWO_MORE = Array.from({ length: 12 }, (_, index) => ({ + model_name: BULK_ROUTER_NAMES[index], + litellm_params: { + model: "auto_router/complexity_router", + complexity_router_config: { tiers: {}, classifier_type: "heuristic" }, + }, + model_info: { + id: `bulk-${index + 1}`, + db_model: true, + created_at: `2026-08-${String(index + 1).padStart(2, "0")}T00:00:00.000000+00:00`, + }, +})); + +/** Row order as rendered, header row dropped. */ +const routerNamesInOrder = () => + screen + .getAllByRole("row") + .slice(1) + .map((row) => row.querySelector("span.text-sm.font-medium")?.textContent ?? ""); + const renderPanel = (canModify = true) => renderWithProviders( { await screen.findByText("config-router"); expect(screen.queryByTestId("auto-router-actions-auto-4")).not.toBeInTheDocument(); }); + + // /v2/model/info returns an unordered model_list, and created_at is absent on config routers + // and on non-enterprise proxies, so both halves of the order have to be pinned here. + it("orders newest first, then the undated routers by name", async () => { + renderPanel(); + + await screen.findByText("tri-tier-router"); + + expect(routerNamesInOrder()).toEqual([ + "tri-tier-router", // 2026-07-28 + "support-router", // 2026-07-27 + "adaptive-router", // undated, sorts after every dated row, then by name + "config-router", + ]); + }); + + // The reported bug: the newest router was rendered last, so it landed on page 2 and read + // as never created. + it("puts a just-created router on the first page of a list longer than one page", async () => { + modelInfoCall.mockResolvedValue(pageOf(A_FULL_PAGE_AND_TWO_MORE)); + + renderPanel(); + + expect(await screen.findByRole("button", { name: "router-12-newest" })).toBeInTheDocument(); + // Page one holds the ten newest, so the two oldest are the ones pushed off it. + expect(screen.queryByRole("button", { name: "router-01-oldest" })).not.toBeInTheDocument(); + expect(routerNamesInOrder()[0]).toBe("router-12-newest"); + }); }); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AutoRouters/AutoRoutersTable.tsx b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AutoRouters/AutoRoutersTable.tsx index 943388f8535..2102f5e55d9 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AutoRouters/AutoRoutersTable.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AutoRouters/AutoRoutersTable.tsx @@ -1,7 +1,7 @@ "use client"; import { SortingState } from "@tanstack/react-table"; -import { useMemo, useState } from "react"; +import { useMemo } from "react"; import { DataTable } from "@/components/shared/DataTable"; import { AutoRouterIcon } from "@/components/shared/table_cells"; @@ -19,6 +19,11 @@ interface AutoRoutersTableProps { const PAGE_SIZE_OPTIONS = [10, 25, 50]; +const DEFAULT_SORTING: SortingState = [ + { id: "createdAt", desc: true }, + { id: "name", desc: false }, +]; + function EmptyState({ canModify }: { canModify: boolean }) { return (
@@ -42,8 +47,6 @@ export function AutoRoutersTable({ onRouterClick, onDeleteClick, }: AutoRoutersTableProps) { - const [sorting, setSorting] = useState([]); - const columns = useMemo( () => getAutoRoutersTableColumns({ canModify, onRouterClick, onDeleteClick }), [canModify, onRouterClick, onDeleteClick], @@ -55,8 +58,7 @@ export function AutoRoutersTable({ columns={columns} getRowId={(router) => router.id} sortingMode="client" - sorting={sorting} - onSortingChange={setSorting} + defaultSorting={DEFAULT_SORTING} paginationMode="client" pageSizeOptions={PAGE_SIZE_OPTIONS} isLoading={isLoading} diff --git a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AutoRouters/AutoRoutersTableColumns.tsx b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AutoRouters/AutoRoutersTableColumns.tsx index 995ba634c34..4a99062988f 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AutoRouters/AutoRoutersTableColumns.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AutoRouters/AutoRoutersTableColumns.tsx @@ -155,6 +155,7 @@ export const getAutoRoutersTableColumns = ({ size: 150, enableSorting: true, sortingFn: "datetime", + sortUndefined: "last", cell: ({ row }) => , }, ...(canModify diff --git a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AutoRouters/autoRouterRows.ts b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AutoRouters/autoRouterRows.ts index a8111ddb02d..bbdf4697315 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AutoRouters/autoRouterRows.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AutoRouters/autoRouterRows.ts @@ -30,7 +30,8 @@ export interface AutoRouterRow { editBlockedReason: EditBlockedReason | null; targets: string[]; defaultModel: string | null; - createdAt: string | null; + /** `undefined`, not `null`: the table's `sortUndefined` pin only matches `undefined` */ + createdAt: string | undefined; deployment: AutoRouterDeployment; } @@ -113,7 +114,7 @@ export const toAutoRouterRow = ( canEdit: canEdit && mayActOnRow, canDelete: canDelete && mayActOnRow, editBlockedReason, - createdAt: info.created_at ?? null, + createdAt: info.created_at ?? undefined, defaultModel: (params[strategy.defaultModelKey] as string | null | undefined) ?? null, deployment, ...PRESENTERS[strategy.kind](asRecord(params[strategy.configKey])),