| |
| |
| |
| |
| |
| |
|
|
| import { calculateCost } from "@/lib/usageDb"; |
|
|
| |
| |
| |
| |
| |
| function getDateRange(range: string) { |
| const end = new Date(); |
| let start; |
|
|
| switch (range) { |
| case "1d": |
| start = new Date(end); |
| start.setDate(start.getDate() - 1); |
| break; |
| case "7d": |
| start = new Date(end); |
| start.setDate(start.getDate() - 7); |
| break; |
| case "30d": |
| start = new Date(end); |
| start.setDate(start.getDate() - 30); |
| break; |
| case "90d": |
| start = new Date(end); |
| start.setDate(start.getDate() - 90); |
| break; |
| case "ytd": |
| start = new Date(end.getFullYear(), 0, 1); |
| break; |
| case "all": |
| default: |
| start = new Date(0); |
| break; |
| } |
|
|
| return { start, end }; |
| } |
|
|
| |
| |
| |
| function toDateKey(date: Date) { |
| const y = date.getFullYear(); |
| const m = String(date.getMonth() + 1).padStart(2, "0"); |
| const d = String(date.getDate()).padStart(2, "0"); |
| return `${y}-${m}-${d}`; |
| } |
|
|
| |
| |
| |
| function shortModelName(model: string) { |
| if (!model) return "unknown"; |
| |
| const parts = model.split("/"); |
| return parts[parts.length - 1] || model; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| export async function computeAnalytics( |
| history: any[], |
| range = "30d", |
| connectionMap: Record<string, string> = {} |
| ) { |
| const { start, end } = getDateRange(range); |
|
|
| |
| const entries = history.filter((e) => { |
| const t = new Date(e.timestamp); |
| return t >= start && t <= end; |
| }); |
|
|
| |
| const summary = { |
| totalTokens: 0, |
| promptTokens: 0, |
| completionTokens: 0, |
| totalCost: 0, |
| totalRequests: entries.length, |
| uniqueModels: new Set<string>(), |
| uniqueAccounts: new Set<string>(), |
| uniqueApiKeys: new Set<string>(), |
| }; |
|
|
| |
| const dailyMap: Record<string, any> = {}; |
| const dailyByModelMap: Record<string, Record<string, number>> = {}; |
|
|
| |
| const heatmapStart = new Date(); |
| heatmapStart.setDate(heatmapStart.getDate() - 364); |
| const activityMap: Record<string, number> = {}; |
|
|
| |
| const byModelMap: Record<string, any> = {}; |
| const byAccountMap: Record<string, any> = {}; |
| const byProviderMap: Record<string, any> = {}; |
| const byApiKeyMap: Record<string, any> = {}; |
|
|
| |
| const weeklyTokens = [0, 0, 0, 0, 0, 0, 0]; |
| const weeklyCounts = [0, 0, 0, 0, 0, 0, 0]; |
|
|
| |
| for (const entry of history) { |
| const entryDate = new Date(entry.timestamp); |
| if (entryDate >= heatmapStart) { |
| const key = toDateKey(entryDate); |
| const tokens = |
| (entry.tokens?.input ?? entry.tokens?.prompt_tokens ?? 0) + |
| (entry.tokens?.output ?? entry.tokens?.completion_tokens ?? 0); |
| activityMap[key] = (activityMap[key] || 0) + tokens; |
| } |
| } |
|
|
| |
| for (const entry of entries) { |
| const pt = entry.tokens?.input ?? entry.tokens?.prompt_tokens ?? 0; |
| const ct = entry.tokens?.output ?? entry.tokens?.completion_tokens ?? 0; |
| const totalTkns = pt + ct; |
| const entryDate = new Date(entry.timestamp); |
| const dateKey = toDateKey(entryDate); |
| const dayOfWeek = entryDate.getDay(); |
| const modelShort = shortModelName(entry.model); |
|
|
| |
| let cost = 0; |
| try { |
| cost = await calculateCost(entry.provider, entry.model, entry.tokens); |
| } catch { |
| |
| } |
|
|
| |
| summary.promptTokens += pt; |
| summary.completionTokens += ct; |
| summary.totalTokens += totalTkns; |
| summary.totalCost += cost; |
| if (entry.model) summary.uniqueModels.add(modelShort); |
| if (entry.connectionId) summary.uniqueAccounts.add(entry.connectionId); |
| if (entry.apiKeyId || entry.apiKeyName) { |
| summary.uniqueApiKeys.add(entry.apiKeyId || entry.apiKeyName); |
| } |
|
|
| |
| if (!dailyMap[dateKey]) { |
| dailyMap[dateKey] = { |
| date: dateKey, |
| requests: 0, |
| promptTokens: 0, |
| completionTokens: 0, |
| cost: 0, |
| }; |
| } |
| dailyMap[dateKey].requests++; |
| dailyMap[dateKey].promptTokens += pt; |
| dailyMap[dateKey].completionTokens += ct; |
| dailyMap[dateKey].cost += cost; |
|
|
| |
| if (!dailyByModelMap[dateKey]) dailyByModelMap[dateKey] = {}; |
| dailyByModelMap[dateKey][modelShort] = (dailyByModelMap[dateKey][modelShort] || 0) + totalTkns; |
|
|
| |
| weeklyTokens[dayOfWeek] += totalTkns; |
| weeklyCounts[dayOfWeek]++; |
|
|
| |
| if (!byModelMap[modelShort]) { |
| byModelMap[modelShort] = { |
| model: modelShort, |
| provider: entry.provider, |
| requests: 0, |
| promptTokens: 0, |
| completionTokens: 0, |
| totalTokens: 0, |
| cost: 0, |
| }; |
| } |
| byModelMap[modelShort].requests++; |
| byModelMap[modelShort].promptTokens += pt; |
| byModelMap[modelShort].completionTokens += ct; |
| byModelMap[modelShort].totalTokens += totalTkns; |
| byModelMap[modelShort].cost += cost; |
|
|
| |
| const accountName = entry.connectionId |
| ? connectionMap[entry.connectionId] || `Account ${entry.connectionId.slice(0, 8)}` |
| : entry.provider || "unknown"; |
| if (!byAccountMap[accountName]) { |
| byAccountMap[accountName] = { account: accountName, totalTokens: 0, requests: 0, cost: 0 }; |
| } |
| byAccountMap[accountName].totalTokens += totalTkns; |
| byAccountMap[accountName].requests++; |
| byAccountMap[accountName].cost += cost; |
|
|
| |
| const prov = entry.provider || "unknown"; |
| if (!byProviderMap[prov]) { |
| byProviderMap[prov] = { |
| provider: prov, |
| requests: 0, |
| promptTokens: 0, |
| completionTokens: 0, |
| totalTokens: 0, |
| cost: 0, |
| }; |
| } |
| byProviderMap[prov].requests++; |
| byProviderMap[prov].promptTokens += pt; |
| byProviderMap[prov].completionTokens += ct; |
| byProviderMap[prov].totalTokens += totalTkns; |
| byProviderMap[prov].cost += cost; |
|
|
| |
| if (entry.apiKeyId || entry.apiKeyName) { |
| const keyName = entry.apiKeyName || entry.apiKeyId || "unknown"; |
| const keyLabel = entry.apiKeyId ? `${keyName} (${entry.apiKeyId})` : keyName; |
| if (!byApiKeyMap[keyLabel]) { |
| byApiKeyMap[keyLabel] = { |
| apiKey: keyLabel, |
| apiKeyId: entry.apiKeyId || null, |
| apiKeyName: keyName, |
| requests: 0, |
| promptTokens: 0, |
| completionTokens: 0, |
| totalTokens: 0, |
| cost: 0, |
| }; |
| } |
| byApiKeyMap[keyLabel].requests++; |
| byApiKeyMap[keyLabel].promptTokens += pt; |
| byApiKeyMap[keyLabel].completionTokens += ct; |
| byApiKeyMap[keyLabel].totalTokens += totalTkns; |
| byApiKeyMap[keyLabel].cost += cost; |
| } |
| } |
|
|
| |
| const dailyTrend = Object.values(dailyMap).sort((a, b) => a.date.localeCompare(b.date)); |
|
|
| |
| const allModels = new Set<string>(); |
| for (const day of Object.values(dailyByModelMap)) { |
| for (const m of Object.keys(day)) allModels.add(m); |
| } |
| const dailyByModel = dailyTrend.map((d) => { |
| const row = { date: d.date }; |
| for (const m of allModels) { |
| row[m] = dailyByModelMap[d.date]?.[m] || 0; |
| } |
| return row; |
| }); |
|
|
| const byModel = Object.values(byModelMap) |
| .sort((a, b) => b.totalTokens - a.totalTokens) |
| .map((m) => ({ |
| ...m, |
| pct: summary.totalTokens > 0 ? ((m.totalTokens / summary.totalTokens) * 100).toFixed(1) : "0", |
| })); |
|
|
| const byAccount = Object.values(byAccountMap).sort((a, b) => b.totalTokens - a.totalTokens); |
| const byProvider = Object.values(byProviderMap).sort((a, b) => b.totalTokens - a.totalTokens); |
| const byApiKey = Object.values(byApiKeyMap).sort((a, b) => b.totalTokens - a.totalTokens); |
|
|
| |
| const weekDays = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; |
| const weeklyPattern = weekDays.map((name, i) => ({ |
| day: name, |
| avgTokens: weeklyCounts[i] > 0 ? Math.round(weeklyTokens[i] / weeklyCounts[i]) : 0, |
| totalTokens: weeklyTokens[i], |
| })); |
|
|
| |
| let streak = 0; |
| const today = new Date(); |
| for (let i = 0; i < 365; i++) { |
| const d = new Date(today); |
| d.setDate(d.getDate() - i); |
| const key = toDateKey(d); |
| if (activityMap[key] && activityMap[key] > 0) { |
| streak++; |
| } else if (i > 0) { |
| break; |
| } |
| } |
|
|
| return { |
| summary: { |
| totalTokens: summary.totalTokens, |
| promptTokens: summary.promptTokens, |
| completionTokens: summary.completionTokens, |
| totalCost: summary.totalCost, |
| totalRequests: summary.totalRequests, |
| uniqueModels: summary.uniqueModels.size, |
| uniqueAccounts: summary.uniqueAccounts.size, |
| uniqueApiKeys: summary.uniqueApiKeys.size, |
| streak, |
| }, |
| dailyTrend, |
| dailyByModel, |
| modelNames: [...allModels], |
| byModel, |
| byAccount, |
| byProvider, |
| byApiKey, |
| activityMap, |
| weeklyPattern, |
| range, |
| }; |
| } |
|
|