import type { ApiSummaryItem, EventRow, ModelSummaryItem, PricingEntry, RateStats, SummaryCardValue, TokenBreakdown, TrendPoint, TrendSeries, UsageDetail, UsageSeriesDimension, UsageSnapshot, UsageTimeRange, } from './types' interface UsageEventWithNames extends UsageDetail { apiName: string modelName: string } const SERIES_COLORS = ['#2563eb', '#7c3aed', '#10b981', '#f97316', '#dc2626', '#0891b2', '#f59e0b'] export function formatNumber(value: number): string { return new Intl.NumberFormat(undefined, { maximumFractionDigits: 4 }).format(value) } function getPriceMap(pricing: PricingEntry[]): Map { return new Map(pricing.map((entry) => [entry.model, entry])) } function formatLocalDayKey(date: Date): string { const pad = (value: number) => String(value).padStart(2, '0') return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}` } function calculateEventCost(event: UsageEventWithNames, priceMap: Map): number { const pricing = priceMap.get(event.modelName) if (!pricing) return 0 return ( (event.tokens.input_tokens / 1_000_000) * pricing.prompt_price_per_1m + (event.tokens.output_tokens / 1_000_000) * pricing.completion_price_per_1m + (event.tokens.cached_tokens / 1_000_000) * pricing.cache_price_per_1m ) } export function collectUsageEvents(usage: UsageSnapshot): UsageEventWithNames[] { const events: UsageEventWithNames[] = [] for (const [apiName, api] of Object.entries(usage.apis)) { for (const [modelName, model] of Object.entries(api.models)) { for (const detail of model.details ?? []) { events.push({ ...detail, apiName, modelName, }) } } } return events.sort((a, b) => Date.parse(b.timestamp) - Date.parse(a.timestamp)) } export function filterUsageSnapshot(usage: UsageSnapshot, range: UsageTimeRange): UsageSnapshot { if (range === 'all' || range === 'custom') { return usage } const events = collectUsageEvents(usage) if (events.length === 0) { return usage } const latestTimestamp = Math.max(...events.map((event) => Date.parse(event.timestamp)).filter((value) => Number.isFinite(value))) if (!Number.isFinite(latestTimestamp)) { return usage } const presetWindowMs: Partial> = { '4h': 4 * 60 * 60 * 1000, '8h': 8 * 60 * 60 * 1000, '12h': 12 * 60 * 60 * 1000, '24h': 24 * 60 * 60 * 1000, '7d': 7 * 24 * 60 * 60 * 1000, } const nowMs = Date.now() const threshold = range === 'today' ? new Date(nowMs).setHours(0, 0, 0, 0) : latestTimestamp - (presetWindowMs[range] ?? 0) const upperThreshold = range === 'today' ? nowMs : Number.POSITIVE_INFINITY const filtered: UsageSnapshot = { total_requests: 0, success_count: 0, failure_count: 0, total_tokens: 0, requests_by_day: {}, requests_by_hour: {}, tokens_by_day: {}, tokens_by_hour: {}, apis: {}, } for (const event of events) { const timestampMs = Date.parse(event.timestamp) if (!Number.isFinite(timestampMs) || timestampMs < threshold || timestampMs > upperThreshold) { continue } const api = filtered.apis[event.apiName] ?? { total_requests: 0, success_count: 0, failure_count: 0, total_tokens: 0, models: {}, } const model = api.models[event.modelName] ?? { total_requests: 0, success_count: 0, failure_count: 0, total_tokens: 0, details: [], } model.total_requests += 1 model.total_tokens += event.tokens.total_tokens const modelDetails = model.details ?? (model.details = []) modelDetails.push({ timestamp: event.timestamp, latency_ms: event.latency_ms, source: event.source, auth_index: event.auth_index, failed: event.failed, tokens: event.tokens, }) api.total_requests += 1 api.total_tokens += event.tokens.total_tokens filtered.total_requests += 1 filtered.total_tokens += event.tokens.total_tokens if (event.failed) { model.failure_count += 1 api.failure_count += 1 filtered.failure_count += 1 } else { model.success_count += 1 api.success_count += 1 filtered.success_count += 1 } const time = new Date(event.timestamp) const dayKey = formatLocalDayKey(time) const hourKey = `${time.toISOString().slice(0, 13)}:00:00Z` filtered.requests_by_day[dayKey] = (filtered.requests_by_day[dayKey] ?? 0) + 1 filtered.requests_by_hour[hourKey] = (filtered.requests_by_hour[hourKey] ?? 0) + 1 filtered.tokens_by_day[dayKey] = (filtered.tokens_by_day[dayKey] ?? 0) + event.tokens.total_tokens filtered.tokens_by_hour[hourKey] = (filtered.tokens_by_hour[hourKey] ?? 0) + event.tokens.total_tokens api.models[event.modelName] = model filtered.apis[event.apiName] = api } return filtered } export function buildRateStats(usage: UsageSnapshot): RateStats { const events = collectUsageEvents(usage) if (events.length === 0) { return { rpm: 0, tpm: 0, requestCount: 0, tokenCount: 0, windowMinutes: 30 } } const latestTimestamp = Math.max(...events.map((event) => Date.parse(event.timestamp)).filter((value) => Number.isFinite(value))) const windowMinutes = 30 const threshold = latestTimestamp - windowMinutes * 60 * 1000 const windowEvents = events.filter((event) => Date.parse(event.timestamp) >= threshold) const requestCount = windowEvents.length const tokenCount = windowEvents.reduce((sum, event) => sum + event.tokens.total_tokens, 0) return { rpm: requestCount / windowMinutes, tpm: tokenCount / windowMinutes, requestCount, tokenCount, windowMinutes, } } export function buildSummaryCards(usage: UsageSnapshot, pricing: PricingEntry[]): SummaryCardValue[] { const rateStats = buildRateStats(usage) const tokenBreakdown = buildTokenBreakdown(usage) const totalCost = buildCostSummary(usage, pricing) const hasPricing = pricing.length > 0 return [ { key: 'requests', label: 'Total requests', value: formatNumber(usage.total_requests), hint: `${formatNumber(usage.success_count)} success / ${formatNumber(usage.failure_count)} failed`, accent: '#2563eb', }, { key: 'tokens', label: 'Total tokens', value: formatNumber(usage.total_tokens), hint: `${formatNumber(tokenBreakdown.cachedTokens)} cached / ${formatNumber(tokenBreakdown.reasoningTokens)} reasoning`, accent: '#7c3aed', }, { key: 'rpm', label: 'RPM (30m)', value: formatNumber(Number(rateStats.rpm.toFixed(2))), hint: `${formatNumber(rateStats.requestCount)} requests in last ${rateStats.windowMinutes}m`, accent: '#10b981', }, { key: 'tpm', label: 'TPM (30m)', value: formatNumber(Number(rateStats.tpm.toFixed(2))), hint: `${formatNumber(rateStats.tokenCount)} tokens in last ${rateStats.windowMinutes}m`, accent: '#f97316', }, { key: 'cost', label: 'Total cost', value: hasPricing ? `$${formatNumber(totalCost)}` : '--', hint: hasPricing ? 'Calculated from saved backend pricing' : 'Save model pricing to unlock cost analytics', accent: '#f59e0b', }, ] } export function buildCostSummary(usage: UsageSnapshot, pricing: PricingEntry[]): number { const priceMap = getPriceMap(pricing) return collectUsageEvents(usage).reduce((sum, event) => sum + calculateEventCost(event, priceMap), 0) } export function buildApiSummary(usage: UsageSnapshot, pricing: PricingEntry[]): ApiSummaryItem[] { const priceMap = getPriceMap(pricing) return Object.entries(usage.apis) .map(([apiName, api]) => { const models = Object.entries(api.models) .map(([modelName, model]) => { const detailEvents = (model.details ?? []).map((detail) => ({ ...detail, apiName, modelName })) const totalCost = detailEvents.reduce((sum, event) => sum + calculateEventCost(event, priceMap), 0) return { modelName, totalRequests: model.total_requests, successCount: model.success_count, failureCount: model.failure_count, totalTokens: model.total_tokens, totalCost, } }) .sort((a, b) => b.totalTokens - a.totalTokens) return { apiName, totalRequests: api.total_requests, successCount: api.success_count, failureCount: api.failure_count, totalTokens: api.total_tokens, modelCount: models.length, totalCost: models.reduce((sum, model) => sum + model.totalCost, 0), models, } }) .sort((a, b) => b.totalTokens - a.totalTokens) } export function buildModelSummary(usage: UsageSnapshot, pricing: PricingEntry[]): ModelSummaryItem[] { const priceMap = getPriceMap(pricing) return Object.entries(usage.apis) .flatMap(([apiName, api]) => Object.entries(api.models).map(([modelName, model]) => { const details = model.details ?? [] const latencyValues = details.map((detail) => detail.latency_ms).filter((value) => Number.isFinite(value)) const totalLatencyMs = latencyValues.reduce((sum, value) => sum + value, 0) const totalCost = details .map((detail) => ({ ...detail, apiName, modelName })) .reduce((sum, event) => sum + calculateEventCost(event, priceMap), 0) return { apiName, modelName, totalRequests: model.total_requests, successCount: model.success_count, failureCount: model.failure_count, totalTokens: model.total_tokens, averageLatencyMs: latencyValues.length > 0 ? Math.round(totalLatencyMs / latencyValues.length) : 0, totalLatencyMs, successRate: model.total_requests > 0 ? (model.success_count / model.total_requests) * 100 : 100, totalCost, } }), ) .sort((a, b) => b.totalTokens - a.totalTokens) } export function buildRecentEvents(usage: UsageSnapshot, limit = 12): EventRow[] { return collectUsageEvents(usage) .map((event) => ({ timestamp: event.timestamp, apiName: event.apiName, modelName: event.modelName, source: event.source || '-', authIndex: event.auth_index || '-', failed: event.failed, latencyMs: event.latency_ms, inputTokens: event.tokens.input_tokens, outputTokens: event.tokens.output_tokens, reasoningTokens: event.tokens.reasoning_tokens, cachedTokens: event.tokens.cached_tokens, totalTokens: event.tokens.total_tokens, })) .slice(0, limit) } export function buildTrendPoints(series: Record): TrendPoint[] { return Object.entries(series) .sort(([left], [right]) => left.localeCompare(right)) .map(([label, value]) => ({ label, value })) } export function buildSeriesTrends(usage: UsageSnapshot, dimension: UsageSeriesDimension, metric: 'requests' | 'tokens'): TrendSeries[] { if (dimension === 'all') { return [ { key: metric, label: metric === 'requests' ? 'All requests' : 'All tokens', color: SERIES_COLORS[0], data: buildTrendPoints(metric === 'requests' ? usage.requests_by_hour : usage.tokens_by_hour), }, ] } const grouped = new Map>() for (const event of collectUsageEvents(usage)) { const key = dimension === 'api' ? event.apiName : event.modelName const hourKey = `${new Date(event.timestamp).toISOString().slice(0, 13)}:00:00Z` const bucket = grouped.get(key) ?? {} bucket[hourKey] = (bucket[hourKey] ?? 0) + (metric === 'requests' ? 1 : event.tokens.total_tokens) grouped.set(key, bucket) } return [...grouped.entries()] .map(([key, values], index) => ({ key, label: key, color: SERIES_COLORS[index % SERIES_COLORS.length], data: buildTrendPoints(values), })) .sort((left, right) => { const leftTotal = left.data.reduce((sum, point) => sum + point.value, 0) const rightTotal = right.data.reduce((sum, point) => sum + point.value, 0) return rightTotal - leftTotal }) .slice(0, 5) } export function buildCostTrendPoints(usage: UsageSnapshot, pricing: PricingEntry[]): TrendPoint[] { const priceMap = getPriceMap(pricing) const buckets: Record = {} for (const event of collectUsageEvents(usage)) { const hourKey = `${new Date(event.timestamp).toISOString().slice(0, 13)}:00:00Z` buckets[hourKey] = (buckets[hourKey] ?? 0) + calculateEventCost(event, priceMap) } return buildTrendPoints(buckets) } export function buildTokenBreakdown(usage: UsageSnapshot): TokenBreakdown { return Object.values(usage.apis).reduce( (totals, api) => { for (const model of Object.values(api.models)) { for (const detail of model.details ?? []) { totals.inputTokens += detail.tokens.input_tokens totals.outputTokens += detail.tokens.output_tokens totals.reasoningTokens += detail.tokens.reasoning_tokens totals.cachedTokens += detail.tokens.cached_tokens } } return totals }, { inputTokens: 0, outputTokens: 0, reasoningTokens: 0, cachedTokens: 0, }, ) }