pjpjq's picture
fix: build usage keeper from source
b034029 verified
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<string, ApiStatsModelSummary>;
}
export type TokenCategory = 'input' | 'output' | 'cached' | 'reasoning';
interface UsageModelSeriesLine {
requests_by_hour?: Record<string, number>;
requests_by_day?: Record<string, number>;
tokens_by_hour?: Record<string, number>;
tokens_by_day?: Record<string, number>;
}
interface UsagePayloadWithModelSeries {
model_series?: Record<string, UsageModelSeriesLine>;
}
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<Extract<UsageTimeRange, '4h' | '8h' | '12h' | '24h' | '7d'>, 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<UsageDetailRecord>): 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<string>();
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<string, ModelPrice> {
try {
const raw = window.localStorage.getItem('cpa-model-prices');
if (!raw) return {};
const parsed = JSON.parse(raw) as Record<string, ModelPrice>;
return parsed && typeof parsed === 'object' ? parsed : {};
} catch {
return {};
}
}
export function saveModelPrices(prices: Record<string, ModelPrice>): void {
window.localStorage.setItem('cpa-model-prices', JSON.stringify(prices));
}
export function calculateCost(detail: UsageDetailRecord, modelPrices: Record<string, ModelPrice>): 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<string>();
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<string, ModelPrice>): ApiStats[] {
if (!usage?.apis) return [];
return Object.entries(usage.apis)
.map(([endpoint, api]) => {
const models: Record<string, ApiStatsModelSummary> = {};
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<string, ModelPrice>): ModelStatsSummary[] {
const grouped = new Map<string, ModelStatsSummary>();
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<string, Map<string, number>>();
const orderedKeys = new Set<string>();
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<string, number>();
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<string, number>();
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<string, number>();
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<string, number>();
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<TokenCategory, number[]> };
}
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<TokenCategory, number[]>;
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<TokenCategory, number[]>;
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<string, ModelPrice>, hourWindowHours = 24, endMs?: number) {
return buildCostSeries(usage, modelPrices, 'hour', hourWindowHours, endMs);
}
export function buildDailyCostSeries(usage: UsagePayload | null, modelPrices: Record<string, ModelPrice>) {
return buildCostSeries(usage, modelPrices, 'day');
}
function buildCostSeries(usage: UsagePayload | null, modelPrices: Record<string, ModelPrice>, 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<string, number>();
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 '';
}