fix(ui): use entity key for export instead of extracting team_id from api_key_breakdown

The export utility always extracted team_id from api_key_breakdown
metadata to populate the entity label/ID columns. This worked for
team exports (where entity key = team_id) but was wrong for every
other entity type — tags, orgs, customers, agents, users all
showed the API key's team name (e.g. "admins") instead of the
actual entity value.

Replace extractTeamIdFromApiKeyBreakdown with resolveEntityDisplay
which uses the entity key directly. For teams the teamAliasMap
still resolves a human-readable alias; for all other types the
entity key itself is the correct label.
This commit is contained in:
Ryan Crabbe 2026-03-31 10:02:42 -07:00
parent a5322c6efc
commit 9dca431989
No known key found for this signature in database

View file

@ -3,19 +3,16 @@ import type { DateRangePickerValue } from "@tremor/react";
import Papa from "papaparse"; import Papa from "papaparse";
import type { EntityBreakdown, EntitySpendData, EntityType, ExportMetadata, ExportScope } from "./types"; import type { EntityBreakdown, EntitySpendData, EntityType, ExportMetadata, ExportScope } from "./types";
// Helper function to extract team_id from api_key_breakdown // Resolve display name for an entity. For teams the teamAliasMap provides
const extractTeamIdFromApiKeyBreakdown = (apiKeyBreakdown: Record<string, any> | undefined): string | null => { // a human-readable alias; for every other entity type the entity key itself
if (!apiKeyBreakdown) return null; // (tag name, org id, customer id, …) is already the correct label.
const resolveEntityDisplay = (
// Look through all API keys to find the first non-null team_id entity: string,
for (const apiKeyData of Object.values(apiKeyBreakdown)) { teamAliasMap: Record<string, string>,
const teamId = (apiKeyData as any)?.metadata?.team_id; ): { id: string; alias: string } => ({
if (teamId) { id: entity,
return teamId; alias: teamAliasMap[entity] || entity,
} });
}
return null;
};
// Mirrors backend SpendMetrics fields (litellm/types/activity_tracking.py). // Mirrors backend SpendMetrics fields (litellm/types/activity_tracking.py).
// If the backend adds a field, add it here too. // If the backend adds a field, add it here too.
@ -68,18 +65,7 @@ export const getEntityBreakdown = (
spendData.results.forEach((day) => { spendData.results.forEach((day) => {
Object.entries(resolveEntities(day.breakdown)).forEach(([entity, data]: [string, any]) => { Object.entries(resolveEntities(day.breakdown)).forEach(([entity, data]: [string, any]) => {
// Extract team_id from api_key_breakdown metadata (not data.metadata which is empty) const { id, alias } = resolveEntityDisplay(entity, teamAliasMap);
const teamId = extractTeamIdFromApiKeyBreakdown(data.api_key_breakdown) || entity;
// Extract key_alias from the first API key that has one
const apiKeyBreakdown = data.api_key_breakdown || {};
let keyAlias: string | null = null;
for (const apiKeyData of Object.values(apiKeyBreakdown)) {
const alias = (apiKeyData as any)?.metadata?.key_alias;
if (alias) {
keyAlias = alias;
break;
}
}
if (!entitySpend[entity]) { if (!entitySpend[entity]) {
entitySpend[entity] = { entitySpend[entity] = {
@ -95,8 +81,8 @@ export const getEntityBreakdown = (
cache_creation_input_tokens: 0, cache_creation_input_tokens: 0,
}, },
metadata: { metadata: {
alias: keyAlias || teamAliasMap[teamId] || entity, alias,
id: teamId, id,
}, },
}; };
} }
@ -124,14 +110,12 @@ export const generateDailyData = (
spendData.results.forEach((day) => { spendData.results.forEach((day) => {
Object.entries(resolveEntities(day.breakdown)).forEach(([entity, data]: [string, any]) => { Object.entries(resolveEntities(day.breakdown)).forEach(([entity, data]: [string, any]) => {
// Extract team_id from api_key_breakdown metadata (not data.metadata which is empty) const { id, alias } = resolveEntityDisplay(entity, teamAliasMap);
const teamId = extractTeamIdFromApiKeyBreakdown(data.api_key_breakdown);
const teamAlias = teamId ? teamAliasMap[teamId] || null : null;
dailyBreakdown.push({ dailyBreakdown.push({
Date: day.date, Date: day.date,
[entityLabel]: teamAlias || "-", [entityLabel]: alias,
[`${entityLabel} ID`]: teamId || "-", [`${entityLabel} ID`]: id,
"Spend ($)": formatNumberWithCommas(data.metrics.spend, 4), "Spend ($)": formatNumberWithCommas(data.metrics.spend, 4),
Requests: data.metrics.api_requests, Requests: data.metrics.api_requests,
"Successful Requests": data.metrics.successful_requests, "Successful Requests": data.metrics.successful_requests,
@ -151,12 +135,12 @@ export const generateDailyWithKeysData = (
entityLabel: string, entityLabel: string,
teamAliasMap: Record<string, string> = {}, teamAliasMap: Record<string, string> = {},
): any[] => { ): any[] => {
// Aggregate by unique (Date, Team ID, Key ID) combination to prevent duplicates // Aggregate by unique (Date, Entity ID, Key ID) combination to prevent duplicates
const aggregatedData: { const aggregatedData: {
[key: string]: { [key: string]: {
Date: string; Date: string;
teamId: string; entityId: string;
teamAlias: string | null; entityAlias: string;
keyId: string; keyId: string;
keyAlias: string | null; keyAlias: string | null;
metrics: { metrics: {
@ -173,23 +157,22 @@ export const generateDailyWithKeysData = (
spendData.results.forEach((day) => { spendData.results.forEach((day) => {
Object.entries(resolveEntities(day.breakdown)).forEach(([entity, data]: [string, any]) => { Object.entries(resolveEntities(day.breakdown)).forEach(([entity, data]: [string, any]) => {
const { id: entityId, alias: entityAlias } = resolveEntityDisplay(entity, teamAliasMap);
const apiKeyBreakdown = data.api_key_breakdown || {}; const apiKeyBreakdown = data.api_key_breakdown || {};
// Iterate through each API key in the breakdown // Iterate through each API key in the breakdown
Object.entries(apiKeyBreakdown).forEach(([keyId, keyData]: [string, any]) => { Object.entries(apiKeyBreakdown).forEach(([keyId, keyData]: [string, any]) => {
const keyAlias = keyData?.metadata?.key_alias || null; const keyAlias = keyData?.metadata?.key_alias || null;
const teamId = keyData?.metadata?.team_id || entity;
const teamAlias = teamId ? teamAliasMap[teamId] || null : null;
// Create unique key for aggregation: Date_TeamID_KeyID // Create unique key for aggregation: Date_EntityID_KeyID
const uniqueKey = `${day.date}_${teamId}_${keyId}`; const uniqueKey = `${day.date}_${entityId}_${keyId}`;
if (!aggregatedData[uniqueKey]) { if (!aggregatedData[uniqueKey]) {
// First time seeing this (Date, Team ID, Key ID) combination // First time seeing this (Date, Entity ID, Key ID) combination
aggregatedData[uniqueKey] = { aggregatedData[uniqueKey] = {
Date: day.date, Date: day.date,
teamId, entityId,
teamAlias, entityAlias,
keyId, keyId,
keyAlias, keyAlias,
metrics: { metrics: {
@ -219,8 +202,8 @@ export const generateDailyWithKeysData = (
// Convert aggregated data to array format // Convert aggregated data to array format
const dailyKeyBreakdown = Object.values(aggregatedData).map((item) => ({ const dailyKeyBreakdown = Object.values(aggregatedData).map((item) => ({
Date: item.Date, Date: item.Date,
[entityLabel]: item.teamAlias || "-", [entityLabel]: item.entityAlias,
[`${entityLabel} ID`]: item.teamId || "-", [`${entityLabel} ID`]: item.entityId,
"Key Alias": item.keyAlias || "-", "Key Alias": item.keyAlias || "-",
"Key ID": item.keyId, "Key ID": item.keyId,
"Spend ($)": formatNumberWithCommas(item.metrics.spend, 4), "Spend ($)": formatNumberWithCommas(item.metrics.spend, 4),
@ -273,16 +256,13 @@ export const generateDailyWithModelsData = (
}); });
Object.entries(dailyEntityModels).forEach(([entity, models]) => { Object.entries(dailyEntityModels).forEach(([entity, models]) => {
const entityData = resolveEntities(day.breakdown)[entity]; const { id, alias } = resolveEntityDisplay(entity, teamAliasMap);
// Extract team_id from api_key_breakdown metadata (not entityData.metadata which is empty)
const teamId = extractTeamIdFromApiKeyBreakdown(entityData?.api_key_breakdown);
const teamAlias = teamId ? teamAliasMap[teamId] || null : null;
Object.entries(models).forEach(([model, metrics]: [string, any]) => { Object.entries(models).forEach(([model, metrics]: [string, any]) => {
dailyModelBreakdown.push({ dailyModelBreakdown.push({
Date: day.date, Date: day.date,
[entityLabel]: teamAlias || "-", [entityLabel]: alias,
[`${entityLabel} ID`]: teamId || "-", [`${entityLabel} ID`]: id,
Model: model, Model: model,
"Spend ($)": formatNumberWithCommas(metrics.spend, 4), "Spend ($)": formatNumberWithCommas(metrics.spend, 4),
Requests: metrics.requests, Requests: metrics.requests,