From c95f57f095b53e48762791cbc7baf90f3ad00b29 Mon Sep 17 00:00:00 2001 From: oss-agent-shin <279349115+oss-agent-shin@users.noreply.github.com> Date: Wed, 6 May 2026 03:06:41 +0000 Subject: [PATCH] Allow internal users to view tag usage Co-authored-by: ishaan-berri --- litellm/proxy/_types.py | 7 +++- .../proxy/auth/test_route_checks.py | 37 +++++++++++++++++++ .../components/UsagePageView.test.tsx | 23 +++++++++++- .../UsagePage/components/UsagePageView.tsx | 10 ++++- .../UsageViewSelect/UsageViewSelect.test.tsx | 12 ++++++ .../UsageViewSelect/UsageViewSelect.tsx | 5 +++ 6 files changed, 89 insertions(+), 5 deletions(-) diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index c6653a722d6..172d870f7fd 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -672,12 +672,17 @@ class LiteLLMRoutes(enum.Enum): "/v2/guardrails/list", "/project/list", "/project/info", + "/tag/daily/activity", + "/tag/list", ] + spend_tracking_routes + key_management_routes ) - internal_user_view_only_routes = spend_tracking_routes + internal_user_view_only_routes = spend_tracking_routes + [ + "/tag/daily/activity", + "/tag/list", + ] self_managed_routes = [ "/team/member_add", diff --git a/tests/test_litellm/proxy/auth/test_route_checks.py b/tests/test_litellm/proxy/auth/test_route_checks.py index cf6feabf85f..4dee0ec7718 100644 --- a/tests/test_litellm/proxy/auth/test_route_checks.py +++ b/tests/test_litellm/proxy/auth/test_route_checks.py @@ -53,6 +53,43 @@ def test_non_admin_config_update_route_rejected(): assert "Your role=internal_user" in str(exc_info.value) +@pytest.mark.parametrize( + "role", + [ + LitellmUserRoles.INTERNAL_USER.value, + LitellmUserRoles.INTERNAL_USER_VIEW_ONLY.value, + ], +) +@pytest.mark.parametrize("route", ["/tag/list", "/tag/daily/activity"]) +def test_internal_users_can_access_tag_usage_read_routes(role, route): + """ + Internal users should be able to load the UI's tag usage view. + """ + user_obj = LiteLLM_UserTable( + user_id="internal_user", + user_email="user@example.com", + user_role=role, + ) + valid_token = UserAPIKeyAuth( + user_id="internal_user", + user_role=role, + ) + request = MagicMock(spec=Request) + request.query_params = {} + + try: + RouteChecks.non_proxy_admin_allowed_routes_check( + user_obj=user_obj, + _user_role=role, + route=route, + request=request, + valid_token=valid_token, + request_data={}, + ) + except Exception as e: + pytest.fail(f"{role} should be able to access {route}. Got error: {str(e)}") + + def test_proxy_admin_viewer_config_update_route_rejected(): """Test that proxy admin viewer users are rejected when trying to call /config/update""" diff --git a/ui/litellm-dashboard/src/components/UsagePage/components/UsagePageView.test.tsx b/ui/litellm-dashboard/src/components/UsagePage/components/UsagePageView.test.tsx index bbcddd572cd..bd4fbe938c6 100644 --- a/ui/litellm-dashboard/src/components/UsagePage/components/UsagePageView.test.tsx +++ b/ui/litellm-dashboard/src/components/UsagePage/components/UsagePageView.test.tsx @@ -57,7 +57,7 @@ vi.mock("./EndpointUsage/EndpointUsage", () => ({ vi.mock("./UsageViewSelect/UsageViewSelect", async () => { const React = await import("react"); - const UsageViewSelect = ({ value, onChange }: any) => { + const UsageViewSelect = ({ value, onChange, canViewTagUsage }: any) => { return React.createElement( "select", { @@ -70,7 +70,7 @@ vi.mock("./UsageViewSelect/UsageViewSelect", async () => { React.createElement("option", { value: "team" }, "Team Usage"), React.createElement("option", { value: "organization" }, "Organization Usage"), React.createElement("option", { value: "customer" }, "Customer Usage"), - React.createElement("option", { value: "tag" }, "Tag Usage"), + canViewTagUsage && React.createElement("option", { value: "tag" }, "Tag Usage"), React.createElement("option", { value: "agent" }, "Agent Usage"), React.createElement("option", { value: "user-agent-activity" }, "User Agent Activity"), ); @@ -808,6 +808,25 @@ describe("UsagePage", () => { }); describe("non-admin user behavior", () => { + it("should render tag usage option for internal users", async () => { + mockUseAuthorized.mockReturnValue({ + isLoading: false, + isAuthorized: true, + token: "mock-token", + accessToken: "test-token", + userId: "user-123", + userEmail: "test@example.com", + userRole: "Internal User", + premiumUser: false, + disabledPersonalKeyCreation: false, + showSSOBanner: false, + }); + + renderWithProviders(); + + expect(screen.getByRole("option", { name: "Tag Usage" })).toBeInTheDocument(); + }); + it("should not render user selector for non-admin users", async () => { mockUseAuthorized.mockReturnValue({ isLoading: false, diff --git a/ui/litellm-dashboard/src/components/UsagePage/components/UsagePageView.tsx b/ui/litellm-dashboard/src/components/UsagePage/components/UsagePageView.tsx index 809f1d4e17b..60a24c1e7df 100644 --- a/ui/litellm-dashboard/src/components/UsagePage/components/UsagePageView.tsx +++ b/ui/litellm-dashboard/src/components/UsagePage/components/UsagePageView.tsx @@ -31,7 +31,7 @@ import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; import { useCurrentUser } from "@/app/(dashboard)/hooks/users/useCurrentUser"; import { useInfiniteUsers } from "@/app/(dashboard)/hooks/users/useUsers"; import { formatNumberWithCommas } from "@/utils/dataUtils"; -import { all_admin_roles } from "../../../utils/roles"; +import { all_admin_roles, internalUserRoles } from "../../../utils/roles"; import { ActivityMetrics, processActivityData } from "../../activity_metrics"; import CloudZeroExportModal from "../../cloudzero_export_modal"; import EntityUsageExportModal from "../../EntityUsageExport"; @@ -84,6 +84,7 @@ const UsagePage: React.FC = ({ teams, organizations }) => { console.log(`currentUser: ${JSON.stringify(currentUser)}`); console.log(`currentUser max budget: ${currentUser?.max_budget}`); const isAdmin = all_admin_roles.includes(userRole || ""); + const canViewTagUsage = isAdmin || internalUserRoles.includes(userRole || ""); // Debounced search for user selector const [userSearchInput, setUserSearchInput] = useState(""); @@ -437,7 +438,12 @@ const UsagePage: React.FC = ({ teams, organizations }) => {
- setUsageView(value)} isAdmin={isAdmin} /> + setUsageView(value)} + isAdmin={isAdmin} + canViewTagUsage={canViewTagUsage} + />
{paginatedResult.isFetchingMore && ( diff --git a/ui/litellm-dashboard/src/components/UsagePage/components/UsageViewSelect/UsageViewSelect.test.tsx b/ui/litellm-dashboard/src/components/UsagePage/components/UsageViewSelect/UsageViewSelect.test.tsx index 7bb80b424b5..ed69dd26ec2 100644 --- a/ui/litellm-dashboard/src/components/UsagePage/components/UsageViewSelect/UsageViewSelect.test.tsx +++ b/ui/litellm-dashboard/src/components/UsagePage/components/UsageViewSelect/UsageViewSelect.test.tsx @@ -110,4 +110,16 @@ describe("UsageViewSelect", () => { expect(mockOnChange).toHaveBeenCalledWith("team"); }); + + it("should show tag usage when tag usage access is allowed", () => { + render(); + + expect(screen.getByRole("option", { name: "Tag Usage" })).toBeInTheDocument(); + }); + + it("should hide tag usage for non-admins without tag usage access", () => { + render(); + + expect(screen.queryByRole("option", { name: "Tag Usage" })).not.toBeInTheDocument(); + }); }); diff --git a/ui/litellm-dashboard/src/components/UsagePage/components/UsageViewSelect/UsageViewSelect.tsx b/ui/litellm-dashboard/src/components/UsagePage/components/UsageViewSelect/UsageViewSelect.tsx index 11fdb8a7cf5..ba2c7daf655 100644 --- a/ui/litellm-dashboard/src/components/UsagePage/components/UsageViewSelect/UsageViewSelect.tsx +++ b/ui/litellm-dashboard/src/components/UsagePage/components/UsageViewSelect/UsageViewSelect.tsx @@ -16,6 +16,7 @@ export interface UsageViewSelectProps { value: UsageOption; onChange: (value: UsageOption) => void; isAdmin: boolean; + canViewTagUsage?: boolean; title?: string; description?: string; "data-id"?: string; @@ -106,12 +107,16 @@ export const UsageViewSelect: React.FC = ({ value, onChange, isAdmin, + canViewTagUsage = false, title = "Usage View", description = "Select the usage data you want to view", "data-id": dataId, }) => { const getFilteredOptions = () => { return OPTIONS.filter((option) => { + if (option.value === "tag") { + return isAdmin || canViewTagUsage; + } if (option.adminOnly && !isAdmin) { return false; }