diff --git a/ui/litellm-dashboard/src/app/(dashboard)/components/Sidebar2.tsx b/ui/litellm-dashboard/src/app/(dashboard)/components/Sidebar2.tsx
deleted file mode 100644
index 90f498912a..0000000000
--- a/ui/litellm-dashboard/src/app/(dashboard)/components/Sidebar2.tsx
+++ /dev/null
@@ -1,472 +0,0 @@
-"use client";
-
-import { Layout, Menu, ConfigProvider } from "antd";
-import {
- KeyOutlined,
- PlayCircleOutlined,
- BlockOutlined,
- BarChartOutlined,
- TeamOutlined,
- BankOutlined,
- UserOutlined,
- SettingOutlined,
- ApiOutlined,
- AppstoreOutlined,
- DatabaseOutlined,
- FileTextOutlined,
- LineChartOutlined,
- SafetyOutlined,
- ExperimentOutlined,
- ToolOutlined,
- TagsOutlined,
- AuditOutlined,
-} from "@ant-design/icons";
-// import {
-// all_admin_roles,
-// rolesWithWriteAccess,
-// internalUserRoles,
-// isAdminRole,
-// } from "../utils/roles";
-// import UsageIndicator from "./usage_indicator";
-import * as React from "react";
-import { useRouter, usePathname } from "next/navigation";
-import { all_admin_roles, internalUserRoles, isAdminRole, rolesWithWriteAccess } from "@/utils/roles";
-import UsageIndicator from "@/components/UsageIndicator";
-import { serverRootPath } from "@/components/networking";
-
-const { Sider } = Layout;
-
-// -------- Types --------
-interface SidebarProps {
- accessToken: string | null;
- userRole: string;
- /** Fallback selection id (legacy), used if path can't be matched */
- defaultSelectedKey: string;
- collapsed?: boolean;
-}
-
-interface MenuItemCfg {
- key: string;
- newTab?: boolean;
- page: string; // legacy id; we map this to a path below
- label: string;
- roles?: string[];
- children?: MenuItemCfg[];
- icon?: React.ReactNode;
-}
-
-/** ---------- Base URL helpers ---------- */
-/**
- * Normalizes NEXT_PUBLIC_BASE_URL to either "/" or "/ui/" (always with a trailing slash).
- * Supported env values: "" or "ui/".
- * Also considers the serverRootPath from the proxy config (e.g., "/my-custom-path").
- */
-const getBasePath = () => {
- const raw = process.env.NEXT_PUBLIC_BASE_URL ?? "";
- const trimmed = raw.replace(/^\/+|\/+$/g, ""); // strip leading/trailing slashes
- const uiPath = trimmed ? `/${trimmed}/` : "/";
-
- // If serverRootPath is set and not "/", prepend it to the UI path
- if (serverRootPath && serverRootPath !== "/") {
- // Remove trailing slash from serverRootPath and ensure uiPath has no leading slash for proper joining
- const cleanServerRoot = serverRootPath.replace(/\/+$/, "");
- const cleanUiPath = uiPath.replace(/^\/+/, "");
- return `${cleanServerRoot}/${cleanUiPath}`;
- }
-
- return uiPath;
-};
-
-/** Map legacy `page` ids to real app routes (relative, no leading slash). */
-const routeFor = (slug: string): string => {
- switch (slug) {
- // top level
- case "api-keys":
- return "virtual-keys";
- case "llm-playground":
- return "test-key";
- case "models":
- return "models-and-endpoints";
- case "new_usage":
- return "usage";
- case "teams":
- return "teams";
- case "organizations":
- return "organizations";
- case "users":
- return "users";
- case "api_ref":
- return "api-reference";
- case "model-hub-table":
- // If you intend the newer in-dashboard page, use "model-hub".
- return "model-hub";
- case "logs":
- return "logs";
- case "guardrails":
- return "guardrails";
- case "policies":
- return "policies";
- case "chat":
- return "chat";
-
- // tools
- case "mcp-servers":
- return "tools/mcp-servers";
- case "vector-stores":
- return "tools/vector-stores";
- case "byok-demo":
- return "tools/byok-demo";
-
- // experimental
- case "caching":
- return "experimental/caching";
- case "prompts":
- return "experimental/prompts";
- case "budgets":
- return "experimental/budgets";
- case "transform-request":
- return "experimental/api-playground";
- case "tag-management":
- return "experimental/tag-management";
- case "claude-code-plugins":
- return "experimental/claude-code-plugins";
- case "usage": // "Old Usage"
- return "experimental/old-usage";
-
- // settings
- case "general-settings":
- return "settings/router-settings";
- case "settings": // "Logging & Alerts"
- return "settings/logging-and-alerts";
- case "admin-panel":
- return "settings/admin-settings";
- case "ui-theme":
- return "settings/ui-theme";
-
- default:
- // treat as already a relative path
- return slug.replace(/^\/+/, "");
- }
-};
-
-/** Prefix base path ("/" or "/ui/") */
-const toHref = (slugOrPath: string) => {
- const base = getBasePath(); // "/" or "/ui/"
- const rel = routeFor(slugOrPath).replace(/^\/+|\/+$/g, "");
- return `${base}${rel}`;
-};
-
-// ----- Menu config (unchanged labels/icons; same appearance) -----
-const menuItems: MenuItemCfg[] = [
- { key: "1", page: "api-keys", label: "Virtual Keys", icon: },
- {
- key: "3",
- page: "llm-playground",
- label: "Test Key",
- icon: ,
- roles: rolesWithWriteAccess,
- },
- {
- key: "2",
- page: "models",
- label: "Models + Endpoints",
- icon: ,
- roles: rolesWithWriteAccess,
- },
- {
- key: "12",
- page: "new_usage",
- label: "Usage",
- icon: ,
- roles: [...all_admin_roles, ...internalUserRoles],
- },
- { key: "6", page: "teams", label: "Teams", icon: },
- {
- key: "17",
- page: "organizations",
- label: "Organizations",
- icon: ,
- roles: all_admin_roles,
- },
- {
- key: "5",
- page: "users",
- label: "Internal Users",
- icon: ,
- roles: all_admin_roles,
- },
- { key: "14", page: "api-reference", label: "API Reference", icon: },
- {
- key: "16",
- page: "model-hub-table",
- label: "Model Hub",
- icon: ,
- },
- { key: "15", page: "logs", label: "Logs", icon: },
- {
- key: "11",
- page: "guardrails",
- label: "Guardrails",
- icon: ,
- roles: all_admin_roles,
- },
- {
- key: "28",
- page: "policies",
- label: "Policies",
- icon: ,
- roles: all_admin_roles,
- },
- {
- key: "26",
- page: "tools",
- label: "Tools",
- icon: ,
- children: [
- { key: "18", page: "mcp-servers", label: "MCP Servers", icon: },
- {
- key: "21",
- page: "vector-stores",
- label: "Vector Stores",
- icon: ,
- roles: all_admin_roles,
- },
- ],
- },
- {
- key: "experimental",
- page: "experimental",
- label: "Experimental",
- icon: ,
- children: [
- {
- key: "9",
- page: "caching",
- label: "Caching",
- icon: ,
- roles: all_admin_roles,
- },
- {
- key: "25",
- page: "prompts",
- label: "Prompts",
- icon: ,
- roles: all_admin_roles,
- },
- {
- key: "10",
- page: "budgets",
- label: "Budgets",
- icon: ,
- roles: all_admin_roles,
- },
- {
- key: "20",
- page: "transform-request",
- label: "API Playground",
- icon: ,
- roles: [...all_admin_roles, ...internalUserRoles],
- },
- {
- key: "19",
- page: "tag-management",
- label: "Tag Management",
- icon: ,
- roles: all_admin_roles,
- },
- {
- key: "27",
- page: "claude-code-plugins",
- label: "Claude Code Plugins",
- icon: ,
- roles: all_admin_roles,
- },
- { key: "4", page: "usage", label: "Old Usage", icon: },
- ],
- },
- {
- key: "settings",
- page: "settings",
- label: "Settings",
- icon: ,
- roles: all_admin_roles,
- children: [
- {
- key: "11",
- page: "general-settings",
- label: "Router Settings",
- icon: ,
- roles: all_admin_roles,
- },
- {
- key: "8",
- page: "settings",
- label: "Logging & Alerts",
- icon: ,
- roles: all_admin_roles,
- },
- {
- key: "13",
- page: "admin-panel",
- label: "Admin Settings",
- icon: ,
- roles: all_admin_roles,
- },
- {
- key: "14",
- page: "ui-theme",
- label: "UI Theme",
- icon: ,
- roles: all_admin_roles,
- },
- ],
- },
-];
-
-const Sidebar2: React.FC = ({ accessToken, userRole, defaultSelectedKey, collapsed = false }) => {
- const router = useRouter();
- const pathname = usePathname() || "/";
-
- // ----- Filter by role without mutating originals -----
- const filteredMenuItems = React.useMemo(() => {
- return menuItems
- .filter((item) => !item.roles || item.roles.includes(userRole))
- .map((item) => ({
- ...item,
- children: item.children ? item.children.filter((c) => !c.roles || c.roles.includes(userRole)) : undefined,
- }));
- }, [userRole]);
-
- // ----- Compute selected key from current path -----
- const selectedMenuKey = React.useMemo(() => {
- const base = getBasePath();
- // strip base prefix and leading slash -> "virtual-keys", "tools/mcp-servers", etc.
- const rel = pathname.startsWith(base) ? pathname.slice(base.length) : pathname.replace(/^\/+/, "");
- const relLower = rel.toLowerCase();
-
- const matchesPath = (slug: string) => {
- const route = routeFor(slug).toLowerCase();
- return relLower === route || relLower.startsWith(`${route}/`);
- };
-
- // search top-level
- for (const item of filteredMenuItems) {
- if (!item.children && matchesPath(item.page)) return item.key;
- if (item.children) {
- for (const child of item.children) {
- if (matchesPath(child.page)) return child.key;
- }
- }
- }
-
- // fallback to legacy defaultSelectedKey mapping
- const fallback = filteredMenuItems.find((i) => i.page === defaultSelectedKey)?.key;
- if (fallback) return fallback;
-
- for (const item of filteredMenuItems) {
- if (item.children?.some((c) => c.page === defaultSelectedKey)) {
- const child = item.children.find((c) => c.page === defaultSelectedKey)!;
- return child.key;
- }
- }
-
- return "1";
- }, [pathname, filteredMenuItems, defaultSelectedKey]);
-
- // ----- Navigation -----
- const goTo = (slug: string, newTab?: boolean) => {
- const href = toHref(slug);
- if (newTab) {
- window.open(href, "_blank");
- } else {
- router.push(href);
- }
- };
-
- // Wrap label in so every nav item supports right-click → "Open in new tab"
- // and Ctrl/Cmd+click to open in a new tab, while preserving SPA navigation for normal clicks.
- const renderNavLink = (label: string, page: string, newTab?: boolean): React.ReactNode => {
- const href = toHref(page);
- return (
- {
- if (newTab) {
- e.stopPropagation();
- return;
- }
- if (e.metaKey || e.ctrlKey || e.shiftKey || e.button === 1) {
- e.stopPropagation();
- return;
- }
- e.preventDefault();
- }}
- style={{ color: "inherit", textDecoration: "none" }}
- >
- {label}
-
- );
- };
-
- return (
-
-
-
-
- {isAdminRole(userRole) && !collapsed && }
-
-
- );
-};
-
-export default Sidebar2;
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/layout.tsx b/ui/litellm-dashboard/src/app/(dashboard)/layout.tsx
index a611d619cc..7dc2b2ad61 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/layout.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/layout.tsx
@@ -7,6 +7,7 @@ import SidebarProvider from "@/app/(dashboard)/components/SidebarProvider";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
import { useRouter, useSearchParams } from "next/navigation";
import { DebugWarningBanner } from "@/components/DebugWarningBanner";
+import { MIGRATED_PAGES } from "@/utils/migratedPages";
/** ---- BASE URL HELPERS ---- */
function normalizeBasePrefix(raw: string | undefined | null): string {
@@ -23,15 +24,6 @@ function withBase(path: string): string {
}
/** -------------------------------- */
-/**
- * Pages that have been migrated to path-based routing under (dashboard)/.
- * When the leftnav triggers one of these, navigate to the path route instead
- * of the legacy query-param root page.
- *
- * Key = legacy page id used in leftnav, Value = route segment under (dashboard)/
- */
-const MIGRATED_PAGES: Record = {};
-
function LayoutContent({ children }: { children: React.ReactNode }) {
const router = useRouter();
const searchParams = useSearchParams();
diff --git a/ui/litellm-dashboard/src/app/page.tsx b/ui/litellm-dashboard/src/app/page.tsx
index da6a0d5a76..f329a00d60 100644
--- a/ui/litellm-dashboard/src/app/page.tsx
+++ b/ui/litellm-dashboard/src/app/page.tsx
@@ -54,6 +54,7 @@ import {
storeReturnUrl,
} from "@/utils/returnUrlUtils";
import { isAdminRole } from "@/utils/roles";
+import { MIGRATED_PAGES } from "@/utils/migratedPages";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { useRouter, useSearchParams } from "next/navigation";
import { Suspense, useEffect, useMemo, useRef, useState } from "react";
@@ -65,13 +66,6 @@ interface ProxySettings {
LITELLM_UI_API_DOC_BASE_URL?: string | null;
}
-/**
- * Map of legacy query-param page keys → new path-based route segments.
- * When a user visits ?page=, they are redirected to /ui/.
- * Add entries here as pages are migrated from the if/else chain to path-based routes.
- */
-const LEGACY_REDIRECTS: Record = {};
-
function CreateKeyPageContent() {
const { authLoading, token, userID, userRole, userEmail, accessToken, premiumUser, setUserRole, setUserEmail } =
useAuth();
@@ -202,11 +196,11 @@ function CreateKeyPageContent() {
}, [redirectToLogin]);
// Redirect legacy query-param pages to their new path-based routes
- const isLegacyRedirect = page in LEGACY_REDIRECTS;
+ const isLegacyRedirect = page in MIGRATED_PAGES;
useEffect(() => {
if (!authLoading && isLegacyRedirect) {
const base = (proxyBaseUrl || "") + "/ui";
- router.replace(`${base}/${LEGACY_REDIRECTS[page]}`);
+ router.replace(`${base}/${MIGRATED_PAGES[page]}`);
}
}, [authLoading, isLegacyRedirect, page, router]);
diff --git a/ui/litellm-dashboard/src/components/leftnav.tsx b/ui/litellm-dashboard/src/components/leftnav.tsx
index 43a28d9404..ff0dac703d 100644
--- a/ui/litellm-dashboard/src/components/leftnav.tsx
+++ b/ui/litellm-dashboard/src/components/leftnav.tsx
@@ -43,31 +43,9 @@ import {
import NewBadge from "./common_components/NewBadge";
import type { Organization } from "./networking";
import UsageIndicator from "./UsageIndicator";
-import { serverRootPath } from "./networking";
+import { MIGRATED_PAGES, migratedHref } from "@/utils/migratedPages";
const { Sider } = Layout;
-/**
- * Pages migrated to path-based routing under (dashboard)/.
- * Key = legacy page id, Value = route segment.
- * Keep in sync with MIGRATED_PAGES in (dashboard)/layout.tsx.
- */
-const MIGRATED_PAGES: Record = {};
-
-/** Build an absolute href for a migrated page, respecting base URL + serverRootPath. */
-function migratedHref(routeSegment: string): string {
- const raw = process.env.NEXT_PUBLIC_BASE_URL ?? "";
- const trimmed = raw.replace(/^\/+|\/+$/g, "");
- let base = trimmed ? `/${trimmed}/` : "/";
-
- if (serverRootPath && serverRootPath !== "/") {
- const cleanRoot = serverRootPath.replace(/\/+$/, "");
- const cleanBase = base.replace(/^\/+/, "");
- base = `${cleanRoot}/${cleanBase}`;
- }
-
- return `${base}${routeSegment}`;
-}
-
// Define the props type
interface SidebarProps {
setPage: (page: string) => void;
diff --git a/ui/litellm-dashboard/src/utils/migratedPages.test.ts b/ui/litellm-dashboard/src/utils/migratedPages.test.ts
new file mode 100644
index 0000000000..2f9ed11b3a
--- /dev/null
+++ b/ui/litellm-dashboard/src/utils/migratedPages.test.ts
@@ -0,0 +1,43 @@
+import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
+
+describe("migratedHref", () => {
+ beforeEach(() => {
+ vi.resetModules();
+ vi.unstubAllEnvs();
+ });
+
+ afterEach(() => {
+ vi.unstubAllEnvs();
+ });
+
+ it("returns an absolute path rooted at / when serverRootPath is /", async () => {
+ vi.doMock("@/components/networking", () => ({ serverRootPath: "/" }));
+ const { migratedHref } = await import("./migratedPages");
+
+ expect(migratedHref("api-reference")).toBe("/api-reference");
+ expect(migratedHref("virtual-keys")).toBe("/virtual-keys");
+ });
+
+ it("prefixes a non-root serverRootPath without duplicating slashes", async () => {
+ vi.doMock("@/components/networking", () => ({ serverRootPath: "/team-x/" }));
+ const { migratedHref } = await import("./migratedPages");
+
+ expect(migratedHref("api-reference")).toBe("/team-x/api-reference");
+ });
+
+ it("honors NEXT_PUBLIC_BASE_URL as a base path segment", async () => {
+ vi.stubEnv("NEXT_PUBLIC_BASE_URL", "ui");
+ vi.doMock("@/components/networking", () => ({ serverRootPath: "/" }));
+ const { migratedHref } = await import("./migratedPages");
+
+ expect(migratedHref("api-reference")).toBe("/ui/api-reference");
+ });
+
+ it("combines NEXT_PUBLIC_BASE_URL with a non-root serverRootPath", async () => {
+ vi.stubEnv("NEXT_PUBLIC_BASE_URL", "ui");
+ vi.doMock("@/components/networking", () => ({ serverRootPath: "/team-x" }));
+ const { migratedHref } = await import("./migratedPages");
+
+ expect(migratedHref("api-reference")).toBe("/team-x/ui/api-reference");
+ });
+});
diff --git a/ui/litellm-dashboard/src/utils/migratedPages.ts b/ui/litellm-dashboard/src/utils/migratedPages.ts
new file mode 100644
index 0000000000..5421f00bf0
--- /dev/null
+++ b/ui/litellm-dashboard/src/utils/migratedPages.ts
@@ -0,0 +1,25 @@
+import { serverRootPath } from "@/components/networking";
+
+/**
+ * Single source of truth for pages cut over from the legacy `?page=` switch in
+ * app/page.tsx to path-based routes under app/(dashboard)/.
+ *
+ * Key = legacy page id emitted by the sidebar. Value = route segment under (dashboard)/.
+ * Add an entry to route the sidebar and deep links to the new path and redirect the
+ * legacy `?page=` URL; remove it to roll back. Empty until a page is migrated.
+ */
+export const MIGRATED_PAGES: Record = {};
+
+export function migratedHref(routeSegment: string): string {
+ const raw = process.env.NEXT_PUBLIC_BASE_URL ?? "";
+ const trimmed = raw.replace(/^\/+|\/+$/g, "");
+ let base = trimmed ? `/${trimmed}/` : "/";
+
+ if (serverRootPath && serverRootPath !== "/") {
+ const cleanRoot = serverRootPath.replace(/\/+$/, "");
+ const cleanBase = base.replace(/^\/+/, "");
+ base = `${cleanRoot}/${cleanBase}`;
+ }
+
+ return `${base}${routeSegment}`;
+}