({ 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", () => ({
@@ -25,7 +26,11 @@ vi.mock("@/components/model_info_view", () => ({
default: ({ modelId }: { modelId: string }) =>
model:{modelId}
,
}));
vi.mock("@/components/team/TeamInfo", () => ({
- default: ({ teamId }: { teamId: string }) =>
team:{teamId}
,
+ default: ({ teamId, is_team_admin }: { teamId: string; is_team_admin: boolean }) => (
+
+ team:{teamId}
+
+ ),
}));
const mockUseAuthorized = vi.fn();
@@ -95,10 +100,19 @@ describe("ModelsAndEndpointsPage", () => {
expect(screen.queryByRole("tab", { name: "All Models" })).not.toBeInTheDocument();
});
- it("renders the team detail overlay from the ?team drill-in", () => {
+ it("renders the team detail overlay from the ?team drill-in with admin edit rights", () => {
detailState.teamId = "team-9";
renderPage();
expect(screen.getByTestId("team-info")).toHaveTextContent("team:team-9");
+ expect(screen.getByTestId("team-info")).toHaveAttribute("data-team-admin", "true");
+ });
+
+ it("opens the ?team drill-in without edit rights for a view-only admin", () => {
+ mockUseAuthorized.mockReturnValue(VIEW_ONLY_ADMIN);
+ detailState.teamId = "team-9";
+ renderPage();
+ expect(screen.getByTestId("team-info")).toHaveTextContent("team:team-9");
+ expect(screen.getByTestId("team-info")).toHaveAttribute("data-team-admin", "false");
});
it("hides admin-only tabs for a non-admin user", () => {
@@ -108,6 +122,35 @@ describe("ModelsAndEndpointsPage", () => {
expect(screen.queryByRole("tab", { name: "Health Status" })).not.toBeInTheDocument();
});
+ it("keeps the full admin tab order for a real admin", () => {
+ renderPage();
+ expect(screen.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);
+ renderPage();
+ expect(screen.getByRole("tab", { name: "All Models" })).toBeInTheDocument();
+ expect(screen.getByRole("tab", { name: "Health Status" })).toBeInTheDocument();
+ expect(screen.queryByRole("tab", { name: "LLM Credentials" })).not.toBeInTheDocument();
+ expect(screen.queryByRole("tab", { name: "Pass-Through Endpoints" })).not.toBeInTheDocument();
+ expect(screen.queryByRole("tab", { name: "Model Retry Settings" })).not.toBeInTheDocument();
+ expect(screen.queryByRole("tab", { name: "Model Group Alias" })).not.toBeInTheDocument();
+ expect(screen.queryByRole("tab", { name: /Model Access Group Budgets/ })).not.toBeInTheDocument();
+ expect(screen.queryByRole("tab", { name: "Price Data Reload" })).not.toBeInTheDocument();
+ });
+
// POST /model/new 403s a proxy_admin_viewer, so the form's tab must not render for one.
it("hides the Add Model tab for a view-only admin session", () => {
mockUseAuthorized.mockReturnValue(VIEW_ONLY_ADMIN);
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 34c9d87004e..4d6a90fc56e 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
@@ -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";
@@ -148,7 +145,7 @@ export default function ModelsAndEndpointsPage() {
teamId={teamId}
onClose={close}
accessToken={accessToken}
- is_team_admin={userRole === "Admin"}
+ is_team_admin={userRole === "Admin" && !isViewOnly}
is_proxy_admin={userRole === "Proxy Admin"}
userModels={allModelsOnProxy}
editTeam={false}