diff --git a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/page.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/page.test.tsx
index 521f89a39f2..a53e8167636 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/page.test.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/page.test.tsx
@@ -14,6 +14,7 @@ vi.mock("./panels/HealthStatusPanel", () => ({ default: () =>
({ default: () =>
}));
vi.mock("./panels/ModelGroupAliasPanel", () => ({ default: () =>
}));
vi.mock("./panels/PriceDataPanel", () => ({ default: () =>
}));
+vi.mock("./panels/AccessGroupBudgetsPanel", () => ({ default: () =>
}));
const detailState = { modelId: null as string | null, teamId: null as string | null };
vi.mock("./detailNavigation", () => ({
@@ -38,8 +39,18 @@ vi.mock("./useModelDashboardData", () => ({
useModelDashboardData: () => ({ availableModelAccessGroups: [], allModelsOnProxy: [], availableModelGroups: [] }),
}));
-const ADMIN = { accessToken: "at", token: "t", userRole: "Admin", userId: "u1", premiumUser: false };
-const NON_ADMIN = { accessToken: "at", token: "t", userRole: "Internal User", userId: "u1", premiumUser: false };
+const ADMIN = { accessToken: "at", token: "t", userRole: "Admin", userId: "u1", premiumUser: false, isViewOnly: false };
+const NON_ADMIN = {
+ accessToken: "at",
+ token: "t",
+ userRole: "Internal User",
+ userId: "u1",
+ premiumUser: false,
+ isViewOnly: false,
+};
+// What useAuthorized returns for a proxy_admin_viewer session: effectiveSessionRole masquerades
+// the role as "Admin" for read parity, and only isViewOnly tells the page it may not write.
+const VIEW_ONLY_ADMIN = { ...ADMIN, isViewOnly: true };
const renderPage = () => {
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false, gcTime: 0 } } });
@@ -99,6 +110,35 @@ describe("ModelsAndEndpointsPage", () => {
expect(queryByRole("tab", { name: "Health Status" })).not.toBeInTheDocument();
});
+ it("keeps the full admin tab order for a real admin", () => {
+ const { getAllByRole } = renderPage();
+ expect(getAllByRole("tab").map((tab) => tab.textContent)).toEqual([
+ "All Models",
+ "Add Model",
+ "Auto-Routers Beta",
+ "LLM Credentials",
+ "Pass-Through Endpoints",
+ "Health Status",
+ "Model Retry Settings",
+ "Model Group Alias",
+ "Model Access Group Budgets Beta",
+ "Price Data Reload",
+ ]);
+ });
+
+ it("hides the admin write-form tabs from a view-only admin, keeping the read views", () => {
+ mockUseAuthorized.mockReturnValue(VIEW_ONLY_ADMIN);
+ const { getByRole, queryByRole } = renderPage();
+ expect(getByRole("tab", { name: "All Models" })).toBeInTheDocument();
+ expect(getByRole("tab", { name: "Health Status" })).toBeInTheDocument();
+ expect(queryByRole("tab", { name: "LLM Credentials" })).not.toBeInTheDocument();
+ expect(queryByRole("tab", { name: "Pass-Through Endpoints" })).not.toBeInTheDocument();
+ expect(queryByRole("tab", { name: "Model Retry Settings" })).not.toBeInTheDocument();
+ expect(queryByRole("tab", { name: "Model Group Alias" })).not.toBeInTheDocument();
+ expect(queryByRole("tab", { name: /Model Access Group Budgets/ })).not.toBeInTheDocument();
+ expect(queryByRole("tab", { name: "Price Data Reload" })).not.toBeInTheDocument();
+ });
+
// Auto-routers are excluded from the All Models table, so this tab is their home: the only
// place in the product to list, create, edit or delete one.
describe("Auto-Routers tab", () => {
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/page.tsx b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/page.tsx
index 9ae7dc12f81..c7455038cb9 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/page.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/page.tsx
@@ -80,7 +80,7 @@ const renderPanel = (key: string) => {
};
export default function ModelsAndEndpointsPage() {
- const { accessToken, userRole, userId: userID, premiumUser } = useAuthorized();
+ const { accessToken, userRole, userId: userID, premiumUser, isViewOnly } = useAuthorized();
const { data: teams } = useTeams();
const { data: uiSettings } = useUISettings();
const queryClient = useQueryClient();
@@ -106,19 +106,16 @@ export default function ModelsAndEndpointsPage() {
"",
...(canCreate ? (["add"] as const) : []),
...(isAdmin || canCreate ? (["auto-routers"] as const) : []),
- ...(isAdmin
- ? ([
- "llm-credentials",
- "pass-through",
- "health",
- "retry-settings",
- "model-group-alias",
- "access-group-budgets",
- "price-data",
- ] as const)
+ // effectiveSessionRole reports proxy_admin_viewer as "Admin", so isAdmin alone would show a
+ // viewer these write-only panels; only the raw-role isViewOnly separates them. Health Status
+ // stays: it is the bucket's one read view, and viewers keep read parity with admins.
+ ...(isAdmin && !isViewOnly ? (["llm-credentials", "pass-through"] as const) : []),
+ ...(isAdmin ? (["health"] as const) : []),
+ ...(isAdmin && !isViewOnly
+ ? (["retry-settings", "model-group-alias", "access-group-budgets", "price-data"] as const)
: []),
],
- [canCreate, isAdmin],
+ [canCreate, isAdmin, isViewOnly],
);
const allModelsLabel = isAdmin ? "All Models" : "Your Models";