diff --git a/src/shared/components/UsageStats.js b/src/shared/components/UsageStats.js index e29c5ca3..3dd8c5b8 100644 --- a/src/shared/components/UsageStats.js +++ b/src/shared/components/UsageStats.js @@ -280,7 +280,7 @@ const ENDPOINT_COLUMNS = [ ]; const PROVIDER_COLUMNS = [ - { field: "provider", label: "Provider" }, + { field: "provider", label: "Provider / API Key" }, { field: "requests", label: "Requests", align: "right" }, { field: "lastUsed", label: "Last Used", align: "right" }, ]; @@ -321,10 +321,13 @@ export default function UsageStats({ // Resolve a provider key (built-in id, alias, or custom node id like // "openai-compatible-chat-096baf9a-...") to a human-friendly name. - // Prefers the connected-providers state (nodeName > connection name) - // and falls back to the module-level registry lookup. + // Prefers the canonical provider name from the registry so e.g. "commandcode" + // shows "Command Code" instead of a connection/account name; falls back to + // the connected-providers state (nodeName > connection name) for custom nodes. const getProviderLabel = useCallback((providerId) => { if (!providerId) return "Unknown Provider"; + const known = getProviderByAlias(providerId) || AI_PROVIDERS[providerId] || FREE_PROVIDERS[providerId]; + if (known?.name) return known.name; const match = providers.find((p) => p.provider === providerId || p.id === providerId); if (match) return match.nodeName || match.name || providerId; return resolveProviderLabel(providerId); @@ -591,12 +594,55 @@ export default function UsageStats({ }; } case "provider": { + // Detail rows = the API keys used per provider (grouped from byApiKey), + // so expanding a provider shows its keys instead of repeating the provider. + // byApiKey[].provider is the display name (node name for custom nodes); + // map display names back to provider ids via the connected-providers state. + const displayToId = {}; + for (const p of providers) { + if (p.nodeName) displayToId[p.nodeName] = p.provider; + if (p.name && !displayToId[p.name]) displayToId[p.name] = p.provider; + } + const keyGroupsByProvider = {}; + for (const ak of Object.values(sortData(stats.byApiKey, {}, sortBy, sortOrder))) { + const pid = displayToId[ak.provider] || ak.provider || "unknown"; + const kname = ak.keyName || ak.apiKeyMasked || "Unknown Key"; + const bucket = keyGroupsByProvider[pid] || (keyGroupsByProvider[pid] = {}); + const g = bucket[kname] || (bucket[kname] = { + requests: 0, + promptTokens: 0, + completionTokens: 0, + cachedTokens: 0, + totalTokens: 0, + cost: 0, + inputCost: 0, + cachedCost: 0, + outputCost: 0, + lastUsed: null, + keyName: kname, + }); + g.requests += ak.requests || 0; + g.promptTokens += ak.promptTokens || 0; + g.completionTokens += ak.completionTokens || 0; + g.cachedTokens += ak.cachedTokens || 0; + g.totalTokens += ak.totalTokens || 0; + g.cost += ak.cost || 0; + g.inputCost += ak.inputCost || 0; + g.cachedCost += ak.cachedCost || 0; + g.outputCost += ak.outputCost || 0; + if (ak.lastUsed && (!g.lastUsed || new Date(ak.lastUsed) > new Date(g.lastUsed))) + g.lastUsed = ak.lastUsed; + } + const groupedData = groupDataByKey( + sortData(stats.byProvider, {}, sortBy, sortOrder), + "provider", + ).map((group) => ({ + ...group, + items: Object.values(keyGroupsByProvider[group.groupKey] || {}), + })); return { columns: PROVIDER_COLUMNS, - groupedData: groupDataByKey( - sortData(stats.byProvider, {}, sortBy, sortOrder), - "provider", - ), + groupedData, storageKey: "usage-stats:expanded-providers", emptyMessage: "No usage recorded yet.", renderSummaryCells: (group) => ( @@ -612,7 +658,7 @@ export default function UsageStats({ renderDetailCells: (item) => ( <>