mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-13 23:11:40 +00:00
Merge 19aa5aaa42 into 1c61c2606e
This commit is contained in:
commit
266fccc0b3
6 changed files with 96 additions and 17 deletions
|
|
@ -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 }[] }) => (
|
||||
<div>
|
||||
<span>Top Keys</span>
|
||||
<span>{`top-keys:${topKeys.map((row) => `${row.api_key}=${row.spend}`).join("|")}`}</span>
|
||||
<span>
|
||||
{`top-keys:${topKeys.map((row) => `${row.api_key}=${row.spend}=${row.user_email ?? "-"}`).join("|")}`}
|
||||
</span>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
|
@ -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(<EntityUsage {...defaultProps} entityType="team" />);
|
||||
|
||||
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();
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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<UsagePageProps> = ({ teams, organizations }) => {
|
|||
}, [userSpendData.results]);
|
||||
|
||||
// Calculate top API keys from the breakdown data
|
||||
const topKeys = useMemo(() => {
|
||||
const topKeys = useMemo<TopKeyItem[]>(() => {
|
||||
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<UsagePageProps> = ({ 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,
|
||||
}))
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
<TopKeyView
|
||||
{...baseProps}
|
||||
topKeys={[
|
||||
{
|
||||
api_key: "key-123",
|
||||
key_alias: "",
|
||||
user_email: "alice@example.com",
|
||||
spend: 100,
|
||||
},
|
||||
]}
|
||||
/>,
|
||||
);
|
||||
|
||||
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(<TopKeyView {...baseProps} />);
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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<TopKeyViewProps> = ({ 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<TopKeyViewProps> = ({ topKeys, teams, showTags = fals
|
|||
accessorKey: "key_alias",
|
||||
cell: (info: any) => info.getValue() || "-",
|
||||
},
|
||||
{
|
||||
header: "User",
|
||||
accessorKey: "user_email",
|
||||
cell: (info: any) => info.getValue() || "-",
|
||||
},
|
||||
];
|
||||
|
||||
const tagsColumn = {
|
||||
|
|
|
|||
|
|
@ -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(<TopKeyView {...mockProps} topKeys={keysWithoutTags} showTags={true} />);
|
||||
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(<TopKeyView {...mockProps} topKeys={keysWithUndefinedTags} showTags={true} />);
|
||||
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(<TopKeyView {...mockProps} topKeys={keysWithNullTags} showTags={true} />);
|
||||
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,
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue