mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-05 08:07:05 +00:00
Resolve team alias in usage page
This commit is contained in:
parent
bf72427b80
commit
87e915e4e2
7 changed files with 279 additions and 10 deletions
|
|
@ -40,6 +40,14 @@ vi.mock("./EntityUsageExport", () => ({
|
|||
UsageExportHeader: () => <div>Usage Export Header</div>,
|
||||
}));
|
||||
|
||||
// Mock useTeams hook
|
||||
vi.mock("@/app/(dashboard)/hooks/useTeams", () => ({
|
||||
default: vi.fn(() => ({
|
||||
teams: [],
|
||||
setTeams: vi.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
describe("EntityUsage", () => {
|
||||
const mockTagDailyActivityCall = vi.mocked(networking.tagDailyActivityCall);
|
||||
const mockTeamDailyActivityCall = vi.mocked(networking.teamDailyActivityCall);
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ import { BreakdownMetrics, DailyData, EntityMetricWithMetadata, KeyMetricWithMet
|
|||
import { valueFormatterSpend } from "../../utils/value_formatters";
|
||||
import TopKeyView from "./TopKeyView";
|
||||
import TopModelView from "./TopModelView";
|
||||
import useTeams from "@/app/(dashboard)/hooks/useTeams";
|
||||
|
||||
interface EntityMetrics {
|
||||
metrics: {
|
||||
|
|
@ -104,9 +105,10 @@ const EntityUsage: React.FC<EntityUsageProps> = ({
|
|||
total_tokens: 0,
|
||||
},
|
||||
});
|
||||
const { teams } = useTeams();
|
||||
|
||||
const modelMetrics = processActivityData(spendData, "models");
|
||||
const keyMetrics = processActivityData(spendData, "api_keys");
|
||||
const modelMetrics = processActivityData(spendData, "models", teams || []);
|
||||
const keyMetrics = processActivityData(spendData, "api_keys", teams || []);
|
||||
const [selectedTags, setSelectedTags] = useState<string[]>([]);
|
||||
|
||||
const fetchSpendData = async () => {
|
||||
|
|
|
|||
|
|
@ -371,9 +371,9 @@ const UsagePage: React.FC<UsagePageProps> = ({ teams, organizations }) => {
|
|||
return () => clearTimeout(timeoutId);
|
||||
}, [fetchUserSpendData]);
|
||||
|
||||
const modelMetrics = processActivityData(userSpendData, "models");
|
||||
const keyMetrics = processActivityData(userSpendData, "api_keys");
|
||||
const mcpServerMetrics = processActivityData(userSpendData, "mcp_servers");
|
||||
const modelMetrics = processActivityData(userSpendData, "models", teams);
|
||||
const keyMetrics = processActivityData(userSpendData, "api_keys", teams);
|
||||
const mcpServerMetrics = processActivityData(userSpendData, "mcp_servers", teams);
|
||||
|
||||
return (
|
||||
<div style={{ width: "100%" }} className="p-8 relative">
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import { render, screen } from "@testing-library/react";
|
||||
import React from "react";
|
||||
import { beforeAll, describe, expect, it, vi } from "vitest";
|
||||
import { ActivityMetrics } from "./activity_metrics";
|
||||
import { ModelActivityData } from "./UsagePage/types";
|
||||
import { ActivityMetrics, processActivityData, formatKeyLabel } from "./activity_metrics";
|
||||
import { ModelActivityData, DailyData, KeyMetricWithMetadata } from "./UsagePage/types";
|
||||
import { Team } from "./key_team_helpers/key_list";
|
||||
|
||||
beforeAll(() => {
|
||||
if (typeof window !== "undefined" && !window.ResizeObserver) {
|
||||
|
|
@ -101,3 +102,211 @@ describe("ActivityMetrics", () => {
|
|||
expect(screen.queryByText("Prompt Caching Metrics")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("processActivityData", () => {
|
||||
const mockDailyActivity: { results: DailyData[] } = {
|
||||
results: [
|
||||
{
|
||||
date: "2025-01-01",
|
||||
metrics: {
|
||||
spend: 100.5,
|
||||
prompt_tokens: 30000,
|
||||
completion_tokens: 20000,
|
||||
total_tokens: 50000,
|
||||
api_requests: 100,
|
||||
successful_requests: 95,
|
||||
failed_requests: 5,
|
||||
cache_read_input_tokens: 1000,
|
||||
cache_creation_input_tokens: 500,
|
||||
},
|
||||
breakdown: {
|
||||
models: {},
|
||||
model_groups: {},
|
||||
mcp_servers: {},
|
||||
providers: {},
|
||||
api_keys: {
|
||||
key1: {
|
||||
metrics: {
|
||||
spend: 50.25,
|
||||
prompt_tokens: 15000,
|
||||
completion_tokens: 10000,
|
||||
total_tokens: 25000,
|
||||
api_requests: 50,
|
||||
successful_requests: 47,
|
||||
failed_requests: 3,
|
||||
cache_read_input_tokens: 500,
|
||||
cache_creation_input_tokens: 250,
|
||||
},
|
||||
metadata: {
|
||||
key_alias: "test-key-1",
|
||||
team_id: "team1",
|
||||
},
|
||||
},
|
||||
},
|
||||
entities: {},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const mockTeams: Team[] = [
|
||||
{
|
||||
team_id: "team1",
|
||||
team_alias: "Test Team 1",
|
||||
models: [],
|
||||
max_budget: null,
|
||||
budget_duration: null,
|
||||
tpm_limit: null,
|
||||
rpm_limit: null,
|
||||
organization_id: "org1",
|
||||
created_at: "2025-01-01",
|
||||
keys: [],
|
||||
members_with_roles: [],
|
||||
},
|
||||
];
|
||||
|
||||
it("should process data for models key without teams parameter", () => {
|
||||
const result = processActivityData(mockDailyActivity, "models");
|
||||
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
it("should process data for api_keys key with teams parameter", () => {
|
||||
const result = processActivityData(mockDailyActivity, "api_keys", mockTeams);
|
||||
|
||||
expect(result).toHaveProperty("key1");
|
||||
expect(result["key1"].label).toBe("test-key-1 (team: Test Team 1)");
|
||||
expect(result["key1"].total_requests).toBe(50);
|
||||
expect(result["key1"].total_spend).toBe(50.25);
|
||||
});
|
||||
|
||||
it("should process data for api_keys key without teams parameter", () => {
|
||||
const result = processActivityData(mockDailyActivity, "api_keys");
|
||||
|
||||
expect(result).toHaveProperty("key1");
|
||||
expect(result["key1"].label).toBe("test-key-1 (team_id: team1)");
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatKeyLabel", () => {
|
||||
const mockTeams: Team[] = [
|
||||
{
|
||||
team_id: "team1",
|
||||
team_alias: "Test Team 1",
|
||||
models: [],
|
||||
max_budget: null,
|
||||
budget_duration: null,
|
||||
tpm_limit: null,
|
||||
rpm_limit: null,
|
||||
organization_id: "org1",
|
||||
created_at: "2025-01-01",
|
||||
keys: [],
|
||||
members_with_roles: [],
|
||||
},
|
||||
{
|
||||
team_id: "team2",
|
||||
team_alias: "Test Team 2",
|
||||
models: [],
|
||||
max_budget: null,
|
||||
budget_duration: null,
|
||||
tpm_limit: null,
|
||||
rpm_limit: null,
|
||||
organization_id: "org2",
|
||||
created_at: "2025-01-01",
|
||||
keys: [],
|
||||
members_with_roles: [],
|
||||
},
|
||||
];
|
||||
|
||||
it("should return key_alias when no team_id is present", () => {
|
||||
const modelData: KeyMetricWithMetadata = {
|
||||
metrics: {
|
||||
spend: 0,
|
||||
prompt_tokens: 0,
|
||||
completion_tokens: 0,
|
||||
total_tokens: 0,
|
||||
api_requests: 0,
|
||||
successful_requests: 0,
|
||||
failed_requests: 0,
|
||||
cache_read_input_tokens: 0,
|
||||
cache_creation_input_tokens: 0,
|
||||
},
|
||||
metadata: {
|
||||
key_alias: "test-key",
|
||||
team_id: null,
|
||||
},
|
||||
};
|
||||
|
||||
const result = formatKeyLabel(modelData, "test-key", mockTeams);
|
||||
expect(result).toBe("test-key");
|
||||
});
|
||||
|
||||
it("should return key_alias with team alias when team_id matches", () => {
|
||||
const modelData: KeyMetricWithMetadata = {
|
||||
metrics: {
|
||||
spend: 0,
|
||||
prompt_tokens: 0,
|
||||
completion_tokens: 0,
|
||||
total_tokens: 0,
|
||||
api_requests: 0,
|
||||
successful_requests: 0,
|
||||
failed_requests: 0,
|
||||
cache_read_input_tokens: 0,
|
||||
cache_creation_input_tokens: 0,
|
||||
},
|
||||
metadata: {
|
||||
key_alias: "test-key",
|
||||
team_id: "team1",
|
||||
},
|
||||
};
|
||||
|
||||
const result = formatKeyLabel(modelData, "test-key", mockTeams);
|
||||
expect(result).toBe("test-key (team: Test Team 1)");
|
||||
});
|
||||
|
||||
it("should return key_alias with team_id when team is not found", () => {
|
||||
const modelData: KeyMetricWithMetadata = {
|
||||
metrics: {
|
||||
spend: 0,
|
||||
prompt_tokens: 0,
|
||||
completion_tokens: 0,
|
||||
total_tokens: 0,
|
||||
api_requests: 0,
|
||||
successful_requests: 0,
|
||||
failed_requests: 0,
|
||||
cache_read_input_tokens: 0,
|
||||
cache_creation_input_tokens: 0,
|
||||
},
|
||||
metadata: {
|
||||
key_alias: "test-key",
|
||||
team_id: "nonexistent-team",
|
||||
},
|
||||
};
|
||||
|
||||
const result = formatKeyLabel(modelData, "test-key", mockTeams);
|
||||
expect(result).toBe("test-key (team_id: nonexistent-team)");
|
||||
});
|
||||
|
||||
it("should use key-hash fallback when key_alias is null", () => {
|
||||
const modelData: KeyMetricWithMetadata = {
|
||||
metrics: {
|
||||
spend: 0,
|
||||
prompt_tokens: 0,
|
||||
completion_tokens: 0,
|
||||
total_tokens: 0,
|
||||
api_requests: 0,
|
||||
successful_requests: 0,
|
||||
failed_requests: 0,
|
||||
cache_read_input_tokens: 0,
|
||||
cache_creation_input_tokens: 0,
|
||||
},
|
||||
metadata: {
|
||||
key_alias: null,
|
||||
team_id: "team1",
|
||||
},
|
||||
};
|
||||
|
||||
const result = formatKeyLabel(modelData, "actual-key", mockTeams);
|
||||
expect(result).toBe("key-hash-actual-key (team: Test Team 1)");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import React from "react";
|
|||
import { CustomLegend, CustomTooltip } from "./common_components/chartUtils";
|
||||
import { DailyData, KeyMetricWithMetadata, ModelActivityData, TopApiKeyData } from "./UsagePage/types";
|
||||
import { valueFormatter } from "./UsagePage/utils/value_formatters";
|
||||
import { Team } from "./key_team_helpers/key_list";
|
||||
import { resolveTeamAliasFromTeamID } from "@/utils/teamUtils";
|
||||
|
||||
interface ActivityMetricsProps {
|
||||
modelMetrics: Record<string, ModelActivityData>;
|
||||
|
|
@ -337,16 +339,21 @@ export const ActivityMetrics: React.FC<ActivityMetricsProps> = ({ modelMetrics,
|
|||
};
|
||||
|
||||
// Helper function to format key label
|
||||
const formatKeyLabel = (modelData: KeyMetricWithMetadata, model: string): string => {
|
||||
export const formatKeyLabel = (modelData: KeyMetricWithMetadata, model: string, teams: Team[]): string => {
|
||||
const keyAlias = modelData.metadata.key_alias || `key-hash-${model}`;
|
||||
const teamId = modelData.metadata.team_id;
|
||||
return teamId ? `${keyAlias} (team_id: ${teamId})` : keyAlias;
|
||||
if (teamId) {
|
||||
const teamAlias = resolveTeamAliasFromTeamID(teamId, teams);
|
||||
return teamAlias ? `${keyAlias} (team: ${teamAlias})` : `${keyAlias} (team_id: ${teamId})`;
|
||||
}
|
||||
return keyAlias;
|
||||
};
|
||||
|
||||
// Process data function
|
||||
export const processActivityData = (
|
||||
dailyActivity: { results: DailyData[] },
|
||||
key: "models" | "api_keys" | "mcp_servers",
|
||||
teams: Team[] = [],
|
||||
): Record<string, ModelActivityData> => {
|
||||
const modelMetrics: Record<string, ModelActivityData> = {};
|
||||
|
||||
|
|
@ -354,7 +361,7 @@ export const processActivityData = (
|
|||
Object.entries(day.breakdown[key] || {}).forEach(([model, modelData]) => {
|
||||
if (!modelMetrics[model]) {
|
||||
modelMetrics[model] = {
|
||||
label: key === "api_keys" ? formatKeyLabel(modelData as KeyMetricWithMetadata, model) : model,
|
||||
label: key === "api_keys" ? formatKeyLabel(modelData as KeyMetricWithMetadata, model, teams) : model,
|
||||
total_requests: 0,
|
||||
total_successful_requests: 0,
|
||||
total_failed_requests: 0,
|
||||
|
|
|
|||
37
ui/litellm-dashboard/src/utils/teamUtils.test.ts
Normal file
37
ui/litellm-dashboard/src/utils/teamUtils.test.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { resolveTeamAliasFromTeamID } from "./teamUtils";
|
||||
import type { Team } from "@/components/networking";
|
||||
|
||||
describe("resolveTeamAliasFromTeamID", () => {
|
||||
it("should return team alias when team is found", () => {
|
||||
const teams = [
|
||||
{
|
||||
team_id: "team1",
|
||||
team_alias: "Team One",
|
||||
},
|
||||
{
|
||||
team_id: "team2",
|
||||
team_alias: "Team Two",
|
||||
},
|
||||
] as unknown as Team[];
|
||||
|
||||
const result = resolveTeamAliasFromTeamID("team1", teams);
|
||||
expect(result).toBe("Team One");
|
||||
});
|
||||
|
||||
it("should return null when team is not found", () => {
|
||||
const teams = [
|
||||
{
|
||||
team_id: "team1",
|
||||
team_alias: "Team One",
|
||||
},
|
||||
{
|
||||
team_id: "team2",
|
||||
team_alias: "Team Two",
|
||||
},
|
||||
] as unknown as Team[];
|
||||
|
||||
const result = resolveTeamAliasFromTeamID("team3", teams);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
6
ui/litellm-dashboard/src/utils/teamUtils.ts
Normal file
6
ui/litellm-dashboard/src/utils/teamUtils.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { Team } from "@/components/networking";
|
||||
|
||||
export const resolveTeamAliasFromTeamID = (teamID: string, teams: Team[]): string | null => {
|
||||
const team = teams.find((team) => team.team_id === teamID);
|
||||
return team ? team.team_alias : null;
|
||||
};
|
||||
Loading…
Add table
Reference in a new issue