diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/useDisableLiteAdmin.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/useDisableLiteAdmin.ts
new file mode 100644
index 00000000000..cbc3a5a81f6
--- /dev/null
+++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/useDisableLiteAdmin.ts
@@ -0,0 +1,34 @@
+import { useSyncExternalStore } from "react";
+import { getProxyBaseUrl } from "@/components/networking";
+import {
+ LOCAL_STORAGE_EVENT,
+ emitLocalStorageChange,
+ getLocalStorageItem,
+ removeLocalStorageItem,
+ setLocalStorageItem,
+} from "@/utils/localStorageUtils";
+
+function subscribe(callback: () => void) {
+ window.addEventListener("storage", callback);
+ window.addEventListener(LOCAL_STORAGE_EVENT, callback);
+ return () => {
+ window.removeEventListener("storage", callback);
+ window.removeEventListener(LOCAL_STORAGE_EVENT, callback);
+ };
+}
+
+export function useDisableLiteAdmin(userId: string | null) {
+ const key = userId ? `disableLiteAdmin:${JSON.stringify([getProxyBaseUrl(), userId])}` : null;
+ const disabled = useSyncExternalStore(
+ subscribe,
+ () => key !== null && getLocalStorageItem(key) === "true",
+ () => false,
+ );
+ const setDisabled = (value: boolean) => {
+ if (key === null) return;
+ if (value) setLocalStorageItem(key, "true");
+ else removeLocalStorageItem(key);
+ emitLocalStorageChange(key);
+ };
+ return [disabled, setDisabled] as const;
+}
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/layout.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/layout.test.tsx
index d854befa197..3b52a2eac33 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/layout.test.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/layout.test.tsx
@@ -1,5 +1,6 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { render, screen, waitFor } from "@testing-library/react";
+import { usePathname } from "next/navigation";
import { AuthProvider } from "@/contexts/AuthContext";
import Layout from "./layout";
@@ -10,7 +11,11 @@ let searchParamsValue = new URLSearchParams();
vi.mock("next/navigation", () => ({
useRouter: vi.fn(() => ({ push: vi.fn(), replace: replaceMock })),
useSearchParams: vi.fn(() => searchParamsValue),
- usePathname: vi.fn(() => "/ui/guardrails"),
+ usePathname: vi.fn(),
+}));
+
+vi.mock("@/components/liteadmin/LiteAdmin", () => ({
+ default: () => ,
}));
vi.mock("@/components/DashboardHeader", () => ({
@@ -79,8 +84,34 @@ describe("(dashboard) Layout", () => {
vi.clearAllMocks();
pendingUiConfig = createDeferred();
searchParamsValue = new URLSearchParams();
+ vi.mocked(usePathname).mockReturnValue("/ui/guardrails");
});
+ it.each(["/ui/playground", "/ui/playground/"])(
+ "hides LiteAdmin on %s and restores it after leaving Playground",
+ async (pathname) => {
+ const dashboard = () => (
+
+
+
+
+
+ );
+ const { rerender } = render(dashboard());
+ pendingUiConfig.resolve();
+ expect(await screen.findByRole("button", { name: "LiteAdmin" })).toBeInTheDocument();
+
+ vi.mocked(usePathname).mockReturnValue(pathname);
+ rerender(dashboard());
+ expect(screen.queryByRole("button", { name: "LiteAdmin" })).not.toBeInTheDocument();
+ expect(screen.getByTestId("page-content")).toBeInTheDocument();
+
+ vi.mocked(usePathname).mockReturnValue("/ui/api-keys");
+ rerender(dashboard());
+ expect(screen.getByRole("button", { name: "LiteAdmin" })).toBeInTheDocument();
+ },
+ );
+
it("does not mount route content until getUiConfig has resolved", async () => {
render(
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/layout.tsx b/ui/litellm-dashboard/src/app/(dashboard)/layout.tsx
index 03612cee3c6..406a323fbfb 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/layout.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/layout.tsx
@@ -15,7 +15,7 @@ import { LicenseExpiryBanner } from "@/components/LicenseExpiryBanner";
import { UserBanner } from "@/components/UserBanner";
import LiteAdmin from "@/components/liteadmin/LiteAdmin";
import { UpgradeBanner } from "@/components/UpgradeBanner";
-import { uiHref } from "@/utils/uiHref";
+import { routeSegmentForPathname, uiHref } from "@/utils/uiHref";
import { PluginModeProvider, usePluginMode } from "@/contexts/PluginModeContext";
import { createApiClient } from "@/lib/http/client";
import { getProxyBaseUrl } from "@/components/networking";
@@ -103,6 +103,7 @@ function DashboardShell({ children }: { children: React.ReactNode }) {
const { accessToken } = useAuth();
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
const { mode } = usePluginMode();
+ const isPlayground = routeSegmentForPathname(usePathname()) === "playground";
const isGateway = mode === "ai-gateway";
@@ -142,7 +143,7 @@ function DashboardShell({ children }: { children: React.ReactNode }) {
{children}
-
+ {!isPlayground && }
);
diff --git a/ui/litellm-dashboard/src/components/Navbar/UserDropdown/UserDropdown.tsx b/ui/litellm-dashboard/src/components/Navbar/UserDropdown/UserDropdown.tsx
index 9c02defc778..ab9a723e57a 100644
--- a/ui/litellm-dashboard/src/components/Navbar/UserDropdown/UserDropdown.tsx
+++ b/ui/litellm-dashboard/src/components/Navbar/UserDropdown/UserDropdown.tsx
@@ -1,6 +1,7 @@
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
import { useDisableBlogPosts } from "@/app/(dashboard)/hooks/useDisableBlogPosts";
import { useDisableBouncingIcon } from "@/app/(dashboard)/hooks/useDisableBouncingIcon";
+import { useDisableLiteAdmin } from "@/app/(dashboard)/hooks/useDisableLiteAdmin";
import { useDisableShowPrompts } from "@/app/(dashboard)/hooks/useDisableShowPrompts";
import {
emitLocalStorageChange,
@@ -10,6 +11,7 @@ import {
} from "@/utils/localStorageUtils";
import { navAccountDisplayName } from "@/components/Navbar/navDisplayName";
import { uiHref } from "@/utils/uiHref";
+import { isProxyAdminRole } from "@/utils/roles";
import { ChevronDown, ChevronsUpDown, Crown, KeyRound, LogOut, Mail, ShieldCheck, User } from "lucide-react";
import { useRouter } from "next/navigation";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
@@ -65,12 +67,22 @@ interface UserDropdownProps {
}
const UserDropdown: React.FC = ({ onLogout, variant = "navbar", collapsed = false }) => {
- const { userId, userEmail, userRoleLabel: userRole, premiumUser, loginMethod } = useAuthorized();
+ const {
+ userId,
+ userEmail,
+ userRole: role,
+ userRoleLabel: userRole,
+ isViewOnly,
+ premiumUser,
+ loginMethod,
+ } = useAuthorized();
const router = useRouter();
const [open, setOpen] = useState(false);
const disableShowPrompts = useDisableShowPrompts();
const disableBlogPosts = useDisableBlogPosts();
const disableBouncingIcon = useDisableBouncingIcon();
+ const [disableLiteAdmin, setDisableLiteAdmin] = useDisableLiteAdmin(userId);
+ const canUseLiteAdmin = userId && !isViewOnly && isProxyAdminRole(role);
const [disableShowNewBadge, setDisableShowNewBadge] = useState(false);
useEffect(() => {
@@ -192,6 +204,17 @@ const UserDropdown: React.FC = ({ onLogout, variant = "navbar
aria-label="Toggle hide bouncing icon"
/>
+ {canUseLiteAdmin && (
+
+ Hide LiteAdmin
+
+
+ )}
);
diff --git a/ui/litellm-dashboard/src/components/SidebarAccountMenu/SidebarAccountMenu.tsx b/ui/litellm-dashboard/src/components/SidebarAccountMenu/SidebarAccountMenu.tsx
index e697cc6f34a..e580b8610d0 100644
--- a/ui/litellm-dashboard/src/components/SidebarAccountMenu/SidebarAccountMenu.tsx
+++ b/ui/litellm-dashboard/src/components/SidebarAccountMenu/SidebarAccountMenu.tsx
@@ -2,6 +2,7 @@ import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
import { useHealthReadinessDetails } from "@/app/(dashboard)/hooks/healthReadiness/useHealthReadinessDetails";
import { useDisableBlogPosts } from "@/app/(dashboard)/hooks/useDisableBlogPosts";
import { useDisableBouncingIcon } from "@/app/(dashboard)/hooks/useDisableBouncingIcon";
+import { useDisableLiteAdmin } from "@/app/(dashboard)/hooks/useDisableLiteAdmin";
import { useDisableShowNewBadge } from "@/app/(dashboard)/hooks/useDisableShowNewBadge";
import { useDisableShowPrompts } from "@/app/(dashboard)/hooks/useDisableShowPrompts";
import { emitLocalStorageChange, removeLocalStorageItem, setLocalStorageItem } from "@/utils/localStorageUtils";
@@ -15,6 +16,7 @@ import { Separator } from "@/components/ui/separator";
import { Switch } from "@/components/ui/switch";
import { cn } from "@/lib/cva.config";
import { uiHref } from "@/utils/uiHref";
+import { isProxyAdminRole } from "@/utils/roles";
import { ChevronsUpDown, Crown, IdCard, KeyRound, LogOut, Mail, ShieldCheck } from "lucide-react";
import { useRouter } from "next/navigation";
import React from "react";
@@ -83,7 +85,16 @@ interface SidebarAccountMenuProps {
}
const SidebarAccountMenu: React.FC = ({ onLogout, collapsed = false }) => {
- const { userId, userEmail, userRoleLabel: userRole, premiumUser, accessToken, loginMethod } = useAuthorized();
+ const {
+ userId,
+ userEmail,
+ userRole: role,
+ userRoleLabel: userRole,
+ isViewOnly,
+ premiumUser,
+ accessToken,
+ loginMethod,
+ } = useAuthorized();
const router = useRouter();
const [open, setOpen] = React.useState(false);
const { data: healthData } = useHealthReadinessDetails(accessToken);
@@ -92,6 +103,8 @@ const SidebarAccountMenu: React.FC = ({ onLogout, colla
const disableBlogPosts = useDisableBlogPosts();
const disableBouncingIcon = useDisableBouncingIcon();
const disableShowNewBadge = useDisableShowNewBadge();
+ const [disableLiteAdmin, setDisableLiteAdmin] = useDisableLiteAdmin(userId);
+ const canUseLiteAdmin = userId && !isViewOnly && isProxyAdminRole(role);
const setFlag = (key: string, checked: boolean) => {
if (checked) {
@@ -235,6 +248,17 @@ const SidebarAccountMenu: React.FC = ({ onLogout, colla
/>
))}
+ {canUseLiteAdmin && (
+
+ Hide LiteAdmin
+
+
+ )}
diff --git a/ui/litellm-dashboard/src/components/liteadmin/LiteAdmin.integration.test.tsx b/ui/litellm-dashboard/src/components/liteadmin/LiteAdmin.integration.test.tsx
index 7031702c53e..eaee2d520ac 100644
--- a/ui/litellm-dashboard/src/components/liteadmin/LiteAdmin.integration.test.tsx
+++ b/ui/litellm-dashboard/src/components/liteadmin/LiteAdmin.integration.test.tsx
@@ -7,6 +7,9 @@ import { setGlobalLitellmHeaderName, switchToWorkerUrl } from "@/components/netw
import { Toaster } from "@/components/ui/sonner";
import { toast } from "@/lib/toast";
import userEvent from "@testing-library/user-event";
+import type { ComponentType } from "react";
+import SidebarAccountMenu from "@/components/SidebarAccountMenu/SidebarAccountMenu";
+import UserDropdown from "@/components/Navbar/UserDropdown/UserDropdown";
import LiteAdmin from "./LiteAdmin";
import { MAX_INPUT_LENGTH } from "./agent";
@@ -18,6 +21,7 @@ const { transport } = vi.hoisted(() => {
vi.unmock("@/app/(dashboard)/hooks/useAuthorized");
vi.unmock("@/lib/toast");
+vi.mock("next/navigation", () => ({ useRouter: () => ({ push: vi.fn() }) }));
const MANAGEMENT = "https://management.test/proxy";
const INFERENCE = "https://management.test/inference";
@@ -80,13 +84,14 @@ function SessionReady() {
return ;
}
-function renderWidget() {
+function renderWidget(Menu?: ComponentType<{ onLogout: () => void }>) {
const client = new QueryClient({ defaultOptions: { queries: { retry: false, gcTime: 0 } } });
const tree = () => (
+ {Menu &&
@@ -111,6 +116,7 @@ function gateway(replies: (ModelReply | Promise)[], options: Gateway
const path = new URL(request.url).pathname;
if (path.endsWith("/litellm-ui-config"))
return json({ proxy_base_url: MANAGEMENT, server_root_path: "", admin_ui_disabled: false });
+ if (path.endsWith("/health/readiness/details")) return json({ status: "healthy" });
if (path.endsWith("/sso/get/ui_settings")) {
if (typeof settings === "function") return settings(request);
return json({ PROXY_BASE_URL: MANAGEMENT, LITELLM_UI_API_DOC_BASE_URL: settings.target }, settings.status);
@@ -176,6 +182,79 @@ afterEach(() => {
});
describe("LiteAdmin in the gateway", () => {
+ it.each([
+ ["sidebar", SidebarAccountMenu],
+ ["navbar", UserDropdown],
+ ] as const)("persists Hide LiteAdmin from the %s account menu", async (_name, Menu) => {
+ gateway([]);
+ const user = userEvent.setup();
+ const view = renderWidget(Menu);
+ await screen.findByRole("button", { name: "LiteAdmin" });
+ await user.click(screen.getByRole("button", { name: /account menu/i }));
+ const toggle = await screen.findByRole("switch", { name: "Toggle hide LiteAdmin" });
+ expect(toggle).not.toBeChecked();
+ await user.click(toggle);
+ expect(toggle).toBeChecked();
+ expect(screen.queryByRole("button", { name: "LiteAdmin" })).not.toBeInTheDocument();
+
+ view.unmount();
+ const restored = renderWidget(Menu);
+ await screen.findByText("Session ready");
+ await waitFor(() => expect(restored.client.isFetching()).toBe(0));
+ expect(screen.queryByRole("button", { name: "LiteAdmin" })).not.toBeInTheDocument();
+ await user.click(screen.getByRole("button", { name: /account menu/i }));
+ const savedToggle = await screen.findByRole("switch", { name: "Toggle hide LiteAdmin" });
+ expect(savedToggle).toBeChecked();
+ await user.click(savedToggle);
+ expect(await screen.findByRole("button", { name: "LiteAdmin" })).toBeInTheDocument();
+ });
+
+ it("isolates Hide LiteAdmin by admin and gateway and reacts to another tab clearing it", async () => {
+ gateway([]);
+ const user = userEvent.setup();
+ const view = renderWidget(SidebarAccountMenu);
+ await screen.findByRole("button", { name: "LiteAdmin" });
+ await user.click(screen.getByRole("button", { name: /account menu/i }));
+ await user.click(await screen.findByRole("switch", { name: "Toggle hide LiteAdmin" }));
+
+ session("proxy_admin", "second-admin");
+ view.refresh();
+ expect(await screen.findByRole("button", { name: "LiteAdmin" })).toBeInTheDocument();
+ expect(screen.getByRole("switch", { name: "Toggle hide LiteAdmin" })).not.toBeChecked();
+ session();
+ view.refresh();
+ expect(screen.queryByRole("button", { name: "LiteAdmin" })).not.toBeInTheDocument();
+
+ switchToWorkerUrl("https://other-gateway.test");
+ view.refresh();
+ expect(await screen.findByRole("button", { name: "LiteAdmin" })).toBeInTheDocument();
+ expect(screen.getByRole("switch", { name: "Toggle hide LiteAdmin" })).not.toBeChecked();
+ switchToWorkerUrl(MANAGEMENT);
+ view.refresh();
+ expect(screen.queryByRole("button", { name: "LiteAdmin" })).not.toBeInTheDocument();
+
+ act(() => {
+ localStorage.clear();
+ window.dispatchEvent(new StorageEvent("storage", { key: null }));
+ });
+ expect(await screen.findByRole("button", { name: "LiteAdmin" })).toBeInTheDocument();
+ expect(screen.getByRole("switch", { name: "Toggle hide LiteAdmin" })).not.toBeChecked();
+ });
+
+ it.each([
+ ["sidebar", SidebarAccountMenu],
+ ["navbar", UserDropdown],
+ ] as const)("does not offer Hide LiteAdmin to a view-only admin in the %s menu", async (_name, Menu) => {
+ session("proxy_admin_viewer");
+ gateway([]);
+ renderWidget(Menu);
+ await screen.findByText("Session ready");
+ const user = userEvent.setup();
+ await user.click(screen.getByRole("button", { name: /account menu/i }));
+ expect(await screen.findByRole("switch", { name: "Toggle hide all prompts" })).toBeInTheDocument();
+ expect(screen.queryByRole("switch", { name: "Toggle hide LiteAdmin" })).not.toBeInTheDocument();
+ });
+
it.each(["proxy_admin_viewer", "internal_user", "internal_user_viewer", "org_admin"])(
"does not expose operations to %s",
async (role) => {
diff --git a/ui/litellm-dashboard/src/components/liteadmin/LiteAdmin.tsx b/ui/litellm-dashboard/src/components/liteadmin/LiteAdmin.tsx
index 0395ed99fb3..8092eb2f5e6 100644
--- a/ui/litellm-dashboard/src/components/liteadmin/LiteAdmin.tsx
+++ b/ui/litellm-dashboard/src/components/liteadmin/LiteAdmin.tsx
@@ -4,6 +4,7 @@ import { useRef, useState, type ReactNode } from "react";
import { useQuery } from "@tanstack/react-query";
import { RotateCcw, Sparkles, X } from "lucide-react";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
+import { useDisableLiteAdmin } from "@/app/(dashboard)/hooks/useDisableLiteAdmin";
import { useProxySettingsQuery } from "@/app/(dashboard)/hooks/proxySettings/useProxySettings";
import { ChatComposer } from "@/app/(dashboard)/playground/components/chat_ui/ChatComposer";
import { EndpointType, isModeCompatibleWithEndpoint } from "@/components/chat_ui/mode_endpoint_mapping";
@@ -34,9 +35,10 @@ type ManagementSession = Omit;
export default function LiteAdmin() {
const auth = useAuthorized();
+ const [disabled] = useDisableLiteAdmin(auth.userId);
const sessionReady = !auth.isLoading && auth.isAuthorized;
const writableAdmin = !auth.isViewOnly && isProxyAdminRole(auth.userRole);
- const allowed = sessionReady && writableAdmin;
+ const allowed = sessionReady && writableAdmin && !disabled;
if (!allowed || !auth.token || !auth.accessToken) return null;
const session = { token: auth.token, accessToken: auth.accessToken, managementBaseUrl: getProxyBaseUrl() };
return (