diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/EntityUsage.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/EntityUsage.test.tsx
index 2a6c2ede478..54e3c55e7a5 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/EntityUsage.test.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/EntityUsage.test.tsx
@@ -44,10 +44,12 @@ vi.mock("../EndpointUsage/EndpointUsage", () => ({
}));
vi.mock("@/components/UsagePage/components/EntityUsage/TopKeyView", () => ({
- default: ({ topKeys }: { topKeys: { api_key: string; spend: number }[] }) => (
+ default: ({ topKeys }: { topKeys: { api_key: string; user_email: string | null; spend: number }[] }) => (
Top Keys
- {`top-keys:${topKeys.map((row) => `${row.api_key}=${row.spend}`).join("|")}`}
+
+ {`top-keys:${topKeys.map((row) => `${row.api_key}=${row.spend}=${row.user_email ?? "-"}`).join("|")}`}
+
),
}));
@@ -1082,7 +1084,12 @@ describe("EntityUsage", () => {
breakdown: {
...mockSpendData.results[0].breakdown,
model_groups: { "gpt-4o": { metrics: { ...usageMetrics, spend: 70.25 }, metadata: {} } },
- api_keys: { "sk-abc": { metrics: usageMetrics, metadata: { key_alias: "prod-key", team_id: null } } },
+ api_keys: {
+ "sk-abc": {
+ metrics: usageMetrics,
+ metadata: { key_alias: "prod-key", team_id: null, user_email: "alice@example.com" },
+ },
+ },
},
},
],
@@ -1091,7 +1098,7 @@ describe("EntityUsage", () => {
render();
await waitFor(() => {
- expect(screen.getByText("top-keys:sk-abc=30.75")).toBeInTheDocument();
+ expect(screen.getByText("top-keys:sk-abc=30.75=alice@example.com")).toBeInTheDocument();
});
expect(screen.getByText("top-models:gpt-4o=70.25")).toBeInTheDocument();
expect(screen.getByText(/^top-models:Code Review Agent=/)).toBeInTheDocument();
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/entityUsageAggregations.ts b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/entityUsageAggregations.ts
index d482a5576ae..60b9c2b8e4d 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/entityUsageAggregations.ts
+++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/entityUsageAggregations.ts
@@ -1,4 +1,5 @@
import { keyActivityLabel } from "@/components/UsagePage/keyActivityLabel";
+import type { TopKeyItem } from "@/components/UsagePage/components/EntityUsage/TopKeyView";
import { BreakdownMetrics, DailyData, KeyMetricWithMetadata, TagUsage } from "@/components/UsagePage/types";
export type ExtendedDailyData = DailyData & {
@@ -85,7 +86,7 @@ export const getTopAgents = (results: ExtendedDailyData[], topAgentsLimit: numbe
.slice(0, topAgentsLimit);
};
-export const getTopAPIKeys = (results: ExtendedDailyData[], topKeysLimit: number) => {
+export const getTopAPIKeys = (results: ExtendedDailyData[], topKeysLimit: number): TopKeyItem[] => {
const keySpend: { [key: string]: KeyMetricWithMetadata } = {};
results.forEach((day) => {
const { breakdown } = day;
@@ -140,7 +141,8 @@ export const getTopAPIKeys = (results: ExtendedDailyData[], topKeysLimit: number
.map(([api_key, metrics]) => ({
api_key,
key_alias: keyActivityLabel(metrics.metadata),
- tags: metrics.metadata.tags || "-",
+ user_email: metrics.metadata.user_email ?? null,
+ tags: metrics.metadata.tags || [],
spend: metrics.metrics.spend,
}))
.sort((a, b) => b.spend - a.spend)
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsagePageView.tsx b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsagePageView.tsx
index a92d1209567..4f28e1e8129 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsagePageView.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsagePageView.tsx
@@ -61,7 +61,7 @@ import EntityUsage, { EntityList } from "./EntityUsage/EntityUsage";
import ModelViewToggle, { ModelViewType } from "./ModelViewToggle";
import SpendByProvider from "./EntityUsage/SpendByProvider";
import { TOP_MODEL_LIMITS } from "./EntityUsage/TopModelView";
-import TopKeyView from "@/components/UsagePage/components/EntityUsage/TopKeyView";
+import TopKeyView, { type TopKeyItem } from "@/components/UsagePage/components/EntityUsage/TopKeyView";
import UsageAIChatPanel from "./UsageAIChatPanel";
import { UsageOption, UsageViewSelect } from "./UsageViewSelect/UsageViewSelect";
@@ -407,7 +407,7 @@ const UsagePage: React.FC = ({ teams, organizations }) => {
}, [userSpendData.results]);
// Calculate top API keys from the breakdown data
- const topKeys = useMemo(() => {
+ const topKeys = useMemo(() => {
const keySpend: { [key: string]: KeyMetricWithMetadata } = {};
userSpendData.results.forEach((day) => {
Object.entries(day.breakdown.api_keys || {}).forEach(([key, metrics]) => {
@@ -448,6 +448,7 @@ const UsagePage: React.FC = ({ teams, organizations }) => {
.map(([api_key, metrics]) => ({
api_key,
key_alias: keyActivityLabel(metrics.metadata),
+ user_email: metrics.metadata.user_email ?? null,
tags: metrics.metadata.tags || [],
spend: metrics.metrics.spend,
}))
diff --git a/ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/TopKeyView.test.tsx b/ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/TopKeyView.test.tsx
index c2837cf412e..88adedf022a 100644
--- a/ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/TopKeyView.test.tsx
+++ b/ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/TopKeyView.test.tsx
@@ -102,6 +102,7 @@ describe("TopKeyView", () => {
{
api_key: "key-123",
key_alias: "Test Key",
+ user_email: null,
spend: 100,
tags: [
{ tag: "tag-1", usage: 50 },
@@ -118,6 +119,25 @@ describe("TopKeyView", () => {
expect(screen.getByText("$100.00")).toBeInTheDocument();
});
+ it("should display user attribution when the key has no alias", () => {
+ render(
+ ,
+ );
+
+ expect(screen.getByText("User")).toBeInTheDocument();
+ expect(screen.getByText("alice@example.com")).toBeInTheDocument();
+ });
+
it("should switch to chart view when chart view button is clicked", async () => {
const user = userEvent.setup();
render();
@@ -142,6 +162,7 @@ describe("TopKeyView", () => {
{
api_key: "key-123",
key_alias: "A Very Long Key Alias",
+ user_email: null,
spend: 100,
},
]}
@@ -197,6 +218,7 @@ describe("TopKeyView", () => {
{
api_key: "sk-1234567890abcdef",
key_alias: "Test Key",
+ user_email: null,
spend: 100,
},
]}
@@ -215,12 +237,13 @@ describe("TopKeyView", () => {
{
api_key: "key-123",
key_alias: "",
+ user_email: null,
spend: 100,
},
]}
/>,
);
- expect(screen.getByText("-")).toBeInTheDocument();
+ expect(screen.getAllByText("-")).toHaveLength(2);
});
it("should format spend values with two decimal places", () => {
@@ -231,6 +254,7 @@ describe("TopKeyView", () => {
{
api_key: "key-123",
key_alias: "Test Key",
+ user_email: null,
spend: 123.456,
},
]}
@@ -247,6 +271,7 @@ describe("TopKeyView", () => {
{
api_key: "key-123",
key_alias: "Test Key",
+ user_email: null,
spend: 0.004,
},
]}
@@ -263,12 +288,13 @@ describe("TopKeyView", () => {
{
api_key: "key-123",
key_alias: "Test Key",
+ user_email: null,
spend: 0,
},
]}
/>,
);
- expect(screen.getByText("-")).toBeInTheDocument();
+ expect(screen.getAllByText("-")).toHaveLength(2);
expect(screen.queryByText("$0.00")).not.toBeInTheDocument();
});
@@ -280,6 +306,7 @@ describe("TopKeyView", () => {
{
api_key: "key-123",
key_alias: "Test Key",
+ user_email: null,
spend: 100,
tags: [],
},
@@ -298,6 +325,7 @@ describe("TopKeyView", () => {
{
api_key: "key-123",
key_alias: "Test Key",
+ user_email: null,
spend: 100,
},
]}
@@ -315,6 +343,7 @@ describe("TopKeyView", () => {
{
api_key: "key-123",
key_alias: "Test Key",
+ user_email: null,
spend: 100,
tags: [
{ tag: "tag-1", usage: 50 },
@@ -340,6 +369,7 @@ describe("TopKeyView", () => {
{
api_key: "key-123",
key_alias: "Test Key",
+ user_email: null,
spend: 100,
tags: [
{ tag: "tag-1", usage: 50 },
@@ -367,6 +397,7 @@ describe("TopKeyView", () => {
{
api_key: "key-123",
key_alias: "Test Key",
+ user_email: null,
spend: 100,
tags: [
{ tag: "tag-1", usage: 50 },
@@ -404,6 +435,7 @@ describe("TopKeyView", () => {
{
api_key: "key-123",
key_alias: "Test Key",
+ user_email: null,
spend: 100,
},
]}
@@ -438,6 +470,7 @@ describe("TopKeyView", () => {
{
api_key: "key-123",
key_alias: "Test Key",
+ user_email: null,
spend: 100,
},
]}
@@ -475,6 +508,7 @@ describe("TopKeyView", () => {
{
api_key: "key-123",
key_alias: "Test Key",
+ user_email: null,
spend: 100,
},
]}
@@ -511,6 +545,7 @@ describe("TopKeyView", () => {
{
api_key: "key-123",
key_alias: "Test Key",
+ user_email: null,
spend: 100,
},
]}
@@ -550,6 +585,7 @@ describe("TopKeyView", () => {
{
api_key: "key-123",
key_alias: "Test Key",
+ user_email: null,
spend: 100,
},
]}
@@ -580,6 +616,7 @@ describe("TopKeyView", () => {
{
api_key: "key-123",
key_alias: "Test Key",
+ user_email: null,
spend: 100,
},
]}
@@ -610,6 +647,7 @@ describe("TopKeyView", () => {
{
api_key: "key-123",
key_alias: "Test Key",
+ user_email: null,
spend: 100,
tags: [
{ tag: "tag-low", usage: 10 },
@@ -643,6 +681,7 @@ describe("TopKeyView", () => {
{
api_key: "key-123",
key_alias: "This is a very long key alias",
+ user_email: null,
spend: 100,
},
]}
@@ -658,12 +697,13 @@ describe("TopKeyView", () => {
topKeys={[
{
api_key: "key-123",
- key_alias: null,
+ key_alias: "",
+ user_email: null,
spend: 100,
},
]}
/>,
);
- expect(screen.getByText("-")).toBeInTheDocument();
+ expect(screen.getAllByText("-")).toHaveLength(2);
});
});
diff --git a/ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/TopKeyView.tsx b/ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/TopKeyView.tsx
index df1a51d8e38..7701721ac32 100644
--- a/ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/TopKeyView.tsx
+++ b/ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/TopKeyView.tsx
@@ -15,8 +15,16 @@ import { TagUsage } from "../../types";
const TOP_KEYS_LIMITS = [5, 10, 25, 50] as const;
+export interface TopKeyItem {
+ api_key: string;
+ key_alias: string;
+ user_email: string | null;
+ tags?: TagUsage[] | null;
+ spend: number;
+}
+
interface TopKeyViewProps {
- topKeys: any[];
+ topKeys: TopKeyItem[];
teams: any[] | null;
showTags?: boolean;
topKeysLimit: number;
@@ -43,7 +51,7 @@ const TopKeyView: React.FC = ({ topKeys, teams, showTags = fals
});
};
- const handleKeyClick = async (item: any) => {
+ const handleKeyClick = async (item: TopKeyItem) => {
if (!accessToken) return;
try {
@@ -95,6 +103,11 @@ const TopKeyView: React.FC = ({ topKeys, teams, showTags = fals
accessorKey: "key_alias",
cell: (info: any) => info.getValue() || "-",
},
+ {
+ header: "User",
+ accessorKey: "user_email",
+ cell: (info: any) => info.getValue() || "-",
+ },
];
const tagsColumn = {
diff --git a/ui/litellm-dashboard/tests/top_key_view.test.tsx b/ui/litellm-dashboard/tests/top_key_view.test.tsx
index 51662b8f453..a105bec50e2 100644
--- a/ui/litellm-dashboard/tests/top_key_view.test.tsx
+++ b/ui/litellm-dashboard/tests/top_key_view.test.tsx
@@ -28,12 +28,15 @@ describe("TopKeyView", () => {
teams: null,
premiumUser: true,
showTags: false,
+ topKeysLimit: 5,
+ setTopKeysLimit: vi.fn(),
};
const mockKeysWithTags = [
{
api_key: "key-1",
key_alias: "Production Key",
+ user_email: null,
tags: [
{ tag: "production", usage: 0.005 } as TagUsage, // <$0.01
{ tag: "high-volume", usage: 125.5 } as TagUsage, // High spend
@@ -44,6 +47,7 @@ describe("TopKeyView", () => {
{
api_key: "key-2",
key_alias: "Staging Key",
+ user_email: null,
tags: [
{ tag: "staging", usage: 45.75 } as TagUsage, // Medium spend
{ tag: "testing", usage: 0.008 } as TagUsage, // <$0.01
@@ -54,6 +58,7 @@ describe("TopKeyView", () => {
{
api_key: "key-3",
key_alias: "Development Key",
+ user_email: null,
tags: [
{ tag: "dev", usage: 0.002 } as TagUsage, // <$0.01
{ tag: "experimental", usage: 0.001 } as TagUsage, // <$0.01
@@ -65,11 +70,15 @@ describe("TopKeyView", () => {
beforeEach(() => {
vi.clearAllMocks();
mockUseAuthorized.mockReturnValue({
+ isLoading: false,
+ isAuthorized: true,
token: "mock-token",
accessToken: mockProps.accessToken,
userId: mockProps.userID,
userEmail: "test@example.com",
userRole: mockProps.userRole,
+ userRoleLabel: mockProps.userRole,
+ isViewOnly: false,
premiumUser: mockProps.premiumUser,
disabledPersonalKeyCreation: false,
showSSOBanner: false,
@@ -181,13 +190,14 @@ describe("TopKeyView", () => {
{
api_key: "key-no-tags",
key_alias: "No Tags Key",
+ user_email: null,
tags: [],
spend: 10.0,
},
];
renderWithProviders();
- expect(screen.getByText("-")).toBeInTheDocument();
+ expect(screen.getAllByText("-")).toHaveLength(2);
});
it("should handle keys with undefined tags", () => {
@@ -195,13 +205,14 @@ describe("TopKeyView", () => {
{
api_key: "key-undefined-tags",
key_alias: "Undefined Tags Key",
+ user_email: null,
tags: undefined,
spend: 5.0,
},
];
renderWithProviders();
- expect(screen.getByText("-")).toBeInTheDocument();
+ expect(screen.getAllByText("-")).toHaveLength(2);
});
it("should handle keys with null tags", () => {
@@ -209,13 +220,14 @@ describe("TopKeyView", () => {
{
api_key: "key-null-tags",
key_alias: "Null Tags Key",
+ user_email: null,
tags: null,
spend: 3.0,
},
];
renderWithProviders();
- expect(screen.getByText("-")).toBeInTheDocument();
+ expect(screen.getAllByText("-")).toHaveLength(2);
});
});
@@ -225,6 +237,7 @@ describe("TopKeyView", () => {
{
api_key: "key-long-tags",
key_alias: "Long Tags Key",
+ user_email: null,
tags: [{ tag: "very-long-tag-name", usage: 10.0 } as TagUsage, { tag: "short", usage: 5.0 } as TagUsage],
spend: 15.0,
},
@@ -245,12 +258,14 @@ describe("TopKeyView", () => {
{
api_key: "key-mixed-1",
key_alias: "Mixed Key 1",
+ user_email: null,
tags: [{ tag: "expensive", usage: 999.99 } as TagUsage, { tag: "cheap", usage: 0.001 } as TagUsage],
spend: 1000.0,
},
{
api_key: "key-mixed-2",
key_alias: "Mixed Key 2",
+ user_email: null,
tags: [{ tag: "moderate", usage: 50.0 } as TagUsage, { tag: "tiny", usage: 0.005 } as TagUsage],
spend: 50.01,
},
@@ -292,6 +307,7 @@ describe("TopKeyView", () => {
{
api_key: "test-key-123",
key_alias: "Test Key",
+ user_email: null,
tags: [],
spend: 25.5,
},