- Add a decision model{source} to use this guardrail. A model shows up here once its mode is evaluation.{" "}
+ Add a decision model{source} to use this guardrail. A model shows up here once its mode is decisions.{" "}
Add a model
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/home/_components/HomePage.integration.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/home/_components/HomePage.integration.test.tsx
index a17a18beee5..4e87cd06f7a 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/home/_components/HomePage.integration.test.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/home/_components/HomePage.integration.test.tsx
@@ -5,6 +5,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { DailyActivityAggregatedResponse } from "@/components/UsagePage/dailyActivityApi";
import { EMPTY_DAILY_ACTIVITY_METADATA } from "@/components/UsagePage/dailyActivityApi";
import { all_admin_roles } from "@/utils/roles";
+import { LiteAdminFrame } from "@/components/liteadmin/LiteAdmin";
import HomePage from "./HomePage";
const auth = vi.hoisted(() => ({
@@ -12,6 +13,10 @@ const auth = vi.hoisted(() => ({
userRole: "",
userId: "u1",
isViewOnly: false,
+ isAuthorized: true,
+ isLoading: false,
+ premiumUser: true,
+ token: "test-token",
}));
vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({ default: () => auth }));
@@ -46,6 +51,12 @@ const requestUrl = (input: RequestInfo | URL): string => {
};
const fakeFetch = (input: RequestInfo | URL) => {
+ if (requestUrl(input).includes("/model_group/info")) {
+ return Promise.resolve(Response.json({ data: [{ model_group: "claude-sonnet-5-5", mode: "chat" }] }));
+ }
+ if (requestUrl(input).endsWith("/sso/get/ui_settings")) {
+ return Promise.resolve(Response.json({ PROXY_BASE_URL: "http://proxy.test" }));
+ }
const body = requestUrl(input).endsWith("/public/whats_new") ? whatsNew : blogPosts;
return Promise.resolve(
new Response(JSON.stringify(body), { status: 200, headers: { "Content-Type": "application/json" } }),
@@ -133,6 +144,20 @@ describe("HomePage", () => {
expect(screen.queryByRole("link", { name: /create new virtual key/i })).not.toBeInTheDocument();
});
+ it("offers LiteAdmin on Home inside the dashboard frame", async () => {
+ render(
+
+
+
+
+ ,
+ );
+ const entry = screen.getByRole("region", { name: "Ask LiteAdmin" });
+ expect(await within(entry).findByPlaceholderText("Ask LiteAdmin…")).toBeInTheDocument();
+ expect(within(entry).getByRole("button", { name: "Send message" })).toBeDisabled();
+ expect(await screen.findByText("$501.74")).toBeInTheDocument();
+ });
+
it("shows an error with a retry instead of zero totals when the usage request fails", async () => {
network.aggregated.mockRejectedValueOnce(new Error("boom"));
renderHome();
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/home/_components/HomePage.tsx b/ui/litellm-dashboard/src/app/(dashboard)/home/_components/HomePage.tsx
index bcae30795c6..dcb0cd771b6 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/home/_components/HomePage.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/home/_components/HomePage.tsx
@@ -8,6 +8,7 @@ import { Skeleton } from "@/components/ui/skeleton";
import { Page } from "@/components/shared/Page";
import { PageHeader, PageHeaderControls, PageHeaderTitle } from "@/components/shared/PageHeader";
import { uiHref } from "@/utils/uiHref";
+import { LiteAdminHome } from "@/components/liteadmin/LiteAdmin";
import { Leaderboard, useBreakdown } from "@/app/(dashboard)/usage/_components/components/overview/BreakdownChart";
import { Panel } from "@/app/(dashboard)/usage/_components/components/overview/Primitives";
import UsageStatStrip from "@/app/(dashboard)/usage/_components/components/overview/UsageStatStrip";
@@ -38,6 +39,8 @@ export default function HomePage() {
)}
+
+
{usage.failed ? (
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/panels/ModelGroupAliasPanel.integration.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/panels/ModelGroupAliasPanel.integration.test.tsx
new file mode 100644
index 00000000000..227a34335a3
--- /dev/null
+++ b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/panels/ModelGroupAliasPanel.integration.test.tsx
@@ -0,0 +1,94 @@
+/* @vitest-environment jsdom */
+import { render, screen, waitFor } from "@testing-library/react";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+import { getCallbacksCall, getRouterSettingsCall } from "@/components/networking";
+import ModelGroupAliasPanel from "./ModelGroupAliasPanel";
+
+vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({
+ default: () => ({ accessToken: "test-token", userId: "user-1", userRole: "Admin" }),
+}));
+
+vi.mock("@/components/networking", () => ({
+ getCallbacksCall: vi.fn(),
+ getRouterSettingsCall: vi.fn(),
+ setCallbacksCall: vi.fn(),
+}));
+
+const aliases = {
+ "fast-model": "claude-haiku",
+ "smart-model": { model: "claude-sonnet", hidden: true },
+};
+
+describe("ModelGroupAliasPanel", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ vi.mocked(getCallbacksCall).mockResolvedValue({ router_settings: { model_group_alias: aliases } });
+ vi.mocked(getRouterSettingsCall).mockResolvedValue({ source: { model_group_alias: "db" } });
+ });
+
+ afterEach(() => {
+ vi.restoreAllMocks();
+ });
+
+ it("locks delete controls when aliases are config-owned", async () => {
+ vi.mocked(getRouterSettingsCall).mockResolvedValue({ source: { model_group_alias: "config" } });
+
+ render(
);
+
+ expect(
+ await screen.findByText(
+ "These aliases are defined in config.yaml and are read-only here. Edit the config file to change them.",
+ ),
+ ).toBeInTheDocument();
+ expect(await screen.findByText("fast-model")).toBeInTheDocument();
+ expect(screen.getByRole("button", { name: "Delete alias fast-model" })).toBeDisabled();
+ expect(screen.getByRole("button", { name: "Delete alias smart-model" })).toBeDisabled();
+ });
+
+ it("keeps aliases locked while ownership is loading", async () => {
+ let resolveOwnership!: (value: Awaited
>) => void;
+ const pendingOwnership = new Promise>>((resolve) => {
+ resolveOwnership = resolve;
+ });
+ vi.mocked(getRouterSettingsCall).mockReturnValue(pendingOwnership);
+
+ render();
+
+ expect(await screen.findByText("fast-model")).toBeInTheDocument();
+ expect(screen.getByRole("button", { name: "Delete alias fast-model" })).toBeDisabled();
+
+ resolveOwnership({ source: { model_group_alias: "db" } });
+ await waitFor(() => expect(screen.getByRole("button", { name: "Delete alias fast-model" })).toBeEnabled());
+ });
+
+ it("leaves delete controls enabled when aliases are database-managed", async () => {
+ render();
+
+ await waitFor(() => expect(screen.getByRole("button", { name: "Delete alias fast-model" })).toBeEnabled());
+ expect(screen.getByRole("button", { name: "Delete alias smart-model" })).toBeEnabled();
+ });
+
+ it("renders aliases with enabled controls when router settings cannot be fetched", async () => {
+ vi.spyOn(console, "error").mockImplementation(() => {});
+ vi.mocked(getRouterSettingsCall).mockRejectedValue(new Error("router settings unavailable"));
+
+ render();
+
+ expect(await screen.findByText("fast-model")).toBeInTheDocument();
+ await waitFor(() => expect(screen.getByRole("button", { name: "Delete alias fast-model" })).toBeEnabled());
+ });
+
+ it("still shows the config-owned notice when callbacks cannot be fetched", async () => {
+ vi.spyOn(console, "error").mockImplementation(() => {});
+ vi.mocked(getCallbacksCall).mockRejectedValue(new Error("callbacks unavailable"));
+ vi.mocked(getRouterSettingsCall).mockResolvedValue({ source: { model_group_alias: "config" } });
+
+ render();
+
+ expect(
+ await screen.findByText(
+ "These aliases are defined in config.yaml and are read-only here. Edit the config file to change them.",
+ ),
+ ).toBeInTheDocument();
+ });
+});
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/panels/ModelGroupAliasPanel.tsx b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/panels/ModelGroupAliasPanel.tsx
index ec808984c34..025923e08e9 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/panels/ModelGroupAliasPanel.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/panels/ModelGroupAliasPanel.tsx
@@ -2,28 +2,59 @@
import { useEffect, useState } from "react";
import ModelGroupAliasSettings from "@/components/model_group_alias_settings";
-import { getCallbacksCall } from "@/components/networking";
+import { getCallbacksCall, getRouterSettingsCall } from "@/components/networking";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
export default function ModelGroupAliasPanel() {
const { accessToken, userId: userID, userRole } = useAuthorized();
const [modelGroupAlias, setModelGroupAlias] = useState<{ [key: string]: string }>({});
+ const [aliasOwnership, setAliasOwnership] = useState<{
+ accessToken: string;
+ userID: typeof userID;
+ userRole: typeof userRole;
+ managedByConfig: boolean;
+ } | null>(null);
+ const currentAliasOwnership =
+ aliasOwnership?.accessToken === accessToken &&
+ aliasOwnership?.userID === userID &&
+ aliasOwnership?.userRole === userRole
+ ? aliasOwnership
+ : null;
+ const managedByConfig = currentAliasOwnership?.managedByConfig ?? false;
+ const ownershipLoading = Boolean(accessToken && !currentAliasOwnership);
useEffect(() => {
- if (!accessToken || !userID || !userRole) {
+ if (!accessToken) {
return;
}
let active = true;
- void (async () => {
- try {
- const info = await getCallbacksCall(accessToken, userID, userRole);
+ const updateAliasOwnership = (managedByConfig: boolean) => {
+ const ownership = { accessToken, userID, userRole, managedByConfig };
+ setAliasOwnership(ownership);
+ };
+ if (userID && userRole) {
+ void getCallbacksCall(accessToken, userID, userRole)
+ .then((info) => {
+ if (active) {
+ setModelGroupAlias(info.router_settings?.model_group_alias || {});
+ }
+ })
+ .catch((error) => {
+ console.error("Error fetching model group alias:", error);
+ });
+ }
+ void getRouterSettingsCall(accessToken)
+ .then((data) => {
if (active) {
- setModelGroupAlias(info.router_settings?.model_group_alias || {});
+ updateAliasOwnership(data?.source?.model_group_alias === "config");
}
- } catch (error) {
- console.error("Error fetching model group alias:", error);
- }
- })();
+ })
+ .catch((error) => {
+ if (active) {
+ updateAliasOwnership(false);
+ }
+ console.error("Error fetching router settings:", error);
+ });
return () => {
active = false;
};
@@ -34,6 +65,8 @@ export default function ModelGroupAliasPanel() {
accessToken={accessToken}
initialModelGroupAlias={modelGroupAlias}
onAliasUpdate={setModelGroupAlias}
+ managedByConfig={managedByConfig}
+ ownershipLoading={ownershipLoading}
/>
);
}
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/CurlPreview.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/CurlPreview.tsx
index ecefd87933b..583486a3d81 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/CurlPreview.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/CurlPreview.tsx
@@ -8,7 +8,7 @@ interface CurlPreviewProps extends CurlCommandInput {
export default function CurlPreview({ errorCount = 0, ...input }: CurlPreviewProps) {
const command = buildCurlCommand(input);
return (
-
+
cURL request
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/DecisionFormControls.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/DecisionFormControls.tsx
index afa88bf7051..dee45f59c11 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/DecisionFormControls.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/DecisionFormControls.tsx
@@ -1,8 +1,7 @@
import { Button } from "@/components/ui/button";
-import { cn } from "@/lib/cva.config";
import { Plus, Trash2 } from "lucide-react";
import { useId, type ReactNode } from "react";
-import { IssueList, ValidationStatus } from "./ValidationFeedback";
+import { IssueList } from "./ValidationFeedback";
import type { PayloadValidation } from "./lib/validatePayload";
export function RemoveButton({ label, onClick }: { label: string; onClick: () => void }) {
@@ -15,28 +14,40 @@ export function RemoveButton({ label, onClick }: { label: string; onClick: () =>
export function AddButton({ children, onClick }: { children: ReactNode; onClick: () => void }) {
return (
-