fix(costs): guard byProject against duplicate null keys, memoize ProviderQuotaCard row aggregations
This commit is contained in:
@@ -33,19 +33,51 @@ export function ProviderQuotaCard({
|
|||||||
showDeficitNotch,
|
showDeficitNotch,
|
||||||
quotaWindows = [],
|
quotaWindows = [],
|
||||||
}: ProviderQuotaCardProps) {
|
}: ProviderQuotaCardProps) {
|
||||||
const totalInputTokens = rows.reduce((s, r) => s + r.inputTokens, 0);
|
// single-pass aggregation over rows — memoized so the 8 derived values are not
|
||||||
const totalOutputTokens = rows.reduce((s, r) => s + r.outputTokens, 0);
|
// recomputed on every parent render tick (providers tab polls every 30s, and each
|
||||||
const totalTokens = totalInputTokens + totalOutputTokens;
|
// card is mounted twice: once in the "all" tab grid and once in its per-provider tab).
|
||||||
const totalCostCents = rows.reduce((s, r) => s + r.costCents, 0);
|
const totals = useMemo(() => {
|
||||||
const totalApiRuns = rows.reduce((s, r) => s + r.apiRunCount, 0);
|
let inputTokens = 0, outputTokens = 0, costCents = 0;
|
||||||
const totalSubRuns = rows.reduce((s, r) => s + r.subscriptionRunCount, 0);
|
let apiRunCount = 0, subRunCount = 0, subInputTokens = 0, subOutputTokens = 0;
|
||||||
const totalSubInputTokens = rows.reduce((s, r) => s + r.subscriptionInputTokens, 0);
|
for (const r of rows) {
|
||||||
const totalSubOutputTokens = rows.reduce((s, r) => s + r.subscriptionOutputTokens, 0);
|
inputTokens += r.inputTokens;
|
||||||
const totalSubTokens = totalSubInputTokens + totalSubOutputTokens;
|
outputTokens += r.outputTokens;
|
||||||
|
costCents += r.costCents;
|
||||||
|
apiRunCount += r.apiRunCount;
|
||||||
|
subRunCount += r.subscriptionRunCount;
|
||||||
|
subInputTokens += r.subscriptionInputTokens;
|
||||||
|
subOutputTokens += r.subscriptionOutputTokens;
|
||||||
|
}
|
||||||
|
const totalTokens = inputTokens + outputTokens;
|
||||||
|
const subTokens = subInputTokens + subOutputTokens;
|
||||||
|
// denominator: api-billed tokens (from cost_events) + subscription tokens (from heartbeat_runs)
|
||||||
|
const allTokens = totalTokens + subTokens;
|
||||||
|
return {
|
||||||
|
totalInputTokens: inputTokens,
|
||||||
|
totalOutputTokens: outputTokens,
|
||||||
|
totalTokens,
|
||||||
|
totalCostCents: costCents,
|
||||||
|
totalApiRuns: apiRunCount,
|
||||||
|
totalSubRuns: subRunCount,
|
||||||
|
totalSubInputTokens: subInputTokens,
|
||||||
|
totalSubOutputTokens: subOutputTokens,
|
||||||
|
totalSubTokens: subTokens,
|
||||||
|
subSharePct: allTokens > 0 ? (subTokens / allTokens) * 100 : 0,
|
||||||
|
};
|
||||||
|
}, [rows]);
|
||||||
|
|
||||||
// sub share = sub tokens / (api tokens + sub tokens)
|
const {
|
||||||
const allTokens = totalTokens + totalSubTokens;
|
totalInputTokens,
|
||||||
const subSharePct = allTokens > 0 ? (totalSubTokens / allTokens) * 100 : 0;
|
totalOutputTokens,
|
||||||
|
totalTokens,
|
||||||
|
totalCostCents,
|
||||||
|
totalApiRuns,
|
||||||
|
totalSubRuns,
|
||||||
|
totalSubInputTokens,
|
||||||
|
totalSubOutputTokens,
|
||||||
|
totalSubTokens,
|
||||||
|
subSharePct,
|
||||||
|
} = totals;
|
||||||
|
|
||||||
// budget bars: use this provider's own spend vs its pro-rata share of budget
|
// budget bars: use this provider's own spend vs its pro-rata share of budget
|
||||||
// pro-rata: if a provider is 40% of total spend, it gets 40% of the budget allocated.
|
// pro-rata: if a provider is 40% of total spend, it gets 40% of the budget allocated.
|
||||||
|
|||||||
@@ -486,7 +486,7 @@ export function Costs() {
|
|||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{spendData.byProject.map((row, i) => (
|
{spendData.byProject.map((row, i) => (
|
||||||
<div
|
<div
|
||||||
key={row.projectId ?? "unattributed"}
|
key={row.projectId ?? `unattributed-${i}`}
|
||||||
className="flex items-center justify-between text-sm"
|
className="flex items-center justify-between text-sm"
|
||||||
>
|
>
|
||||||
<span className="truncate">
|
<span className="truncate">
|
||||||
|
|||||||
Reference in New Issue
Block a user