mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-24 00:52:24 +00:00
feat(ui): add per-user breakdown to team usage export
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
beceb1bedb
commit
b7d1423ad3
4 changed files with 87 additions and 4 deletions
|
|
@ -49,8 +49,8 @@ describe("ExportTypeSelector", () => {
|
|||
it("should hide the per-user scope for user exports while keeping the other scopes", () => {
|
||||
renderWithProviders(<ExportTypeSelector value="daily" onChange={vi.fn()} entityType="user" />);
|
||||
|
||||
expect(screen.queryByRole("radio", { name: /and user/i })).toBeNull();
|
||||
expect(screen.getByRole("radio", { name: /Day-by-day breakdown by user$/i })).toBeInTheDocument();
|
||||
expect(screen.queryByRole("radio", { name: /and user/i })).not.toBeInTheDocument();
|
||||
expect(screen.getByRole("radio", { name: /Day-by-day breakdown by user Daily metrics for each user$/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole("radio", { name: /Day-by-day breakdown by user and key/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole("radio", { name: /Day-by-day by user and model/i })).toBeInTheDocument();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ interface ExportTypeSelectorProps {
|
|||
}
|
||||
|
||||
const ExportTypeSelector: React.FC<ExportTypeSelectorProps> = ({ value, onChange, entityType }) => {
|
||||
const scopes: { value: ExportScope; title: string; description: string }[] = [
|
||||
const allScopes: { value: ExportScope; title: string; description: string }[] = [
|
||||
{
|
||||
value: "daily",
|
||||
title: `Day-by-day breakdown by ${entityType}`,
|
||||
|
|
@ -25,7 +25,13 @@ const ExportTypeSelector: React.FC<ExportTypeSelectorProps> = ({ value, onChange
|
|||
title: `Day-by-day by ${entityType} and model`,
|
||||
description: "Daily metrics split by model",
|
||||
},
|
||||
{
|
||||
value: "daily_with_users",
|
||||
title: `Day-by-day breakdown by ${entityType} and user`,
|
||||
description: `Daily metrics for each ${entityType}, split by key owner`,
|
||||
},
|
||||
];
|
||||
const scopes = allScopes.filter((scope) => scope.value !== "daily_with_users" || entityType !== "user");
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import type { DateRangePickerValue } from "@/components/shared/date_picker_types
|
|||
import type { Team } from "@/components/key_team_helpers/key_list";
|
||||
|
||||
export type ExportFormat = "csv" | "json";
|
||||
export type ExportScope = "daily" | "daily_with_keys" | "daily_with_models";
|
||||
export type ExportScope = "daily" | "daily_with_keys" | "daily_with_models" | "daily_with_users";
|
||||
export type EntityType = "tag" | "team" | "organization" | "customer" | "agent" | "user";
|
||||
|
||||
export interface EntitySpendData {
|
||||
|
|
|
|||
|
|
@ -166,6 +166,8 @@ export const generateDailyWithKeysData = (
|
|||
entityAlias: string;
|
||||
keyId: string;
|
||||
keyAlias: string | null;
|
||||
userId: string | null;
|
||||
userEmail: string | null;
|
||||
metrics: {
|
||||
spend: number;
|
||||
api_requests: number;
|
||||
|
|
@ -200,6 +202,8 @@ export const generateDailyWithKeysData = (
|
|||
entityAlias,
|
||||
keyId,
|
||||
keyAlias,
|
||||
userId: keyData?.metadata?.user_id || null,
|
||||
userEmail: keyData?.metadata?.user_email || null,
|
||||
metrics: {
|
||||
spend: keyData.metrics?.spend || 0,
|
||||
api_requests: keyData.metrics?.api_requests || 0,
|
||||
|
|
@ -236,6 +240,9 @@ export const generateDailyWithKeysData = (
|
|||
[`${entityLabel} ID`]: item.entityId,
|
||||
"Key Alias": item.keyAlias || "-",
|
||||
"Key ID": item.keyId,
|
||||
...(entityLabel === "User"
|
||||
? {}
|
||||
: { "User ID": item.userId || "-", "User Email": item.userEmail || "-" }),
|
||||
"Spend ($)": formatNumberWithCommas(item.metrics.spend, 4),
|
||||
Requests: item.metrics.api_requests,
|
||||
"Successful Requests": item.metrics.successful_requests,
|
||||
|
|
@ -250,6 +257,74 @@ export const generateDailyWithKeysData = (
|
|||
return dailyKeyBreakdown.sort((a, b) => new Date(a.Date).getTime() - new Date(b.Date).getTime());
|
||||
};
|
||||
|
||||
export const generateDailyWithUsersData = (
|
||||
spendData: EntitySpendData,
|
||||
entityLabel: string,
|
||||
teamAliasMap: Record<string, string> = {},
|
||||
): any[] => {
|
||||
const aggregatedData: {
|
||||
[key: string]: {
|
||||
Date: string;
|
||||
entityId: string;
|
||||
entityAlias: string;
|
||||
userId: string;
|
||||
userEmail: string | null;
|
||||
keyIds: Set<string>;
|
||||
metrics: Record<(typeof METRIC_KEYS)[number], number>;
|
||||
};
|
||||
} = {};
|
||||
|
||||
spendData.results.forEach((day) => {
|
||||
Object.entries(resolveEntities(day.breakdown)).forEach(([entity, data]: [string, any]) => {
|
||||
const { id: entityId, alias: entityAlias } = resolveEntityDisplay(entity, teamAliasMap, data.metadata);
|
||||
Object.entries(data.api_key_breakdown || {}).forEach(([keyId, keyData]: [string, any]) => {
|
||||
const userId = keyData?.metadata?.user_id || "Unassigned";
|
||||
const uniqueKey = `${day.date}_${entityId}_${userId}`;
|
||||
if (!aggregatedData[uniqueKey]) {
|
||||
aggregatedData[uniqueKey] = {
|
||||
Date: day.date,
|
||||
entityId,
|
||||
entityAlias,
|
||||
userId,
|
||||
userEmail: null,
|
||||
keyIds: new Set(),
|
||||
metrics: Object.fromEntries(METRIC_KEYS.map((k) => [k, 0])) as Record<
|
||||
(typeof METRIC_KEYS)[number],
|
||||
number
|
||||
>,
|
||||
};
|
||||
}
|
||||
const bucket = aggregatedData[uniqueKey];
|
||||
bucket.userEmail = bucket.userEmail || keyData?.metadata?.user_email || null;
|
||||
bucket.keyIds.add(keyId);
|
||||
for (const k of METRIC_KEYS) {
|
||||
bucket.metrics[k] += keyData?.metrics?.[k] || 0;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return Object.values(aggregatedData)
|
||||
.map((item) => ({
|
||||
Date: item.Date,
|
||||
[entityLabel]: item.entityAlias,
|
||||
[`${entityLabel} ID`]: item.entityId,
|
||||
"User ID": item.userId,
|
||||
"User Email": item.userEmail || "-",
|
||||
Keys: item.keyIds.size,
|
||||
"Spend ($)": formatNumberWithCommas(item.metrics.spend, 4),
|
||||
Requests: item.metrics.api_requests,
|
||||
"Successful Requests": item.metrics.successful_requests,
|
||||
"Failed Requests": item.metrics.failed_requests,
|
||||
"Total Tokens": item.metrics.total_tokens,
|
||||
"Prompt Tokens": item.metrics.prompt_tokens,
|
||||
"Completion Tokens": item.metrics.completion_tokens,
|
||||
"Cache Read Input Tokens": item.metrics.cache_read_input_tokens,
|
||||
"Cache Creation Input Tokens": item.metrics.cache_creation_input_tokens,
|
||||
}))
|
||||
.sort((a, b) => new Date(a.Date).getTime() - new Date(b.Date).getTime());
|
||||
};
|
||||
|
||||
export const generateDailyWithModelsData = (
|
||||
spendData: EntitySpendData,
|
||||
entityLabel: string,
|
||||
|
|
@ -340,6 +415,8 @@ export const generateExportData = (
|
|||
return generateDailyWithKeysData(spendData, entityLabel, teamAliasMap);
|
||||
case "daily_with_models":
|
||||
return generateDailyWithModelsData(spendData, entityLabel, teamAliasMap);
|
||||
case "daily_with_users":
|
||||
return generateDailyWithUsersData(spendData, entityLabel, teamAliasMap);
|
||||
default:
|
||||
return generateDailyData(spendData, entityLabel, teamAliasMap);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue