mirror of
https://github.com/BerriAI/litellm.git
synced 2026-08-28 05:25:59 +00:00
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
This commit is contained in:
parent
ca9007be39
commit
490face7de
4 changed files with 66 additions and 7 deletions
|
|
@ -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(
|
||||
<AutoRoutersPanel
|
||||
|
|
@ -257,4 +284,32 @@ describe("AutoRoutersPanel", () => {
|
|||
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");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className="flex flex-col items-center gap-1 py-6">
|
||||
|
|
@ -42,8 +47,6 @@ export function AutoRoutersTable({
|
|||
onRouterClick,
|
||||
onDeleteClick,
|
||||
}: AutoRoutersTableProps) {
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
|
||||
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}
|
||||
|
|
|
|||
|
|
@ -155,6 +155,7 @@ export const getAutoRoutersTableColumns = ({
|
|||
size: 150,
|
||||
enableSorting: true,
|
||||
sortingFn: "datetime",
|
||||
sortUndefined: "last",
|
||||
cell: ({ row }) => <DateCell value={row.original.createdAt} precision="date" />,
|
||||
},
|
||||
...(canModify
|
||||
|
|
|
|||
|
|
@ -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])),
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue