fix(dashboard): label usage-by-provider column as Provider / API Key
This commit is contained in:
@@ -280,7 +280,7 @@ const ENDPOINT_COLUMNS = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const PROVIDER_COLUMNS = [
|
const PROVIDER_COLUMNS = [
|
||||||
{ field: "provider", label: "Provider" },
|
{ field: "provider", label: "Provider / API Key" },
|
||||||
{ field: "requests", label: "Requests", align: "right" },
|
{ field: "requests", label: "Requests", align: "right" },
|
||||||
{ field: "lastUsed", label: "Last Used", 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
|
// Resolve a provider key (built-in id, alias, or custom node id like
|
||||||
// "openai-compatible-chat-096baf9a-...") to a human-friendly name.
|
// "openai-compatible-chat-096baf9a-...") to a human-friendly name.
|
||||||
// Prefers the connected-providers state (nodeName > connection name)
|
// Prefers the canonical provider name from the registry so e.g. "commandcode"
|
||||||
// and falls back to the module-level registry lookup.
|
// 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) => {
|
const getProviderLabel = useCallback((providerId) => {
|
||||||
if (!providerId) return "Unknown Provider";
|
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);
|
const match = providers.find((p) => p.provider === providerId || p.id === providerId);
|
||||||
if (match) return match.nodeName || match.name || providerId;
|
if (match) return match.nodeName || match.name || providerId;
|
||||||
return resolveProviderLabel(providerId);
|
return resolveProviderLabel(providerId);
|
||||||
@@ -591,12 +594,55 @@ export default function UsageStats({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
case "provider": {
|
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 {
|
return {
|
||||||
columns: PROVIDER_COLUMNS,
|
columns: PROVIDER_COLUMNS,
|
||||||
groupedData: groupDataByKey(
|
groupedData,
|
||||||
sortData(stats.byProvider, {}, sortBy, sortOrder),
|
|
||||||
"provider",
|
|
||||||
),
|
|
||||||
storageKey: "usage-stats:expanded-providers",
|
storageKey: "usage-stats:expanded-providers",
|
||||||
emptyMessage: "No usage recorded yet.",
|
emptyMessage: "No usage recorded yet.",
|
||||||
renderSummaryCells: (group) => (
|
renderSummaryCells: (group) => (
|
||||||
@@ -612,7 +658,7 @@ export default function UsageStats({
|
|||||||
renderDetailCells: (item) => (
|
renderDetailCells: (item) => (
|
||||||
<>
|
<>
|
||||||
<td className="px-6 py-3 font-medium">
|
<td className="px-6 py-3 font-medium">
|
||||||
{getProviderLabel(item.provider || item.key)}
|
{item.keyName}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-3 text-right">{fmt(item.requests)}</td>
|
<td className="px-6 py-3 text-right">{fmt(item.requests)}</td>
|
||||||
<td className="px-6 py-3 text-right text-text-muted whitespace-nowrap">
|
<td className="px-6 py-3 text-right text-text-muted whitespace-nowrap">
|
||||||
@@ -664,7 +710,7 @@ export default function UsageStats({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [stats, tableView, sortBy, sortOrder, getProviderLabel]);
|
}, [stats, tableView, sortBy, sortOrder, getProviderLabel, providers]);
|
||||||
|
|
||||||
if (!stats && !loading)
|
if (!stats && !loading)
|
||||||
return (
|
return (
|
||||||
|
|||||||
Reference in New Issue
Block a user