import type { ActivityItem } from '@openrouter/sdk/models/index.js'; import type { ModelStats, ProviderStats, TokenStats, UsageSummary } from './types.js'; import { combineUsageAggregates, createZeroAggregate, type LocalUsageEvent } from './types.js'; import { getUtcDateFromTimestamp } from './local-usage.js'; /** Convert a Date to YYYY-MM-DD string in UTC (matching OpenRouter API format) */ function utcISODate(date: Date): string { return date.toISOString().slice(0, 10); } export function aggregateUsage( credits: { totalUsage: number; totalCredits?: number }, analytics: ActivityItem[], timestamp: number = Date.now(), localEvents: LocalUsageEvent[] = [], ): UsageSummary { const now = new Date(); // Use UTC dates to match OpenRouter API (which uses UTC) const startOfDay = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())); const startOfWeek = new Date(startOfDay); startOfWeek.setUTCDate(startOfWeek.getUTCDate() - 7); const weekData = analytics.filter((d) => { // API dates are YYYY-MM-DD in UTC; compare by UTC date boundary return d.date >= utcISODate(startOfWeek); }); const weekFromAnalytics = sumSpend(weekData); const todayFromAnalytics = analytics .filter((d) => d.date >= utcISODate(startOfDay)) .reduce((sum, d) => sum + d.usage, 0); const month = credits.totalUsage; // Add local events to compute combined totals const weekFromLocal = localEvents .filter((e) => getUtcDateFromTimestamp(e.completedAt) >= utcISODate(startOfWeek)) .reduce((sum, e) => sum + (e.cost || 0), 0); const todayFromLocal = localEvents .filter((e) => getUtcDateFromTimestamp(e.completedAt) >= utcISODate(startOfDay)) .reduce((sum, e) => sum + (e.cost || 0), 0); const week = weekFromAnalytics + weekFromLocal; const today = todayFromAnalytics + todayFromLocal; // Merge analytics with local events for model/provider stats // Convert local events to ActivityItem-like format for existing functions const localItems = localEvents.map((e) => ({ date: getUtcDateFromTimestamp(e.completedAt), usage: e.cost || 0, promptTokens: e.promptTokens || 0, completionTokens: e.completionTokens || 0, reasoningTokens: e.reasoningTokens || 0, requests: e.requests || 1, model: e.model || 'unknown', providerName: e.provider || 'unknown', // Required fields that aren't used by our functions byokUsageInference: 0, endpointId: 'local-turns', modelPermaslug: e.model || 'unknown', })); const allData = [...analytics, ...localItems]; // Build model stats for both 7d and 30d windows // Combine weekData with local events in the 7d window const weekLocalItems = localItems.filter((e) => e.date >= utcISODate(startOfWeek)); const weekAllData = [...weekData, ...weekLocalItems]; const modelStatsMap = buildModelStats(weekAllData, allData); const topModels = Array.from(modelStatsMap.values()) .sort((a, b) => b.spend30d - a.spend30d) .slice(0, 10); const official = createZeroAggregate(); const local = createZeroAggregate(); const summary = { today, week, month, cap: credits.totalCredits ?? 0, burnRate: (week / 7) * 30, topModels, byProvider: buildProviderStats(allData), byDay: aggregateByDay(allData), timestamp, hasActivityData: true, // aggregateUsage is only called when analytics data is available officialThroughDate: undefined as string | undefined, official, local, combined: combineUsageAggregates(official, local), } as UsageSummary; return summary; } function sumSpend(data: ActivityItem[]): number { return data.reduce((sum, d) => sum + d.usage, 0); } function aggregateTokens(data: ActivityItem[]): TokenStats { return data.reduce( (acc, d) => { acc.input += d.promptTokens || 0; acc.output += d.completionTokens || 0; acc.reasoning += (d.reasoningTokens || 0) as number; acc.total += (d.promptTokens || 0) + (d.completionTokens || 0) + ((d.reasoningTokens || 0) as number); return acc; }, { input: 0, output: 0, reasoning: 0, total: 0 } as TokenStats, ); } function aggregateRequests(data: ActivityItem[]): number { return data.reduce((sum, d) => sum + (d.requests || 0), 0); } function buildModelStats(data7d: ActivityItem[], data30d: ActivityItem[]): Map { const all = new Map(); const modelNames = new Set(); // Collect all unique model names from both time windows for (const d of data30d) modelNames.add(d.model); for (const d of data7d) modelNames.add(d.model); for (const name of modelNames) { const data7dForModel = data7d.filter((d) => d.model === name); const data30dForModel = data30d.filter((d) => d.model === name); all.set(name, { name, spend7d: data7dForModel.reduce((s, d) => s + d.usage, 0), spend30d: data30dForModel.reduce((s, d) => s + d.usage, 0), tokens7d: aggregateTokens(data7dForModel), tokens30d: aggregateTokens(data30dForModel), requests7d: aggregateRequests(data7dForModel), requests30d: aggregateRequests(data30dForModel), }); } return all; } function buildProviderStats(data: ActivityItem[]): ProviderStats[] { const byName = new Map(); for (const d of data) { const existing = byName.get(d.providerName) || []; existing.push(d); byName.set(d.providerName, existing); } const stats: ProviderStats[] = []; for (const [name, items] of byName) { stats.push({ name, spend: items.reduce((s, d) => s + d.usage, 0), tokens: aggregateTokens(items), requests: aggregateRequests(items), }); } return stats.sort((a, b) => b.spend - a.spend); } function aggregateByDay(data: ActivityItem[]): Record { const byDay: Record = {}; for (const d of data) { const day = d.date; byDay[day] = (byDay[day] || 0) + d.usage; } return byDay; }