import type { UsageFilterWindow, UsageTimeRange } from '@/lib/types'; import type { UsagePayload } from '@/components/usage/hooks/useUsageData'; import { LATENCY_SOURCE_FIELD, LATENCY_SOURCE_UNIT, extractLatencyMs, calculateLatencyStatsFromDetails, formatDurationMs } from '@/utils/usage/latency'; export * from '@/lib/usage'; export { LATENCY_SOURCE_FIELD, LATENCY_SOURCE_UNIT, extractLatencyMs, calculateLatencyStatsFromDetails, formatDurationMs }; export type { UsageTimeRange, UsageFilterWindow } from '@/lib/types'; export type { UsagePayload } from '@/components/usage/hooks/useUsageData'; export interface ModelPrice { prompt: number; completion: number; cache: number; } export interface ChartDataset { label: string; data: number[]; borderColor: string; backgroundColor: string; pointBackgroundColor?: string; pointBorderColor?: string; fill?: boolean; tension?: number; } export interface ChartData { labels: string[]; datasets: ChartDataset[]; } export interface ModelStatsSummary { model: string; requests: number; successCount: number; failureCount: number; tokens: number; averageLatencyMs: number | null; totalLatencyMs: number | null; latencySampleCount: number; cost: number; } export interface ApiStatsModelSummary { requests: number; successCount: number; failureCount: number; tokens: number; } export interface ApiStats { endpoint: string; displayName: string; totalRequests: number; successCount: number; failureCount: number; totalTokens: number; totalCost: number; models: Record; } export type TokenCategory = 'input' | 'output' | 'cached' | 'reasoning'; interface UsageModelSeriesLine { requests_by_hour?: Record; requests_by_day?: Record; tokens_by_hour?: Record; tokens_by_day?: Record; } interface UsagePayloadWithModelSeries { model_series?: Record; } export interface UsageDetailRecord { timestamp: string; source: string; source_raw?: string; source_display?: string; source_type?: string; source_key?: string; auth_index: string; failed: boolean; latency_ms: number; tokens: { input_tokens: number; output_tokens: number; reasoning_tokens: number; cached_tokens: number; cache_tokens?: number; total_tokens: number; }; __apiName?: string; __apiDisplayName?: string; __modelName?: string; __timestampMs?: number; [key: string]: unknown; } export interface StatusBlockDetail { startTime: number; endTime: number; success: number; failure: number; rate: number; } export interface ServiceHealthData { totalSuccess: number; totalFailure: number; successRate: number; rows: number; columns: number; bucketSeconds: number; windowStart: number; windowEnd: number; blockDetails: StatusBlockDetail[]; } const CHART_COLORS = ['#8b8680', '#8b5cf6', '#22c55e', '#f97316', '#f59e0b', '#06b6d4', '#ef4444', '#6366f1', '#ec4899']; const SOURCE_PREFIXES = ['sk-', 'gsk_', 'rk-', 'pk-', 'AIza', 'claude-', 'vertex-', 'gemini-']; const toNumber = (value: unknown): number => { const parsed = Number(value); return Number.isFinite(parsed) ? parsed : 0; }; const formatLocalDayKey = (date: Date): string => { const pad = (value: number) => String(value).padStart(2, '0'); return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`; }; const startOfDayKey = (timestamp: string): string => { const date = new Date(timestamp); return Number.isNaN(date.getTime()) ? '' : formatLocalDayKey(date); }; const formatHourBucketKey = (timestampMs: number): string => `${new Date(timestampMs).toISOString().slice(0, 13)}:00:00Z`; const startOfHourKey = (timestamp: string): string => { const date = new Date(timestamp); return Number.isNaN(date.getTime()) ? '' : formatHourBucketKey(date.getTime()); }; const formatHourLabel = (key: string): string => { const date = new Date(key); if (Number.isNaN(date.getTime())) return key; const md = `${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`; const time = `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`; return `${md} ${time}`; }; const formatDayLabel = (key: string): string => key; const normalizeHourWindow = (hourWindowHours?: number): number => { if (!Number.isFinite(hourWindowHours) || !hourWindowHours || hourWindowHours <= 0) { return 24; } return Math.min(Math.max(Math.floor(hourWindowHours), 1), 24); }; const resolveHourlyChartWindowHours = (hourWindowHours?: number): number => normalizeHourWindow(hourWindowHours); const buildHourlyWindow = (hourWindowHours?: number, endMs?: number) => { const resolvedHourWindow = resolveHourlyChartWindowHours(hourWindowHours); const bucketCount = resolvedHourWindow >= 24 ? 24 : resolvedHourWindow + 1; const hourMs = 60 * 60 * 1000; const currentHour = new Date(Number.isFinite(endMs) && endMs && endMs > 0 ? endMs : Date.now()); currentHour.setUTCMinutes(0, 0, 0); const earliestTime = currentHour.getTime() - ((bucketCount - 1) * hourMs); const labels = Array.from({ length: bucketCount }, (_, index) => formatHourLabel(formatHourBucketKey(earliestTime + index * hourMs)) ); return { hourMs, earliestTime, lastBucketTime: earliestTime + (labels.length - 1) * hourMs, labels }; }; const resolveHourlyChartEndMs = (details: UsageDetailRecord[], _hourWindowHours?: number, endMs?: number): number | undefined => { const requestedEndMs = Number.isFinite(endMs) && endMs && endMs > 0 ? endMs : undefined; if (requestedEndMs !== undefined) return requestedEndMs; if (!details.length) return undefined; return getDetailTimestampBounds(details)?.latestMs; }; const sum = (values: number[]) => values.reduce((total, value) => total + value, 0); const PRESET_WINDOW_HOURS: Record, number> = { '4h': 4, '8h': 8, '12h': 12, '24h': 24, '7d': 24 * 7 }; const toValidTimestamp = (value: unknown): number | null => { const timestamp = typeof value === 'number' ? value : Date.parse(String(value ?? '')); return Number.isFinite(timestamp) && timestamp > 0 ? timestamp : null; }; const getDetailTimestampBounds = (details: UsageDetailRecord[]): { earliestMs: number; latestMs: number } | null => { let earliestMs = Number.POSITIVE_INFINITY; let latestMs = Number.NEGATIVE_INFINITY; details.forEach((detail) => { const timestamp = detail.__timestampMs ?? 0; if (!Number.isFinite(timestamp) || timestamp <= 0) return; earliestMs = Math.min(earliestMs, timestamp); latestMs = Math.max(latestMs, timestamp); }); if (!Number.isFinite(earliestMs) || !Number.isFinite(latestMs)) return null; return { earliestMs, latestMs }; }; export function sanitizeChartLines(chartLines: string[], modelNames: string[]): string[] { const lines = chartLines.length ? chartLines : ['all']; const validModels = new Set(modelNames.map((name) => name.trim()).filter(Boolean)); const sanitized = lines.filter((line) => line === 'all' || validModels.has(line)); return sanitized.length ? sanitized : ['all']; } export function formatCompactNumber(value: number): string { const abs = Math.abs(value); const formatScaled = (scaled: number, suffix: string) => `${scaled.toFixed(2)}${suffix}`; if (abs < 1_000) { return new Intl.NumberFormat(undefined, { maximumFractionDigits: 0 }).format(value); } if (abs < 1_000_000) { return formatScaled(value / 1_000, 'K'); } if (abs < 1_000_000_000) { return formatScaled(value / 1_000_000, 'M'); } return formatScaled(value / 1_000_000_000, 'B'); } export function formatFixedTwoDecimals(value: number): string { return new Intl.NumberFormat(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(value || 0); } export function formatPerMinuteValue(value: number): string { return new Intl.NumberFormat(undefined, { maximumFractionDigits: value >= 100 ? 0 : value >= 10 ? 1 : 2 }).format(value); } export function formatUsd(value: number): string { return new Intl.NumberFormat(undefined, { style: 'currency', currency: 'USD', minimumFractionDigits: value < 1 ? 4 : 2, maximumFractionDigits: value < 1 ? 4 : 2 }).format(value || 0); } export function normalizeAuthIndex(value: unknown): string { if (value === null || value === undefined) return ''; return String(value).trim(); } export function extractTotalTokens(detail: Partial): number { const tokens = detail.tokens ?? { input_tokens: 0, output_tokens: 0, reasoning_tokens: 0, cached_tokens: 0, total_tokens: 0 }; const explicit = toNumber(tokens.total_tokens); if (explicit > 0) return explicit; return toNumber(tokens.input_tokens) + toNumber(tokens.output_tokens) + toNumber(tokens.reasoning_tokens) + Math.max(toNumber(tokens.cached_tokens), toNumber(tokens.cache_tokens)); } export function collectUsageDetails(usage: UsagePayload | null | undefined): UsageDetailRecord[] { if (!usage?.apis) return []; const rows: UsageDetailRecord[] = []; Object.entries(usage.apis).forEach(([apiName, api]) => { const apiDisplayName = String(api.display_name ?? apiName).trim() || apiName; Object.entries(api.models ?? {}).forEach(([modelName, model]) => { (model.details ?? []).forEach((detail) => { const timestampMs = Date.parse(detail.timestamp); rows.push({ ...detail, latency_ms: toNumber(detail.latency_ms), __apiName: apiName, __apiDisplayName: apiDisplayName, __modelName: modelName, __timestampMs: Number.isFinite(timestampMs) ? timestampMs : 0 }); }); }); }); return rows.sort((a, b) => (b.__timestampMs ?? 0) - (a.__timestampMs ?? 0)); } export function getModelNamesFromUsage(usage: UsagePayload | null | undefined): string[] { const names = new Set(); Object.values(usage?.apis ?? {}).forEach((api) => { Object.keys(api.models ?? {}).forEach((modelName) => { const normalized = modelName.trim(); if (normalized) { names.add(normalized); } }); }); return Array.from(names).sort((a, b) => a.localeCompare(b)); } export function resolveUsageFilterWindow( usage: UsagePayload | null | undefined, range: UsageTimeRange, options: { nowMs?: number; customStart?: string | number; customEnd?: string | number; } = {} ): UsageFilterWindow { const details = collectUsageDetails(usage); const bounds = getDetailTimestampBounds(details); const fallbackNow = toValidTimestamp(options.nowMs) ?? Date.now(); if (range === 'all') { if (!bounds) return {}; const spanMinutes = Math.max((bounds.latestMs - bounds.earliestMs) / 60000, 1); return { startMs: bounds.earliestMs, endMs: bounds.latestMs, windowMinutes: spanMinutes }; } if (range === 'custom') { const startMs = toValidTimestamp(options.customStart); const endMs = toValidTimestamp(options.customEnd); if (startMs === null || endMs === null || startMs > endMs) { return {}; } return { startMs, endMs, windowMinutes: Math.max((endMs - startMs) / 60000, 1) }; } if (range === 'today') { const start = new Date(fallbackNow); start.setHours(0, 0, 0, 0); const startMs = start.getTime(); const endMs = fallbackNow; return { startMs, endMs, windowMinutes: Math.max((endMs - startMs) / 60000, 1) }; } const windowHours = PRESET_WINDOW_HOURS[range]; const endMs = fallbackNow; const startMs = endMs - windowHours * 60 * 60 * 1000; return { startMs, endMs, windowMinutes: windowHours * 60 }; } export function filterUsageByWindow(usage: UsagePayload, window: UsageFilterWindow): UsagePayload { const details = collectUsageDetails(usage); if (!details.length) return usage; const { startMs, endMs } = window; if (startMs === undefined && endMs === undefined) { return usage; } const filteredDetails = details.filter((detail) => { const timestamp = detail.__timestampMs ?? 0; if (!Number.isFinite(timestamp) || timestamp <= 0) return false; if (startMs !== undefined && timestamp < startMs) return false; if (endMs !== undefined && timestamp > endMs) return false; return true; }); return buildUsageFromDetails(filteredDetails); } export function filterUsageByTimeRange( usage: UsagePayload, range: UsageTimeRange, options: { nowMs?: number; customStart?: string | number; customEnd?: string | number; } = {} ): UsagePayload { const window = resolveUsageFilterWindow(usage, range, options); return filterUsageByWindow(usage, window); } export function loadModelPrices(): Record { try { const raw = window.localStorage.getItem('cpa-model-prices'); if (!raw) return {}; const parsed = JSON.parse(raw) as Record; return parsed && typeof parsed === 'object' ? parsed : {}; } catch { return {}; } } export function saveModelPrices(prices: Record): void { window.localStorage.setItem('cpa-model-prices', JSON.stringify(prices)); } export function calculateCost(detail: UsageDetailRecord, modelPrices: Record): number { const modelName = detail.__modelName ?? ''; const pricing = modelPrices[modelName]; if (!pricing) return 0; const inputTokens = Math.max(toNumber(detail.tokens.input_tokens), 0); const completionTokens = Math.max(toNumber(detail.tokens.output_tokens), 0); const cachedTokens = Math.max( toNumber(detail.tokens.cached_tokens), toNumber(detail.tokens.cache_tokens) ); const promptTokens = Math.max(inputTokens - cachedTokens, 0); return ( (promptTokens / 1_000_000) * pricing.prompt + (completionTokens / 1_000_000) * pricing.completion + (cachedTokens / 1_000_000) * pricing.cache ); } export function buildCandidateUsageSourceIds({ apiKey, prefix }: { apiKey?: string; prefix?: string }): string[] { const set = new Set(); if (apiKey?.trim()) { set.add(apiKey.trim()); set.add(`t:${apiKey.trim()}`); } if (prefix?.trim()) { set.add(prefix.trim()); set.add(`t:${prefix.trim()}`); } return Array.from(set); } export function getApiStats(usage: UsagePayload | null, modelPrices: Record): ApiStats[] { if (!usage?.apis) return []; return Object.entries(usage.apis) .map(([endpoint, api]) => { const models: Record = {}; let totalCost = 0; Object.entries(api.models ?? {}).forEach(([modelName, model]) => { models[modelName] = { requests: toNumber(model.total_requests), successCount: toNumber(model.success_count), failureCount: toNumber(model.failure_count), tokens: toNumber(model.total_tokens) }; (model.details ?? []).forEach((detail) => { totalCost += calculateCost({ ...detail, __modelName: modelName }, modelPrices); }); }); return { endpoint, displayName: String(api.display_name ?? endpoint).trim() || endpoint, totalRequests: toNumber(api.total_requests), successCount: toNumber(api.success_count), failureCount: toNumber(api.failure_count), totalTokens: toNumber(api.total_tokens), totalCost, models }; }) .sort((a, b) => b.totalRequests - a.totalRequests); } export function getModelStats(usage: UsagePayload | null, modelPrices: Record): ModelStatsSummary[] { const grouped = new Map(); collectUsageDetails(usage).forEach((detail) => { const model = detail.__modelName || '-'; const current = grouped.get(model) ?? { model, requests: 0, successCount: 0, failureCount: 0, tokens: 0, averageLatencyMs: null, totalLatencyMs: 0, latencySampleCount: 0, cost: 0 }; current.requests += 1; current.tokens += extractTotalTokens(detail); current.cost += calculateCost(detail, modelPrices); if (detail.failed) current.failureCount += 1; else current.successCount += 1; const latency = extractLatencyMs(detail); if (latency !== null) { current.totalLatencyMs = (current.totalLatencyMs ?? 0) + latency; current.latencySampleCount += 1; current.averageLatencyMs = (current.totalLatencyMs ?? 0) / current.latencySampleCount; } grouped.set(model, current); }); return Array.from(grouped.values()).sort((a, b) => b.requests - a.requests); } export function buildChartData( usage: UsagePayload, period: 'hour' | 'day', metric: 'requests' | 'tokens', chartLines: string[], options: { hourWindowHours?: number; endMs?: number } = {} ): ChartData { const details = collectUsageDetails(usage); if (!details.length) { const lines = chartLines.length ? chartLines : ['all']; const bucketMap = period === 'hour' ? (metric === 'requests' ? usage.requests_by_hour : usage.tokens_by_hour) : (metric === 'requests' ? usage.requests_by_day : usage.tokens_by_day); const rawBucketKeys = Object.keys(bucketMap ?? {}).sort((a, b) => a.localeCompare(b)); if (!rawBucketKeys.length) { return { labels: [], datasets: [] }; } const bucketKeys = period === 'hour' ? (() => { const endMs = options.endMs ?? Date.parse(rawBucketKeys[rawBucketKeys.length - 1]); const { earliestTime, hourMs, labels } = buildHourlyWindow(options.hourWindowHours, endMs); return labels.map((_, index) => formatHourBucketKey(earliestTime + index * hourMs)); })() : rawBucketKeys; const datasets: ChartDataset[] = []; if (lines.includes('all')) { datasets.push({ label: 'All', data: bucketKeys.map((key) => toNumber(bucketMap?.[key])), borderColor: CHART_COLORS[0], backgroundColor: `${CHART_COLORS[0]}22`, pointBackgroundColor: CHART_COLORS[0], pointBorderColor: CHART_COLORS[0], fill: false, tension: 0.35 }); } const modelSeries = (usage as UsagePayload & UsagePayloadWithModelSeries).model_series ?? {}; lines.filter((line) => line !== 'all').forEach((line) => { const series = modelSeries[line]; const lineBucketMap = period === 'hour' ? (metric === 'requests' ? series?.requests_by_hour : series?.tokens_by_hour) : (metric === 'requests' ? series?.requests_by_day : series?.tokens_by_day); if (!lineBucketMap) return; const color = CHART_COLORS[datasets.length % CHART_COLORS.length]; datasets.push({ label: line, data: bucketKeys.map((key) => toNumber(lineBucketMap[key])), borderColor: color, backgroundColor: `${color}22`, pointBackgroundColor: color, pointBorderColor: color, fill: false, tension: 0.35 }); }); return { labels: bucketKeys.map((key) => (period === 'hour' ? formatHourLabel(key) : formatDayLabel(key))), datasets }; } const lines = chartLines.length ? chartLines : ['all']; const bucketsByLine = new Map>(); const orderedKeys = new Set(); if (period === 'hour') { const hourEndMs = resolveHourlyChartEndMs(details, options.hourWindowHours, options.endMs); const { labels, earliestTime, lastBucketTime, hourMs } = buildHourlyWindow(options.hourWindowHours, hourEndMs); const bucketKeys = labels.map((_, index) => formatHourBucketKey(earliestTime + index * hourMs)); bucketKeys.forEach((key) => orderedKeys.add(key)); details.forEach((detail) => { const timestamp = detail.__timestampMs ?? 0; if (!Number.isFinite(timestamp) || timestamp <= 0) return; const normalized = new Date(timestamp); normalized.setUTCMinutes(0, 0, 0); const bucketStart = normalized.getTime(); if (bucketStart < earliestTime || bucketStart > lastBucketTime) return; const key = new Date(bucketStart).toISOString(); const lineKey = lines.includes(detail.__modelName ?? '') ? detail.__modelName ?? 'all' : lines.includes(detail.__apiName ?? '') ? detail.__apiName ?? 'all' : 'all'; if (!lines.includes('all') && lineKey === 'all') return; const line = bucketsByLine.get(lineKey) ?? new Map(); const value = metric === 'requests' ? 1 : extractTotalTokens(detail); line.set(key, (line.get(key) ?? 0) + value); bucketsByLine.set(lineKey, line); if (lines.includes('all')) { const allLine = bucketsByLine.get('all') ?? new Map(); allLine.set(key, (allLine.get(key) ?? 0) + value); bucketsByLine.set('all', allLine); } }); return { labels, datasets: Array.from(bucketsByLine.entries()).map(([label, values], index) => ({ label: label === 'all' ? 'All' : label, data: bucketKeys.map((key) => values.get(key) ?? 0), borderColor: CHART_COLORS[index % CHART_COLORS.length], backgroundColor: `${CHART_COLORS[index % CHART_COLORS.length]}22`, pointBackgroundColor: CHART_COLORS[index % CHART_COLORS.length], pointBorderColor: CHART_COLORS[index % CHART_COLORS.length], fill: false, tension: 0.35 })) }; } details.forEach((detail) => { const key = startOfDayKey(detail.timestamp); if (!key) return; orderedKeys.add(key); const lineKey = lines.includes(detail.__modelName ?? '') ? detail.__modelName ?? 'all' : lines.includes(detail.__apiName ?? '') ? detail.__apiName ?? 'all' : 'all'; if (!lines.includes('all') && lineKey === 'all') return; const line = bucketsByLine.get(lineKey) ?? new Map(); const value = metric === 'requests' ? 1 : extractTotalTokens(detail); line.set(key, (line.get(key) ?? 0) + value); bucketsByLine.set(lineKey, line); if (lines.includes('all')) { const allLine = bucketsByLine.get('all') ?? new Map(); allLine.set(key, (allLine.get(key) ?? 0) + value); bucketsByLine.set('all', allLine); } }); const bucketKeys = Array.from(orderedKeys).sort((a, b) => a.localeCompare(b)); return { labels: bucketKeys.map((key) => formatDayLabel(key)), datasets: Array.from(bucketsByLine.entries()).map(([label, values], index) => ({ label: label === 'all' ? 'All' : label, data: bucketKeys.map((key) => values.get(key) ?? 0), borderColor: CHART_COLORS[index % CHART_COLORS.length], backgroundColor: `${CHART_COLORS[index % CHART_COLORS.length]}22`, pointBackgroundColor: CHART_COLORS[index % CHART_COLORS.length], pointBorderColor: CHART_COLORS[index % CHART_COLORS.length], fill: false, tension: 0.35 })) }; } export function buildHourlyTokenBreakdown(usage: UsagePayload | null, hourWindowHours = 24, endMs?: number) { return buildTokenBreakdownSeries(usage, 'hour', hourWindowHours, endMs); } export function buildDailyTokenBreakdown(usage: UsagePayload | null) { return buildTokenBreakdownSeries(usage, 'day'); } function buildTokenBreakdownSeries(usage: UsagePayload | null, period: 'hour' | 'day', hourWindowHours?: number, endMs?: number) { const details = collectUsageDetails(usage); if (!details.length) { return { labels: [], dataByCategory: { input: [], output: [], cached: [], reasoning: [] } as Record }; } if (period === 'hour') { const hourEndMs = resolveHourlyChartEndMs(details, hourWindowHours, endMs); const { labels, earliestTime, lastBucketTime, hourMs } = buildHourlyWindow(hourWindowHours, hourEndMs); const dataByCategory = { input: new Array(labels.length).fill(0), output: new Array(labels.length).fill(0), cached: new Array(labels.length).fill(0), reasoning: new Array(labels.length).fill(0) } as Record; details.forEach((detail) => { const timestamp = detail.__timestampMs ?? 0; if (!Number.isFinite(timestamp) || timestamp <= 0) return; const normalized = new Date(timestamp); normalized.setUTCMinutes(0, 0, 0); const bucketStart = normalized.getTime(); if (bucketStart < earliestTime || bucketStart > lastBucketTime) return; const bucketIndex = Math.floor((bucketStart - earliestTime) / hourMs); if (bucketIndex < 0 || bucketIndex >= labels.length) return; dataByCategory.input[bucketIndex] += toNumber(detail.tokens.input_tokens); dataByCategory.output[bucketIndex] += toNumber(detail.tokens.output_tokens); dataByCategory.cached[bucketIndex] += Math.max(toNumber(detail.tokens.cached_tokens), toNumber(detail.tokens.cache_tokens)); dataByCategory.reasoning[bucketIndex] += toNumber(detail.tokens.reasoning_tokens); }); return { labels, dataByCategory }; } const keys = Array.from(new Set(details.map((detail) => startOfDayKey(detail.timestamp)))).filter(Boolean).sort((a, b) => a.localeCompare(b)); const dataByCategory = { input: [], output: [], cached: [], reasoning: [] } as Record; keys.forEach((key) => { const matching = details.filter((detail) => startOfDayKey(detail.timestamp) === key); dataByCategory.input.push(sum(matching.map((detail) => toNumber(detail.tokens.input_tokens)))); dataByCategory.output.push(sum(matching.map((detail) => toNumber(detail.tokens.output_tokens)))); dataByCategory.cached.push(sum(matching.map((detail) => Math.max(toNumber(detail.tokens.cached_tokens), toNumber(detail.tokens.cache_tokens))))); dataByCategory.reasoning.push(sum(matching.map((detail) => toNumber(detail.tokens.reasoning_tokens)))); }); return { labels: keys.map((key) => formatDayLabel(key)), dataByCategory }; } export function buildHourlyCostSeries(usage: UsagePayload | null, modelPrices: Record, hourWindowHours = 24, endMs?: number) { return buildCostSeries(usage, modelPrices, 'hour', hourWindowHours, endMs); } export function buildDailyCostSeries(usage: UsagePayload | null, modelPrices: Record) { return buildCostSeries(usage, modelPrices, 'day'); } function buildCostSeries(usage: UsagePayload | null, modelPrices: Record, period: 'hour' | 'day', hourWindowHours?: number, endMs?: number) { const details = collectUsageDetails(usage); if (!details.length) return { labels: [], data: [], hasData: false }; if (period === 'hour') { const hourEndMs = resolveHourlyChartEndMs(details, hourWindowHours, endMs); const { labels, earliestTime, lastBucketTime, hourMs } = buildHourlyWindow(hourWindowHours, hourEndMs); const data = new Array(labels.length).fill(0); let hasData = false; details.forEach((detail) => { const timestamp = detail.__timestampMs ?? 0; if (!Number.isFinite(timestamp) || timestamp <= 0) return; const normalized = new Date(timestamp); normalized.setUTCMinutes(0, 0, 0); const bucketStart = normalized.getTime(); if (bucketStart < earliestTime || bucketStart > lastBucketTime) return; const bucketIndex = Math.floor((bucketStart - earliestTime) / hourMs); if (bucketIndex < 0 || bucketIndex >= labels.length) return; const cost = calculateCost(detail, modelPrices); if (cost > 0) { data[bucketIndex] += cost; hasData = true; } }); return { labels, data, hasData }; } const grouped = new Map(); details.forEach((detail) => { const key = startOfDayKey(detail.timestamp); if (!key) return; grouped.set(key, (grouped.get(key) ?? 0) + calculateCost(detail, modelPrices)); }); const keys = Array.from(grouped.keys()).sort((a, b) => a.localeCompare(b)); const data = keys.map((key) => grouped.get(key) ?? 0); return { labels: keys.map((key) => formatDayLabel(key)), data, hasData: data.some((value) => value > 0) }; } export function calculateServiceHealthData(details: UsageDetailRecord[]): ServiceHealthData { const rowCount = 7; const blockCount = 96; const windowMs = 15 * 60 * 1000; const totalBlocks = rowCount * blockCount; const timelineAnchor = Date.now(); const currentBucketStart = Math.floor(timelineAnchor / windowMs) * windowMs; const newestWindowEnd = currentBucketStart + windowMs; const oldestWindowStart = newestWindowEnd - totalBlocks * windowMs; const blockDetails = Array.from({ length: totalBlocks }, (_, index) => { const startTime = oldestWindowStart + index * windowMs; const endTime = startTime + windowMs; const matching = details.filter((detail) => { const timestamp = detail.__timestampMs ?? 0; return timestamp >= startTime && timestamp < endTime; }); const success = matching.filter((detail) => !detail.failed).length; const failure = matching.filter((detail) => detail.failed).length; const total = success + failure; return { startTime, endTime, success, failure, rate: total > 0 ? success / total : -1 }; }); const totalSuccess = details.filter((detail) => !detail.failed).length; const totalFailure = details.filter((detail) => detail.failed).length; const total = totalSuccess + totalFailure; return { totalSuccess, totalFailure, successRate: total > 0 ? (totalSuccess / total) * 100 : 0, rows: rowCount, columns: blockCount, bucketSeconds: Math.floor(windowMs / 1000), windowStart: oldestWindowStart, windowEnd: newestWindowEnd, blockDetails }; } export function buildUsageFromDetails(details: UsageDetailRecord[]): UsagePayload { const usage: UsagePayload = { 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: {} }; details.forEach((detail) => { const apiName = detail.__apiName || 'unknown'; const modelName = detail.__modelName || 'unknown'; const tokens = extractTotalTokens(detail); const dayKey = startOfDayKey(detail.timestamp); const hourKey = startOfHourKey(detail.timestamp); const apis = usage.apis ?? (usage.apis = {}); const api = apis[apiName] ?? { display_name: detail.__apiDisplayName || apiName, total_requests: 0, success_count: 0, failure_count: 0, total_tokens: 0, models: {} }; const model = api.models[modelName] ?? { total_requests: 0, success_count: 0, failure_count: 0, total_tokens: 0, details: [] }; usage.total_requests = (usage.total_requests ?? 0) + 1; usage.total_tokens = (usage.total_tokens ?? 0) + tokens; api.total_requests += 1; api.total_tokens += tokens; model.total_requests += 1; model.total_tokens += tokens; if (detail.failed) { usage.failure_count = (usage.failure_count ?? 0) + 1; api.failure_count += 1; model.failure_count += 1; } else { usage.success_count = (usage.success_count ?? 0) + 1; api.success_count += 1; model.success_count += 1; } const modelDetails = model.details ?? (model.details = []); modelDetails.push({ timestamp: detail.timestamp, latency_ms: toNumber(detail.latency_ms), source: detail.source ?? '', source_raw: detail.source_raw ?? '', source_display: detail.source_display ?? '', source_type: detail.source_type ?? '', source_key: detail.source_key ?? '', auth_index: detail.auth_index ?? '', failed: detail.failed === true, tokens: { input_tokens: toNumber(detail.tokens.input_tokens), output_tokens: toNumber(detail.tokens.output_tokens), reasoning_tokens: toNumber(detail.tokens.reasoning_tokens), cached_tokens: Math.max(toNumber(detail.tokens.cached_tokens), toNumber(detail.tokens.cache_tokens)), total_tokens: tokens } }); const requestsByDay = usage.requests_by_day ?? (usage.requests_by_day = {}); const requestsByHour = usage.requests_by_hour ?? (usage.requests_by_hour = {}); const tokensByDay = usage.tokens_by_day ?? (usage.tokens_by_day = {}); const tokensByHour = usage.tokens_by_hour ?? (usage.tokens_by_hour = {}); requestsByDay[dayKey] = (requestsByDay[dayKey] ?? 0) + 1; requestsByHour[hourKey] = (requestsByHour[hourKey] ?? 0) + 1; tokensByDay[dayKey] = (tokensByDay[dayKey] ?? 0) + tokens; tokensByHour[hourKey] = (tokensByHour[hourKey] ?? 0) + tokens; api.models[modelName] = model; apis[apiName] = api; }); return usage; } export function inferSourceType(source: string): string { const value = source.trim(); if (!value) return ''; if (value.startsWith('t:')) return 'token'; if (SOURCE_PREFIXES.some((prefix) => value.startsWith(prefix))) return 'api-key'; return ''; }