From 63c484d0628a50c5ce4ecd706b2f35e34f119ab0 Mon Sep 17 00:00:00 2001 From: "Andrew B." <37745667+AndrianBalanescu@users.noreply.github.com> Date: Wed, 5 Aug 2026 21:52:38 -0500 Subject: [PATCH] fix(usage): aggregate provider window costs in SQL (#8892) Validated in local merge-train T4 (HouMinXi+Zartharas+Andrian+artickc) --- src/lib/usage/providerWindowCosts.ts | 455 +++++++++++++++++------ tests/unit/provider-window-costs.test.ts | 35 ++ 2 files changed, 383 insertions(+), 107 deletions(-) diff --git a/src/lib/usage/providerWindowCosts.ts b/src/lib/usage/providerWindowCosts.ts index c92f5dcead..bc62c65080 100644 --- a/src/lib/usage/providerWindowCosts.ts +++ b/src/lib/usage/providerWindowCosts.ts @@ -1,3 +1,11 @@ +/** + * @file providerWindowCosts.ts + * @description Provider weekly USD cost breakdown for the dashboard modal. + * + * @changes + * - [2026-07-24] [Composer] - Aggregate usage_history in SQL instead of loading all rows into JS + */ + import { getCostSummary } from "@/domain/costRules"; import { getApiKeys } from "@/lib/db/apiKeys"; import { getDbInstance } from "@/lib/db/core"; @@ -10,6 +18,27 @@ const RECORDED_COST_MATCH_TOLERANCE_MS = 30_000; type JsonRecord = Record; +interface UsageHistoryFilter { + whereSql: string; + params: Record; +} + +interface AggregatedUsageCostRow { + apiKeyId: string | null; + apiKeyName: string | null; + provider: string; + model: string; + serviceTier: string; + requests: number; + promptTokens: number; + completionTokens: number; + cacheReadTokens: number; + cacheCreationTokens: number; + reasoningTokens: number; + totalTokens: number; + lastUsed: string | null; +} + interface UsageCostRow { id: number; apiKeyId: string | null; @@ -26,6 +55,11 @@ interface UsageCostRow { timestamp: string | null; } +interface RecordedCostSummary { + totalCost: number; + entryCount: number; +} + interface RecordedCostRow { rowId: number; apiKeyId: string; @@ -264,7 +298,7 @@ async function getCurrentApiKeyNames(): Promise> { return names; } -function uniqueApiKeyIds(rows: UsageCostRow[]): string[] { +function uniqueApiKeyIds(rows: Array<{ apiKeyId: string | null }>): string[] { return Array.from( new Set( rows @@ -274,6 +308,162 @@ function uniqueApiKeyIds(rows: UsageCostRow[]): string[] { ); } +function buildUsageHistoryFilter( + providerKey: string, + windowStartAt: string, + nowIso: string, + windowResetAt: string | null, + connectionId: string | null +): UsageHistoryFilter { + const where = [ + "LOWER(provider) = @provider", + "timestamp >= @since", + "timestamp <= @nowIso", + "COALESCE(success, 1) = 1", + ]; + const params: Record = { + provider: providerKey, + since: windowStartAt, + nowIso, + }; + if (windowResetAt) { + where.push("timestamp < @resetAt"); + params.resetAt = windowResetAt; + } + if (connectionId) { + where.push("connection_id = @connectionId"); + params.connectionId = connectionId; + } + return { whereSql: where.join(" AND "), params }; +} + +function fetchAggregatedUsageRows(filter: UsageHistoryFilter): AggregatedUsageCostRow[] { + return getDbInstance() + .prepare( + ` + SELECT + NULLIF(api_key_id, '') as apiKeyId, + NULLIF(api_key_name, '') as apiKeyName, + LOWER(provider) as provider, + LOWER(model) as model, + COALESCE(NULLIF(service_tier, ''), 'standard') as serviceTier, + COUNT(*) as requests, + COALESCE(SUM(tokens_input), 0) as promptTokens, + COALESCE(SUM(tokens_output), 0) as completionTokens, + COALESCE(SUM(tokens_cache_read), 0) as cacheReadTokens, + COALESCE(SUM(tokens_cache_creation), 0) as cacheCreationTokens, + COALESCE(SUM(tokens_reasoning), 0) as reasoningTokens, + COALESCE(SUM(tokens_input + tokens_output), 0) as totalTokens, + MAX(timestamp) as lastUsed + FROM usage_history + WHERE ${filter.whereSql} + GROUP BY + COALESCE(NULLIF(api_key_id, ''), ''), + COALESCE(NULLIF(api_key_name, ''), ''), + LOWER(provider), + LOWER(model), + COALESCE(NULLIF(service_tier, ''), 'standard') + ORDER BY totalTokens DESC + ` + ) + .all(filter.params); +} + +function fetchUsageRequestCountByApiKey(filter: UsageHistoryFilter): Map { + const rows = getDbInstance() + .prepare<{ apiKeyId: string; requestCount: number }>( + ` + SELECT + COALESCE(NULLIF(api_key_id, ''), '') as apiKeyId, + COUNT(*) as requestCount + FROM usage_history + WHERE ${filter.whereSql} + GROUP BY COALESCE(NULLIF(api_key_id, ''), '') + ` + ) + .all(filter.params); + + const counts = new Map(); + for (const row of rows) { + if (!row.apiKeyId) continue; + counts.set(row.apiKeyId, toNumber(row.requestCount)); + } + return counts; +} + +function fetchDetailedUsageRowsForApiKey( + filter: UsageHistoryFilter, + apiKeyId: string +): UsageCostRow[] { + return getDbInstance() + .prepare( + ` + SELECT + id, + NULLIF(api_key_id, '') as apiKeyId, + NULLIF(api_key_name, '') as apiKeyName, + LOWER(provider) as provider, + LOWER(model) as model, + COALESCE(NULLIF(service_tier, ''), 'standard') as serviceTier, + COALESCE(tokens_input, 0) as promptTokens, + COALESCE(tokens_output, 0) as completionTokens, + COALESCE(tokens_cache_read, 0) as cacheReadTokens, + COALESCE(tokens_cache_creation, 0) as cacheCreationTokens, + COALESCE(tokens_reasoning, 0) as reasoningTokens, + COALESCE(tokens_input + tokens_output, 0) as totalTokens, + timestamp + FROM usage_history + WHERE ${filter.whereSql} + AND api_key_id = @apiKeyId + ORDER BY timestamp ASC, id ASC + ` + ) + .all({ ...filter.params, apiKeyId }); +} + +function getRecordedCostSummariesByApiKey( + apiKeyIds: string[], + sinceMs: number, + untilMs: number +): Map { + if (apiKeyIds.length === 0) return new Map(); + + try { + const params: Record = { + sinceMs: Math.max(0, sinceMs - RECORDED_COST_MATCH_TOLERANCE_MS), + untilMs: untilMs + RECORDED_COST_MATCH_TOLERANCE_MS, + }; + const placeholders = appendNamedPlaceholders(params, "apiKey", apiKeyIds); + const rows = getDbInstance() + .prepare<{ apiKeyId: string; totalCost: number; entryCount: number }>( + ` + SELECT + api_key_id as apiKeyId, + COALESCE(SUM(cost), 0) as totalCost, + COUNT(*) as entryCount + FROM domain_cost_history + WHERE api_key_id IN (${placeholders}) + AND timestamp >= @sinceMs + AND timestamp <= @untilMs + GROUP BY api_key_id + ` + ) + .all(params); + + const summaries = new Map(); + for (const row of rows) { + if (!row.apiKeyId) continue; + summaries.set(row.apiKeyId, { + totalCost: Math.max(0, toNumber(row.totalCost)), + entryCount: toNumber(row.entryCount), + }); + } + return summaries; + } catch { + return new Map(); + } +} + function appendNamedPlaceholders( params: Record, prefix: string, @@ -387,6 +577,75 @@ async function getUsageRowCostUsd( ); } +async function getAggregatedGroupCostUsd(row: AggregatedUsageCostRow): Promise { + return roundUsd( + await calculateCost( + row.provider, + row.model, + { + input: toNumber(row.promptTokens), + output: toNumber(row.completionTokens), + cacheRead: toNumber(row.cacheReadTokens), + cacheCreation: toNumber(row.cacheCreationTokens), + reasoning: toNumber(row.reasoningTokens), + }, + { serviceTier: row.serviceTier } + ) + ); +} + +function distributeRecordedCostAcrossGroups( + groups: AggregatedUsageCostRow[], + recordedTotalUsd: number, + groupCalculatedCosts: number[] +): number[] { + const calculatedTotal = groupCalculatedCosts.reduce((sum, value) => sum + value, 0); + if (calculatedTotal <= 0) { + const evenShare = recordedTotalUsd / Math.max(groups.length, 1); + return groups.map(() => roundUsd(evenShare)); + } + return groupCalculatedCosts.map((value) => + roundUsd((recordedTotalUsd * value) / calculatedTotal) + ); +} + +async function buildApiKeyCostAllocations(args: { + groups: AggregatedUsageCostRow[]; + filter: UsageHistoryFilter; + usageRequestCount: number; + recordedSummary: RecordedCostSummary | undefined; + recordedCostsByApiKey: Map; + usedRecordedRows: Set; +}): Promise { + const calculatedCosts = await Promise.all( + args.groups.map((group) => getAggregatedGroupCostUsd(group)) + ); + + if (!args.recordedSummary || args.recordedSummary.entryCount <= 0) { + return calculatedCosts; + } + + if (args.recordedSummary.entryCount === args.usageRequestCount) { + return distributeRecordedCostAcrossGroups( + args.groups, + args.recordedSummary.totalCost, + calculatedCosts + ); + } + + const apiKeyId = args.groups[0]?.apiKeyId; + if (!apiKeyId) return calculatedCosts; + + const detailedRows = fetchDetailedUsageRowsForApiKey(args.filter, apiKeyId); + const rowCosts = await Promise.all( + detailedRows.map((row) => + getUsageRowCostUsd(row, args.recordedCostsByApiKey, args.usedRecordedRows) + ) + ); + const detailedTotal = roundUsd(rowCosts.reduce((sum, value) => sum + value, 0)); + return distributeRecordedCostAcrossGroups(args.groups, detailedTotal, calculatedCosts); +} + export async function getProviderWindowCostBreakdown({ provider, connectionId = null, @@ -402,130 +661,112 @@ export async function getProviderWindowCostBreakdown({ const windowStartAt = new Date(window.startMs).toISOString(); const windowResetAt = window.resetMs ? new Date(window.resetMs).toISOString() : null; const nowIso = new Date(nowMs).toISOString(); - - const where = [ - "LOWER(provider) = @provider", - "timestamp >= @since", - "timestamp <= @nowIso", - "COALESCE(success, 1) = 1", - ]; - const params: Record = { - provider: providerKey, - since: windowStartAt, + const filter = buildUsageHistoryFilter( + providerKey, + windowStartAt, nowIso, - }; - if (windowResetAt) { - where.push("timestamp < @resetAt"); - params.resetAt = windowResetAt; - } - if (connectionId) { - where.push("connection_id = @connectionId"); - params.connectionId = connectionId; - } - - const usageRows = getDbInstance() - .prepare( - ` - SELECT - id, - NULLIF(api_key_id, '') as apiKeyId, - NULLIF(api_key_name, '') as apiKeyName, - LOWER(provider) as provider, - LOWER(model) as model, - COALESCE(NULLIF(service_tier, ''), 'standard') as serviceTier, - COALESCE(tokens_input, 0) as promptTokens, - COALESCE(tokens_output, 0) as completionTokens, - COALESCE(tokens_cache_read, 0) as cacheReadTokens, - COALESCE(tokens_cache_creation, 0) as cacheCreationTokens, - COALESCE(tokens_reasoning, 0) as reasoningTokens, - COALESCE(tokens_input + tokens_output, 0) as totalTokens, - timestamp - FROM usage_history - WHERE ${where.join(" AND ")} - ORDER BY timestamp ASC, id ASC - ` - ) - .all(params); - - const currentApiKeyNames = await getCurrentApiKeyNames(); - const recordedCostsByApiKey = getRecordedCostsByApiKey( - uniqueApiKeyIds(usageRows), - window.startMs, - nowMs + windowResetAt, + connectionId ); + + const aggregatedRows = fetchAggregatedUsageRows(filter); + const usageRequestCounts = fetchUsageRequestCountByApiKey(filter); + const currentApiKeyNames = await getCurrentApiKeyNames(); + const apiKeyIds = uniqueApiKeyIds(aggregatedRows); + const recordedSummaries = getRecordedCostSummariesByApiKey(apiKeyIds, window.startMs, nowMs); + const recordedCostsByApiKey = getRecordedCostsByApiKey(apiKeyIds, window.startMs, nowMs); const usedRecordedRows = new Set(); const byApiKey = new Map(); - for (const row of usageRows) { - const apiKeyId = row.apiKeyId || null; - const apiKeyName = row.apiKeyName || null; - const apiKeyKey = makeApiKeyKey(apiKeyId, apiKeyName); + const groupsByApiKey = new Map(); + for (const row of aggregatedRows) { + const apiKeyKey = makeApiKeyKey(row.apiKeyId, row.apiKeyName); + const list = groupsByApiKey.get(apiKeyKey) ?? []; + list.push(row); + groupsByApiKey.set(apiKeyKey, list); + } + + for (const [apiKeyKey, groups] of groupsByApiKey.entries()) { + const first = groups[0]; + const apiKeyId = first.apiKeyId || null; + const apiKeyName = first.apiKeyName || null; const displayName = (apiKeyId ? currentApiKeyNames.get(apiKeyId) : null) || apiKeyName || apiKeyId || "Unattributed"; - const costUsd = roundUsd( - await getUsageRowCostUsd(row, recordedCostsByApiKey, usedRecordedRows) - ); + const usageRequestCount = apiKeyId + ? (usageRequestCounts.get(apiKeyId) ?? 0) + : groups.reduce((sum, group) => sum + toNumber(group.requests), 0); + const groupCosts = await buildApiKeyCostAllocations({ + groups, + filter, + usageRequestCount, + recordedSummary: apiKeyId ? recordedSummaries.get(apiKeyId) : undefined, + recordedCostsByApiKey, + usedRecordedRows, + }); - let aggregate = byApiKey.get(apiKeyKey); - if (!aggregate) { - let limitUsd: number | null = null; - let limitPeriod: string | null = null; - let budgetResetAt: string | null = null; - if (apiKeyId) { - const summary = getCostSummary(apiKeyId); - if (summary.activeLimitUsd > 0) { - limitUsd = summary.activeLimitUsd; - limitPeriod = summary.resetInterval; - budgetResetAt = - typeof summary.nextResetAt === "number" && Number.isFinite(summary.nextResetAt) - ? new Date(summary.nextResetAt).toISOString() - : null; - } + let limitUsd: number | null = null; + let limitPeriod: string | null = null; + let budgetResetAt: string | null = null; + if (apiKeyId) { + const summary = getCostSummary(apiKeyId); + if (summary.activeLimitUsd > 0) { + limitUsd = summary.activeLimitUsd; + limitPeriod = summary.resetInterval; + budgetResetAt = + typeof summary.nextResetAt === "number" && Number.isFinite(summary.nextResetAt) + ? new Date(summary.nextResetAt).toISOString() + : null; } - aggregate = { - apiKeyKey, - apiKeyId, - apiKeyName: displayName, - requests: 0, - promptTokens: 0, - completionTokens: 0, - totalTokens: 0, - costUsd: 0, - limitUsd, - limitPeriod, - limitUsedPercent: null, - budgetResetAt, - lastUsed: null, - models: [], - modelMap: new Map(), - }; - byApiKey.set(apiKeyKey, aggregate); } - aggregate.requests += 1; - aggregate.promptTokens += toNumber(row.promptTokens); - aggregate.completionTokens += toNumber(row.completionTokens); - aggregate.totalTokens += toNumber(row.totalTokens); - aggregate.costUsd = roundUsd(aggregate.costUsd + costUsd); - if (!aggregate.lastUsed || (row.timestamp && row.timestamp > aggregate.lastUsed)) { - aggregate.lastUsed = row.timestamp || aggregate.lastUsed; - } - const modelKey = `${row.provider}\0${row.model}\0${row.serviceTier}`; - const model = aggregate.modelMap.get(modelKey) ?? { - model: row.model, - provider: row.provider, - serviceTier: row.serviceTier, + const aggregate: ProviderWindowCostAggregateRow = { + apiKeyKey, + apiKeyId, + apiKeyName: displayName, requests: 0, + promptTokens: 0, + completionTokens: 0, totalTokens: 0, costUsd: 0, + limitUsd, + limitPeriod, + limitUsedPercent: null, + budgetResetAt, + lastUsed: null, + models: [], + modelMap: new Map(), }; - model.requests += 1; - model.totalTokens += toNumber(row.totalTokens); - model.costUsd = roundUsd(model.costUsd + costUsd); - aggregate.modelMap.set(modelKey, model); + + groups.forEach((row, index) => { + const costUsd = groupCosts[index] ?? 0; + aggregate.requests += toNumber(row.requests); + aggregate.promptTokens += toNumber(row.promptTokens); + aggregate.completionTokens += toNumber(row.completionTokens); + aggregate.totalTokens += toNumber(row.totalTokens); + aggregate.costUsd = roundUsd(aggregate.costUsd + costUsd); + if (!aggregate.lastUsed || (row.lastUsed && row.lastUsed > aggregate.lastUsed)) { + aggregate.lastUsed = row.lastUsed || aggregate.lastUsed; + } + + const modelKey = `${row.provider}\0${row.model}\0${row.serviceTier}`; + const model = aggregate.modelMap.get(modelKey) ?? { + model: row.model, + provider: row.provider, + serviceTier: row.serviceTier, + requests: 0, + totalTokens: 0, + costUsd: 0, + }; + model.requests += toNumber(row.requests); + model.totalTokens += toNumber(row.totalTokens); + model.costUsd = roundUsd(model.costUsd + costUsd); + aggregate.modelMap.set(modelKey, model); + }); + + byApiKey.set(apiKeyKey, aggregate); } const breakdownRows = Array.from(byApiKey.values()) diff --git a/tests/unit/provider-window-costs.test.ts b/tests/unit/provider-window-costs.test.ts index bca518644d..63ac2e23ec 100644 --- a/tests/unit/provider-window-costs.test.ts +++ b/tests/unit/provider-window-costs.test.ts @@ -450,3 +450,38 @@ test("provider window costs prefer recorded USD history over repricing usage tok assert.equal(result.rows[0].apiKeyName, "Recorded USD Key"); assert.equal(result.rows[0].costUsd, 17); }); + +test("provider window costs aggregate many usage rows in SQL without per-row scans", async () => { + await localDb.updatePricing({ + "featherless-ai": { + "glm-5.2": { input: 1, output: 1, cached: 0, cache_creation: 0, reasoning: 0 }, + }, + }); + + const key = await apiKeys.createApiKey("Featherless Key", "machine-featherless-agg"); + + for (let index = 0; index < 250; index += 1) { + await usageHistory.saveRequestUsage({ + provider: "featherless-ai", + model: "glm-5.2", + connectionId: "featherless-conn", + apiKeyId: key.id, + apiKeyName: "Featherless Key", + tokens: { input: 1_000, output: 0 }, + timestamp: new Date(Date.parse("2026-06-26T10:00:00.000Z") + index * 1_000).toISOString(), + }); + } + + const result = await getProviderWindowCostBreakdown({ + provider: "featherless-ai", + connectionId: "featherless-conn", + now: Date.parse("2026-06-28T12:00:00.000Z"), + }); + + assert.equal(result.rows.length, 1); + assert.equal(result.rows[0].requests, 250); + assert.equal(result.rows[0].promptTokens, 250_000); + assert.equal(result.rows[0].costUsd, 0.25); + assert.equal(result.rows[0].models.length, 1); + assert.equal(result.rows[0].models[0].requests, 250); +});