diff --git a/ui/litellm-dashboard/src/components/EntityUsageExport/ExportTypeSelector.test.tsx b/ui/litellm-dashboard/src/components/EntityUsageExport/ExportTypeSelector.test.tsx index 6c039b106bf..6743167bff6 100644 --- a/ui/litellm-dashboard/src/components/EntityUsageExport/ExportTypeSelector.test.tsx +++ b/ui/litellm-dashboard/src/components/EntityUsageExport/ExportTypeSelector.test.tsx @@ -33,4 +33,33 @@ describe("ExportTypeSelector", () => { renderWithProviders(); expect(screen.getByRole("radio", { name: /Day-by-day by team and model/i })).toBeChecked(); }); + + it("should offer the per-user scope for teams and call onChange with daily_with_users", async () => { + const user = userEvent.setup(); + const onChange = vi.fn(); + renderWithProviders(); + + const option = screen.getByRole("radio", { name: /Day-by-day breakdown by team and user/i }); + await user.click(option); + + expect(onChange).toHaveBeenCalledWith("daily_with_users"); + expect(screen.getByText("Daily metrics for each team, split by key owner")).toBeInTheDocument(); + }); + + it("should hide the per-user scope for user exports while keeping the other scopes", () => { + renderWithProviders(); + + 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(); + }); + + it("should offer the per-user scope for tags", () => { + renderWithProviders(); + + expect(screen.getByRole("radio", { name: /Day-by-day breakdown by tag and user/i })).toBeInTheDocument(); + }); }); diff --git a/ui/litellm-dashboard/src/components/EntityUsageExport/ExportTypeSelector.tsx b/ui/litellm-dashboard/src/components/EntityUsageExport/ExportTypeSelector.tsx index edd055ba7a7..f6fdece83a8 100644 --- a/ui/litellm-dashboard/src/components/EntityUsageExport/ExportTypeSelector.tsx +++ b/ui/litellm-dashboard/src/components/EntityUsageExport/ExportTypeSelector.tsx @@ -9,7 +9,7 @@ interface ExportTypeSelectorProps { } const ExportTypeSelector: React.FC = ({ 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 = ({ 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 (
diff --git a/ui/litellm-dashboard/src/components/EntityUsageExport/types.ts b/ui/litellm-dashboard/src/components/EntityUsageExport/types.ts index 30714ad632d..15f193ecc3f 100644 --- a/ui/litellm-dashboard/src/components/EntityUsageExport/types.ts +++ b/ui/litellm-dashboard/src/components/EntityUsageExport/types.ts @@ -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 { diff --git a/ui/litellm-dashboard/src/components/EntityUsageExport/utils.test.ts b/ui/litellm-dashboard/src/components/EntityUsageExport/utils.test.ts index 7ed014d43b2..3f9cf58ec20 100644 --- a/ui/litellm-dashboard/src/components/EntityUsageExport/utils.test.ts +++ b/ui/litellm-dashboard/src/components/EntityUsageExport/utils.test.ts @@ -8,6 +8,7 @@ import { generateDailyData, generateDailyWithKeysData, generateDailyWithModelsData, + generateDailyWithUsersData, generateExportData, generateMetadata, getEntityBreakdown, @@ -151,6 +152,204 @@ describe("EntityUsageExport utils", () => { "team-2": "Team Two", }; + const usersFixture: EntitySpendData = { + results: [ + { + date: "2025-03-01", + breakdown: { + entities: { + "team-1": { + metrics: { + spend: 16.5, + api_requests: 165, + successful_requests: 156, + failed_requests: 9, + total_tokens: 1650, + prompt_tokens: 940, + completion_tokens: 710, + cache_read_input_tokens: 90, + cache_creation_input_tokens: 60, + }, + api_key_breakdown: { + kA: { + metrics: { + spend: 1.1, + api_requests: 11, + successful_requests: 10, + failed_requests: 1, + total_tokens: 110, + prompt_tokens: 60, + completion_tokens: 50, + cache_read_input_tokens: 6, + cache_creation_input_tokens: 4, + }, + metadata: { + team_id: "team-1", + key_alias: "alice-key", + user_id: "u1", + user_email: "a@x", + }, + }, + kB: { + metrics: { + spend: 2.2, + api_requests: 22, + successful_requests: 20, + failed_requests: 2, + total_tokens: 220, + prompt_tokens: 130, + completion_tokens: 90, + cache_read_input_tokens: 12, + cache_creation_input_tokens: 8, + }, + metadata: { + team_id: "team-1", + user_id: "u1", + user_email: "a@x", + }, + }, + kC: { + metrics: { + spend: 3.3, + api_requests: 33, + successful_requests: 31, + failed_requests: 2, + total_tokens: 330, + prompt_tokens: 190, + completion_tokens: 140, + cache_read_input_tokens: 18, + cache_creation_input_tokens: 12, + }, + metadata: { + team_id: "team-1", + user_id: "u2", + user_email: null, + }, + }, + kD: { + metrics: { + spend: 4.4, + api_requests: 44, + successful_requests: 42, + failed_requests: 2, + total_tokens: 440, + prompt_tokens: 250, + completion_tokens: 190, + cache_read_input_tokens: 24, + cache_creation_input_tokens: 16, + }, + metadata: { + team_id: "team-1", + user_id: null, + }, + }, + kE: { + metrics: { + spend: 5.5, + api_requests: 55, + successful_requests: 53, + failed_requests: 2, + total_tokens: 550, + prompt_tokens: 310, + completion_tokens: 240, + cache_read_input_tokens: 30, + cache_creation_input_tokens: 20, + }, + metadata: { + team_id: "team-1", + user_id: "u3", + key_exists: false, + }, + }, + }, + }, + "team-2": { + metrics: { + spend: 6.6, + api_requests: 66, + successful_requests: 64, + failed_requests: 2, + total_tokens: 660, + prompt_tokens: 370, + completion_tokens: 290, + cache_read_input_tokens: 36, + cache_creation_input_tokens: 24, + }, + api_key_breakdown: { + kF: { + metrics: { + spend: 6.6, + api_requests: 66, + successful_requests: 64, + failed_requests: 2, + total_tokens: 660, + prompt_tokens: 370, + completion_tokens: 290, + cache_read_input_tokens: 36, + cache_creation_input_tokens: 24, + }, + metadata: { + team_id: "team-2", + user_id: "u1", + user_email: "a@x", + }, + }, + }, + }, + }, + }, + }, + { + date: "2025-03-02", + breakdown: { + entities: { + "team-1": { + metrics: { + spend: 7.7, + api_requests: 77, + successful_requests: 75, + failed_requests: 2, + total_tokens: 770, + prompt_tokens: 430, + completion_tokens: 340, + cache_read_input_tokens: 42, + cache_creation_input_tokens: 28, + }, + api_key_breakdown: { + kA: { + metrics: { + spend: 7.7, + api_requests: 77, + successful_requests: 75, + failed_requests: 2, + total_tokens: 770, + prompt_tokens: 430, + completion_tokens: 340, + cache_read_input_tokens: 42, + cache_creation_input_tokens: 28, + }, + metadata: { + team_id: "team-1", + key_alias: "alice-key", + user_id: "u1", + user_email: "a@x", + }, + }, + }, + }, + }, + }, + }, + ], + metadata: { + total_spend: 30.8, + total_api_requests: 308, + total_successful_requests: 295, + total_failed_requests: 13, + total_tokens: 3080, + }, + }; + beforeEach(() => { vi.clearAllMocks(); }); @@ -1056,6 +1255,27 @@ describe("EntityUsageExport utils", () => { expect(keyIds).toContain("key1"); expect(keyIds).toContain("key2"); }); + + it("should emit key owner columns right after Key ID", () => { + const result = generateDailyWithKeysData(usersFixture, "Team"); + + const columnNames = Object.keys(result[0]); + expect(columnNames[columnNames.indexOf("Key ID") + 1]).toBe("User ID"); + expect(columnNames[columnNames.indexOf("User ID") + 1]).toBe("User Email"); + + const kARow = result.find((r) => r["Key ID"] === "kA" && r.Date === "2025-03-01"); + expect(kARow?.["User ID"]).toBe("u1"); + expect(kARow?.["User Email"]).toBe("a@x"); + expect(kARow?.["Key Alias"]).toBe("alice-key"); + + const kCRow = result.find((r) => r["Key ID"] === "kC"); + expect(kCRow?.["User ID"]).toBe("u2"); + expect(kCRow?.["User Email"]).toBe("-"); + + const kDRow = result.find((r) => r["Key ID"] === "kD"); + expect(kDRow?.["User ID"]).toBe("-"); + expect(kDRow?.["User Email"]).toBe("-"); + }); }); describe("generateDailyWithModelsData", () => { @@ -2010,6 +2230,21 @@ describe("EntityUsageExport utils", () => { window.Blob = originalBlob; }); + + it("should generate the daily_with_users filename and include User ID in the rows", () => { + const anchorElement = document.createElement("a"); + vi.spyOn(document, "createElement").mockReturnValue(anchorElement); + vi.useFakeTimers(); + vi.setSystemTime(new Date("2025-03-01T12:00:00Z")); + + handleExportCSV(usersFixture, "daily_with_users", "Team", "team", mockTeamAliasMap); + vi.useRealTimers(); + + expect(anchorElement.download).toBe("team_usage_daily_with_users_2025-03-01.csv"); + + const unparsedRows = vi.mocked(Papa.unparse).mock.calls[0][0] as Record[]; + expect(unparsedRows[0]).toHaveProperty("User ID"); + }); }); describe("handleExportJSON", () => { @@ -2462,4 +2697,312 @@ describe("EntityUsageExport utils", () => { expect(result.find((r) => r["User ID"] === "user-b")?.["User"]).toBe("Grace"); }); }); + + describe("generateDailyWithUsersData", () => { + it("should reconcile spend with daily_with_keys and daily per date and team", () => { + const byUser = generateDailyWithUsersData(usersFixture, "Team"); + const byKey = generateDailyWithKeysData(usersFixture, "Team"); + const daily = generateDailyData(usersFixture, "Team"); + + expect(byUser.length).toBeGreaterThan(0); + expect(byKey.length).toBeGreaterThan(0); + expect(daily.length).toBeGreaterThan(0); + + const sumSpend = (rows: any[]): Record => { + const totals: Record = {}; + rows.forEach((r) => { + const bucket = `${r.Date}|${r["Team ID"]}`; + totals[bucket] = (totals[bucket] || 0) + Number(r["Spend ($)"]); + }); + return totals; + }; + + const userTotals = sumSpend(byUser); + const keyTotals = sumSpend(byKey); + + daily.forEach((row) => { + const bucket = `${row.Date}|${row["Team ID"]}`; + expect(userTotals[bucket]).toBeCloseTo(Number(row["Spend ($)"]), 4); + expect(keyTotals[bucket]).toBeCloseTo(Number(row["Spend ($)"]), 4); + }); + }); + + it("should roll multiple keys owned by one user in a team into a single row", () => { + const rows = generateDailyWithUsersData(usersFixture, "Team"); + const matches = rows.filter((r) => r.Date === "2025-03-01" && r["Team ID"] === "team-1" && r["User ID"] === "u1"); + + expect(matches).toHaveLength(1); + const row = matches[0]; + expect(row.Keys).toBe(2); + expect(row["User Email"]).toBe("a@x"); + expect(row["Spend ($)"]).toBe("3.3000"); + expect(row.Requests).toBe(33); + expect(row["Successful Requests"]).toBe(30); + expect(row["Failed Requests"]).toBe(3); + expect(row["Total Tokens"]).toBe(330); + expect(row["Prompt Tokens"]).toBe(190); + expect(row["Completion Tokens"]).toBe(140); + expect(row["Cache Read Input Tokens"]).toBe(18); + expect(row["Cache Creation Input Tokens"]).toBe(12); + }); + + it("should bucket keys with no owner into an Unassigned row without dropping spend", () => { + const rows = generateDailyWithUsersData(usersFixture, "Team"); + const row = rows.find( + (r) => r.Date === "2025-03-01" && r["Team ID"] === "team-1" && r["User ID"] === "Unassigned", + ); + + expect(row).toBeDefined(); + expect(row?.["User Email"]).toBe("-"); + expect(Number(row?.["Spend ($)"])).toBeCloseTo(4.4, 4); + }); + + it("should keep different users in the same team as separate rows", () => { + const rows = generateDailyWithUsersData(usersFixture, "Team"); + const teamRows = rows.filter((r) => r.Date === "2025-03-01" && r["Team ID"] === "team-1"); + + const u1Rows = teamRows.filter((r) => r["User ID"] === "u1"); + const u2Rows = teamRows.filter((r) => r["User ID"] === "u2"); + expect(u1Rows).toHaveLength(1); + expect(u2Rows).toHaveLength(1); + expect(Number(u2Rows[0]["Spend ($)"])).toBeCloseTo(3.3, 4); + }); + + it("should keep the same user in different teams as separate rows", () => { + const rows = generateDailyWithUsersData(usersFixture, "Team"); + const u1Rows = rows.filter((r) => r.Date === "2025-03-01" && r["User ID"] === "u1"); + + expect(u1Rows).toHaveLength(2); + const team1Row = u1Rows.find((r) => r["Team ID"] === "team-1"); + const team2Row = u1Rows.find((r) => r["Team ID"] === "team-2"); + expect(team1Row?.Keys).toBe(2); + expect(team2Row?.Keys).toBe(1); + expect(Number(team2Row?.["Spend ($)"])).toBeCloseTo(6.6, 4); + }); + + it("should show a dash email when the user has none", () => { + const rows = generateDailyWithUsersData(usersFixture, "Team"); + const row = rows.find((r) => r.Date === "2025-03-01" && r["Team ID"] === "team-1" && r["User ID"] === "u3"); + + expect(row).toBeDefined(); + expect(row?.["User Email"]).toBe("-"); + }); + + it("should still attribute a deleted key to its user", () => { + const rows = generateDailyWithUsersData(usersFixture, "Team"); + const row = rows.find((r) => r.Date === "2025-03-01" && r["Team ID"] === "team-1" && r["User ID"] === "u3"); + + expect(row).toBeDefined(); + expect(Number(row?.["Spend ($)"])).toBeCloseTo(5.5, 4); + expect(row?.Requests).toBe(55); + }); + + it("should group rows under team_id on the aggregated endpoint shape", () => { + const aggregatedFixture: EntitySpendData = { + results: [ + { + date: "2025-03-01", + breakdown: { + entities: {}, + api_keys: { + kA: { + metrics: { + spend: 1.1, + api_requests: 11, + successful_requests: 10, + failed_requests: 1, + total_tokens: 110, + prompt_tokens: 60, + completion_tokens: 50, + cache_read_input_tokens: 6, + cache_creation_input_tokens: 4, + }, + metadata: { team_id: "team-1", user_id: "u1", user_email: "a@x" }, + }, + kF: { + metrics: { + spend: 6.6, + api_requests: 66, + successful_requests: 64, + failed_requests: 2, + total_tokens: 660, + prompt_tokens: 370, + completion_tokens: 290, + cache_read_input_tokens: 36, + cache_creation_input_tokens: 24, + }, + metadata: { team_id: "team-2", user_id: "u2" }, + }, + }, + }, + }, + ], + metadata: usersFixture.metadata, + }; + + const rows = generateDailyWithUsersData(aggregatedFixture, "Team"); + + expect(rows).toHaveLength(2); + const team1Row = rows.find((r) => r["Team ID"] === "team-1"); + const team2Row = rows.find((r) => r["Team ID"] === "team-2"); + expect(team1Row?.["User ID"]).toBe("u1"); + expect(team2Row?.["User ID"]).toBe("u2"); + expect(Number(team1Row?.["Spend ($)"])).toBeCloseTo(1.1, 4); + expect(Number(team2Row?.["Spend ($)"])).toBeCloseTo(6.6, 4); + }); + + it("should emit the exact column order and sort by date ascending", () => { + const rows = generateDailyWithUsersData(usersFixture, "Team"); + + expect(Object.keys(rows[0])).toEqual([ + "Date", + "Team", + "Team ID", + "User ID", + "User Email", + "Keys", + "Spend ($)", + "Requests", + "Successful Requests", + "Failed Requests", + "Total Tokens", + "Prompt Tokens", + "Completion Tokens", + "Cache Read Input Tokens", + "Cache Creation Input Tokens", + ]); + + const dates = rows.map((r) => new Date(r.Date).getTime()); + for (let i = 0; i < dates.length - 1; i++) { + expect(dates[i]).toBeLessThanOrEqual(dates[i + 1]); + } + }); + + it("should dispatch daily_with_users through generateExportData", () => { + expect(generateExportData(usersFixture, "daily_with_users", "Team")).toEqual( + generateDailyWithUsersData(usersFixture, "Team"), + ); + }); + + it("should keep owners separate when entity and user ids contain underscores", () => { + const collisionFixture: EntitySpendData = { + results: [ + { + date: "2025-03-01", + breakdown: { + entities: { + team_1: { + metrics: { spend: 1, api_requests: 1, total_tokens: 10 }, + api_key_breakdown: { + kX: { + metrics: { spend: 1, api_requests: 1, total_tokens: 10 }, + metadata: { team_id: "team_1", user_id: "u1" }, + }, + }, + }, + team: { + metrics: { spend: 2, api_requests: 2, total_tokens: 20 }, + api_key_breakdown: { + kY: { + metrics: { spend: 2, api_requests: 2, total_tokens: 20 }, + metadata: { team_id: "team", user_id: "1_u1" }, + }, + }, + }, + }, + }, + }, + ], + metadata: usersFixture.metadata, + }; + + const rows = generateDailyWithUsersData(collisionFixture, "Team"); + + expect(rows).toHaveLength(2); + const team1Row = rows.find((r) => r["Team ID"] === "team_1"); + expect(team1Row?.["User ID"]).toBe("u1"); + expect(team1Row?.Keys).toBe(1); + expect(team1Row?.["Spend ($)"]).toBe("1.0000"); + const teamRow = rows.find((r) => r["Team ID"] === "team"); + expect(teamRow?.["User ID"]).toBe("1_u1"); + expect(teamRow?.Keys).toBe(1); + expect(teamRow?.["Spend ($)"]).toBe("2.0000"); + }); + + it("should leave daily and daily_with_models output without user columns", () => { + const daily = generateDailyData(usersFixture, "Team"); + expect(daily[0]).not.toHaveProperty("User ID"); + + const modelsFixture: EntitySpendData = { + results: [ + { + date: "2025-03-01", + breakdown: { + entities: { + "team-1": { + metrics: { + spend: 1.1, + api_requests: 11, + successful_requests: 10, + failed_requests: 1, + total_tokens: 110, + prompt_tokens: 60, + completion_tokens: 50, + }, + api_key_breakdown: { + kA: { + metrics: { + spend: 1.1, + api_requests: 11, + successful_requests: 10, + failed_requests: 1, + total_tokens: 110, + }, + metadata: { team_id: "team-1", user_id: "u1", user_email: "a@x" }, + }, + }, + }, + }, + models: { + "gpt-4o": { + metrics: { spend: 1.1, api_requests: 11, total_tokens: 110 }, + api_key_breakdown: { + kA: { + metrics: { + spend: 1.1, + api_requests: 11, + successful_requests: 10, + failed_requests: 1, + total_tokens: 110, + }, + metadata: {}, + }, + }, + }, + }, + }, + }, + ], + metadata: usersFixture.metadata, + }; + + const modelRows = generateDailyWithModelsData(modelsFixture, "Team"); + expect(modelRows).toHaveLength(1); + expect(Object.keys(modelRows[0])).toEqual([ + "Date", + "Team", + "Team ID", + "Model", + "Spend ($)", + "Requests", + "Successful", + "Failed", + "Total Tokens", + "Prompt Tokens", + "Completion Tokens", + "Cache Read Input Tokens", + "Cache Creation Input Tokens", + ]); + }); + }); }); diff --git a/ui/litellm-dashboard/src/components/EntityUsageExport/utils.ts b/ui/litellm-dashboard/src/components/EntityUsageExport/utils.ts index 8fd75134bcc..95ce584cc89 100644 --- a/ui/litellm-dashboard/src/components/EntityUsageExport/utils.ts +++ b/ui/litellm-dashboard/src/components/EntityUsageExport/utils.ts @@ -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,7 @@ 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 +255,71 @@ 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 = {}, +): any[] => { + const aggregatedData: { + [key: string]: { + Date: string; + entityId: string; + entityAlias: string; + userId: string; + userEmail: string | null; + keyIds: Set; + 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 = JSON.stringify([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 +410,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); }