Files
Cli-Proxy-API-Management-Ce…/src/utils/usage.ts

907 lines
26 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 使用统计相关工具
* 迁移自基线 modules/usage.js 的纯逻辑部分
*/
import type { ScriptableContext } from 'chart.js';
import { maskApiKey } from './format';
export interface KeyStatBucket {
success: number;
failure: number;
}
export interface KeyStats {
bySource: Record<string, KeyStatBucket>;
byAuthIndex: Record<string, KeyStatBucket>;
}
export interface TokenBreakdown {
cachedTokens: number;
reasoningTokens: number;
}
export interface RateStats {
rpm: number;
tpm: number;
windowMinutes: number;
requestCount: number;
tokenCount: number;
}
export interface ModelPrice {
prompt: number;
completion: number;
cache: number;
}
export interface UsageDetail {
timestamp: string;
source: string;
auth_index: number;
tokens: {
input_tokens: number;
output_tokens: number;
reasoning_tokens: number;
cached_tokens: number;
cache_tokens?: number;
total_tokens: number;
};
failed: boolean;
__modelName?: string;
}
export interface ApiStats {
endpoint: string;
totalRequests: number;
totalTokens: number;
totalCost: number;
models: Record<string, { requests: number; tokens: number }>;
}
const TOKENS_PER_PRICE_UNIT = 1_000_000;
const MODEL_PRICE_STORAGE_KEY = 'cli-proxy-model-prices-v2';
const normalizeAuthIndex = (value: any) => {
if (typeof value === 'number' && Number.isFinite(value)) {
return value.toString();
}
if (typeof value === 'string') {
const trimmed = value.trim();
return trimmed ? trimmed : null;
}
return null;
};
/**
* 对使用数据中的敏感字段进行遮罩
*/
export function maskUsageSensitiveValue(value: unknown, masker: (val: string) => string = maskApiKey): string {
if (value === null || value === undefined) {
return '';
}
const raw = typeof value === 'string' ? value : String(value);
if (!raw) {
return '';
}
let masked = raw;
const queryRegex = /([?&])(api[-_]?key|key|token|access_token|authorization)=([^&#\s]+)/gi;
masked = masked.replace(queryRegex, (_full, prefix, keyName, valuePart) => `${prefix}${keyName}=${masker(valuePart)}`);
const headerRegex = /(api[-_]?key|key|token|access[-_]?token|authorization)\s*([:=])\s*([A-Za-z0-9._-]+)/gi;
masked = masked.replace(headerRegex, (_full, keyName, separator, valuePart) => `${keyName}${separator}${masker(valuePart)}`);
const keyLikeRegex = /(sk-[A-Za-z0-9]{6,}|AI[a-zA-Z0-9_-]{6,}|AIza[0-9A-Za-z-_]{8,}|hf_[A-Za-z0-9]{6,}|pk_[A-Za-z0-9]{6,}|rk_[A-Za-z0-9]{6,})/g;
masked = masked.replace(keyLikeRegex, (match) => masker(match));
if (masked === raw) {
const trimmed = raw.trim();
if (trimmed && !/\s/.test(trimmed)) {
const looksLikeKey =
/^sk-/i.test(trimmed) ||
/^AI/i.test(trimmed) ||
/^AIza/i.test(trimmed) ||
/^hf_/i.test(trimmed) ||
/^pk_/i.test(trimmed) ||
/^rk_/i.test(trimmed) ||
(!/[\\/]/.test(trimmed) && (/\d/.test(trimmed) || trimmed.length >= 10)) ||
trimmed.length >= 24;
if (looksLikeKey) {
return masker(trimmed);
}
}
}
return masked;
}
/**
* 格式化 tokens 为百万单位
*/
export function formatTokensInMillions(value: number): string {
const num = Number(value);
if (!Number.isFinite(num)) {
return '0.00M';
}
return `${(num / 1_000_000).toFixed(2)}M`;
}
/**
* 格式化每分钟数值
*/
export function formatPerMinuteValue(value: number): string {
const num = Number(value);
if (!Number.isFinite(num)) {
return '0.00';
}
const abs = Math.abs(num);
if (abs >= 1000) {
return Math.round(num).toLocaleString();
}
if (abs >= 100) {
return num.toFixed(0);
}
if (abs >= 10) {
return num.toFixed(1);
}
return num.toFixed(2);
}
/**
* 格式化紧凑数字
*/
export function formatCompactNumber(value: number): string {
const num = Number(value);
if (!Number.isFinite(num)) {
return '0';
}
const abs = Math.abs(num);
if (abs >= 1_000_000) {
return `${(num / 1_000_000).toFixed(1)}M`;
}
if (abs >= 1_000) {
return `${(num / 1_000).toFixed(1)}K`;
}
return abs >= 1 ? num.toFixed(0) : num.toFixed(2);
}
/**
* 格式化美元
*/
export function formatUsd(value: number): string {
const num = Number(value);
if (!Number.isFinite(num)) {
return '$0.00';
}
const fixed = num.toFixed(2);
const parts = Number(fixed).toLocaleString(undefined, {
minimumFractionDigits: 2,
maximumFractionDigits: 2
});
return `$${parts}`;
}
/**
* 从使用数据中收集所有请求明细
*/
export function collectUsageDetails(usageData: any): UsageDetail[] {
if (!usageData) {
return [];
}
const apis = usageData.apis || {};
const details: UsageDetail[] = [];
Object.values(apis as Record<string, any>).forEach((apiEntry) => {
const models = apiEntry?.models || {};
Object.entries(models as Record<string, any>).forEach(([modelName, modelEntry]) => {
const modelDetails = Array.isArray(modelEntry.details) ? modelEntry.details : [];
modelDetails.forEach((detail: any) => {
if (detail && detail.timestamp) {
details.push({
...detail,
__modelName: modelName
});
}
});
});
});
return details;
}
/**
* 从单条明细提取总 tokens
*/
export function extractTotalTokens(detail: any): number {
const tokens = detail?.tokens || {};
if (typeof tokens.total_tokens === 'number') {
return tokens.total_tokens;
}
const inputTokens = typeof tokens.input_tokens === 'number' ? tokens.input_tokens : 0;
const outputTokens = typeof tokens.output_tokens === 'number' ? tokens.output_tokens : 0;
const reasoningTokens = typeof tokens.reasoning_tokens === 'number' ? tokens.reasoning_tokens : 0;
const cachedTokens = Math.max(
typeof tokens.cached_tokens === 'number' ? Math.max(tokens.cached_tokens, 0) : 0,
typeof tokens.cache_tokens === 'number' ? Math.max(tokens.cache_tokens, 0) : 0
);
return inputTokens + outputTokens + reasoningTokens + cachedTokens;
}
/**
* 计算 token 分类统计
*/
export function calculateTokenBreakdown(usageData: any): TokenBreakdown {
const details = collectUsageDetails(usageData);
if (!details.length) {
return { cachedTokens: 0, reasoningTokens: 0 };
}
let cachedTokens = 0;
let reasoningTokens = 0;
details.forEach(detail => {
const tokens = detail?.tokens || {};
cachedTokens += Math.max(
typeof tokens.cached_tokens === 'number' ? Math.max(tokens.cached_tokens, 0) : 0,
typeof tokens.cache_tokens === 'number' ? Math.max(tokens.cache_tokens, 0) : 0
);
if (typeof tokens.reasoning_tokens === 'number') {
reasoningTokens += tokens.reasoning_tokens;
}
});
return { cachedTokens, reasoningTokens };
}
/**
* 计算最近 N 分钟的 RPM/TPM
*/
export function calculateRecentPerMinuteRates(windowMinutes: number = 30, usageData: any): RateStats {
const details = collectUsageDetails(usageData);
const effectiveWindow = Number.isFinite(windowMinutes) && windowMinutes > 0 ? windowMinutes : 30;
if (!details.length) {
return { rpm: 0, tpm: 0, windowMinutes: effectiveWindow, requestCount: 0, tokenCount: 0 };
}
const now = Date.now();
const windowStart = now - effectiveWindow * 60 * 1000;
let requestCount = 0;
let tokenCount = 0;
details.forEach(detail => {
const timestamp = Date.parse(detail.timestamp);
if (Number.isNaN(timestamp) || timestamp < windowStart) {
return;
}
requestCount += 1;
tokenCount += extractTotalTokens(detail);
});
const denominator = effectiveWindow > 0 ? effectiveWindow : 1;
return {
rpm: requestCount / denominator,
tpm: tokenCount / denominator,
windowMinutes: effectiveWindow,
requestCount,
tokenCount
};
}
/**
* 从使用数据获取模型名称列表
*/
export function getModelNamesFromUsage(usageData: any): string[] {
if (!usageData) {
return [];
}
const apis = usageData.apis || {};
const names = new Set<string>();
Object.values(apis as Record<string, any>).forEach(apiEntry => {
const models = apiEntry?.models || {};
Object.keys(models).forEach(modelName => {
if (modelName) {
names.add(modelName);
}
});
});
return Array.from(names).sort((a, b) => a.localeCompare(b));
}
/**
* 计算成本数据
*/
export function calculateCost(detail: any, modelPrices: Record<string, ModelPrice>): number {
const modelName = detail.__modelName || '';
const price = modelPrices[modelName];
if (!price) {
return 0;
}
const tokens = detail?.tokens || {};
const rawInputTokens = Number(tokens.input_tokens);
const rawCompletionTokens = Number(tokens.output_tokens);
const rawCachedTokensPrimary = Number(tokens.cached_tokens);
const rawCachedTokensAlternate = Number(tokens.cache_tokens);
const inputTokens = Number.isFinite(rawInputTokens) ? Math.max(rawInputTokens, 0) : 0;
const completionTokens = Number.isFinite(rawCompletionTokens) ? Math.max(rawCompletionTokens, 0) : 0;
const cachedTokens = Math.max(
Number.isFinite(rawCachedTokensPrimary) ? Math.max(rawCachedTokensPrimary, 0) : 0,
Number.isFinite(rawCachedTokensAlternate) ? Math.max(rawCachedTokensAlternate, 0) : 0
);
const promptTokens = Math.max(inputTokens - cachedTokens, 0);
const promptCost = (promptTokens / TOKENS_PER_PRICE_UNIT) * (Number(price.prompt) || 0);
const cachedCost = (cachedTokens / TOKENS_PER_PRICE_UNIT) * (Number(price.cache) || 0);
const completionCost = (completionTokens / TOKENS_PER_PRICE_UNIT) * (Number(price.completion) || 0);
const total = promptCost + cachedCost + completionCost;
return Number.isFinite(total) && total > 0 ? total : 0;
}
/**
* 计算总成本
*/
export function calculateTotalCost(usageData: any, modelPrices: Record<string, ModelPrice>): number {
const details = collectUsageDetails(usageData);
if (!details.length || !Object.keys(modelPrices).length) {
return 0;
}
return details.reduce((sum, detail) => sum + calculateCost(detail, modelPrices), 0);
}
/**
* 从 localStorage 加载模型价格
*/
export function loadModelPrices(): Record<string, ModelPrice> {
try {
if (typeof localStorage === 'undefined') {
return {};
}
const raw = localStorage.getItem(MODEL_PRICE_STORAGE_KEY);
if (!raw) {
return {};
}
const parsed = JSON.parse(raw);
if (!parsed || typeof parsed !== 'object') {
return {};
}
const normalized: Record<string, ModelPrice> = {};
Object.entries(parsed).forEach(([model, price]: [string, any]) => {
if (!model) return;
const promptRaw = Number(price?.prompt);
const completionRaw = Number(price?.completion);
const cacheRaw = Number(price?.cache);
if (!Number.isFinite(promptRaw) && !Number.isFinite(completionRaw) && !Number.isFinite(cacheRaw)) {
return;
}
const prompt = Number.isFinite(promptRaw) && promptRaw >= 0 ? promptRaw : 0;
const completion = Number.isFinite(completionRaw) && completionRaw >= 0 ? completionRaw : 0;
const cache =
Number.isFinite(cacheRaw) && cacheRaw >= 0
? cacheRaw
: Number.isFinite(promptRaw) && promptRaw >= 0
? promptRaw
: prompt;
normalized[model] = {
prompt,
completion,
cache
};
});
return normalized;
} catch {
return {};
}
}
/**
* 保存模型价格到 localStorage
*/
export function saveModelPrices(prices: Record<string, ModelPrice>): void {
try {
if (typeof localStorage === 'undefined') {
return;
}
localStorage.setItem(MODEL_PRICE_STORAGE_KEY, JSON.stringify(prices));
} catch {
console.warn('保存模型价格失败');
}
}
/**
* 获取 API 统计数据
*/
export function getApiStats(usageData: any, modelPrices: Record<string, ModelPrice>): ApiStats[] {
if (!usageData?.apis) {
return [];
}
const apis = usageData.apis;
const result: ApiStats[] = [];
Object.entries(apis as Record<string, any>).forEach(([endpoint, apiData]) => {
const models: Record<string, { requests: number; tokens: number }> = {};
let totalCost = 0;
const modelsData = apiData?.models || {};
Object.entries(modelsData as Record<string, any>).forEach(([modelName, modelData]) => {
models[modelName] = {
requests: modelData.total_requests || 0,
tokens: modelData.total_tokens || 0
};
const price = modelPrices[modelName];
if (price) {
const details = Array.isArray(modelData.details) ? modelData.details : [];
details.forEach((detail: any) => {
totalCost += calculateCost({ ...detail, __modelName: modelName }, modelPrices);
});
}
});
result.push({
endpoint: maskUsageSensitiveValue(endpoint) || endpoint,
totalRequests: apiData.total_requests || 0,
totalTokens: apiData.total_tokens || 0,
totalCost,
models
});
});
return result;
}
/**
* 获取模型统计数据
*/
export function getModelStats(usageData: any, modelPrices: Record<string, ModelPrice>): Array<{
model: string;
requests: number;
tokens: number;
cost: number;
}> {
if (!usageData?.apis) {
return [];
}
const modelMap = new Map<string, { requests: number; tokens: number; cost: number }>();
Object.values(usageData.apis as Record<string, any>).forEach(apiData => {
const models = apiData?.models || {};
Object.entries(models as Record<string, any>).forEach(([modelName, modelData]) => {
const existing = modelMap.get(modelName) || { requests: 0, tokens: 0, cost: 0 };
existing.requests += modelData.total_requests || 0;
existing.tokens += modelData.total_tokens || 0;
const price = modelPrices[modelName];
if (price) {
const details = Array.isArray(modelData.details) ? modelData.details : [];
details.forEach((detail: any) => {
existing.cost += calculateCost({ ...detail, __modelName: modelName }, modelPrices);
});
}
modelMap.set(modelName, existing);
});
});
return Array.from(modelMap.entries())
.map(([model, stats]) => ({ model, ...stats }))
.sort((a, b) => b.requests - a.requests);
}
/**
* 格式化小时标签
*/
export function formatHourLabel(date: Date): string {
if (!(date instanceof Date)) {
return '';
}
const month = (date.getMonth() + 1).toString().padStart(2, '0');
const day = date.getDate().toString().padStart(2, '0');
const hour = date.getHours().toString().padStart(2, '0');
return `${month}-${day} ${hour}:00`;
}
/**
* 格式化日期标签
*/
export function formatDayLabel(date: Date): string {
if (!(date instanceof Date)) {
return '';
}
const year = date.getFullYear();
const month = (date.getMonth() + 1).toString().padStart(2, '0');
const day = date.getDate().toString().padStart(2, '0');
return `${year}-${month}-${day}`;
}
/**
* 构建小时级别的数据序列
*/
export function buildHourlySeriesByModel(usageData: any, metric: 'requests' | 'tokens' = 'requests'): {
labels: string[];
dataByModel: Map<string, number[]>;
hasData: boolean;
} {
const hourMs = 60 * 60 * 1000;
const now = new Date();
const currentHour = new Date(now);
currentHour.setMinutes(0, 0, 0);
const earliestBucket = new Date(currentHour);
earliestBucket.setHours(earliestBucket.getHours() - 23);
const earliestTime = earliestBucket.getTime();
const labels: string[] = [];
for (let i = 0; i < 24; i++) {
const bucketStart = earliestTime + i * hourMs;
labels.push(formatHourLabel(new Date(bucketStart)));
}
const details = collectUsageDetails(usageData);
const dataByModel = new Map<string, number[]>();
let hasData = false;
if (!details.length) {
return { labels, dataByModel, hasData };
}
details.forEach(detail => {
const timestamp = Date.parse(detail.timestamp);
if (Number.isNaN(timestamp)) {
return;
}
const normalized = new Date(timestamp);
normalized.setMinutes(0, 0, 0);
const bucketStart = normalized.getTime();
const lastBucketTime = earliestTime + (labels.length - 1) * hourMs;
if (bucketStart < earliestTime || bucketStart > lastBucketTime) {
return;
}
const bucketIndex = Math.floor((bucketStart - earliestTime) / hourMs);
if (bucketIndex < 0 || bucketIndex >= labels.length) {
return;
}
const modelName = detail.__modelName || 'Unknown';
if (!dataByModel.has(modelName)) {
dataByModel.set(modelName, new Array(labels.length).fill(0));
}
const bucketValues = dataByModel.get(modelName)!;
if (metric === 'tokens') {
bucketValues[bucketIndex] += extractTotalTokens(detail);
} else {
bucketValues[bucketIndex] += 1;
}
hasData = true;
});
return { labels, dataByModel, hasData };
}
/**
* 构建日级别的数据序列
*/
export function buildDailySeriesByModel(usageData: any, metric: 'requests' | 'tokens' = 'requests'): {
labels: string[];
dataByModel: Map<string, number[]>;
hasData: boolean;
} {
const details = collectUsageDetails(usageData);
const valuesByModel = new Map<string, Map<string, number>>();
const labelsSet = new Set<string>();
let hasData = false;
if (!details.length) {
return { labels: [], dataByModel: new Map(), hasData };
}
details.forEach(detail => {
const timestamp = Date.parse(detail.timestamp);
if (Number.isNaN(timestamp)) {
return;
}
const dayLabel = formatDayLabel(new Date(timestamp));
if (!dayLabel) {
return;
}
const modelName = detail.__modelName || 'Unknown';
if (!valuesByModel.has(modelName)) {
valuesByModel.set(modelName, new Map());
}
const modelDayMap = valuesByModel.get(modelName)!;
const increment = metric === 'tokens' ? extractTotalTokens(detail) : 1;
modelDayMap.set(dayLabel, (modelDayMap.get(dayLabel) || 0) + increment);
labelsSet.add(dayLabel);
hasData = true;
});
const labels = Array.from(labelsSet).sort();
const dataByModel = new Map<string, number[]>();
valuesByModel.forEach((dayMap, modelName) => {
const series = labels.map(label => dayMap.get(label) || 0);
dataByModel.set(modelName, series);
});
return { labels, dataByModel, hasData };
}
export interface ChartDataset {
label: string;
data: number[];
borderColor: string;
backgroundColor: string | CanvasGradient | ((context: ScriptableContext<'line'>) => string | CanvasGradient);
fill: boolean;
tension: number;
}
export interface ChartData {
labels: string[];
datasets: ChartDataset[];
}
const CHART_COLORS = [
{ borderColor: '#3b82f6', backgroundColor: 'rgba(59, 130, 246, 0.15)' },
{ borderColor: '#22c55e', backgroundColor: 'rgba(34, 197, 94, 0.15)' },
{ borderColor: '#f59e0b', backgroundColor: 'rgba(245, 158, 11, 0.15)' },
{ borderColor: '#ef4444', backgroundColor: 'rgba(239, 68, 68, 0.15)' },
{ borderColor: '#8b5cf6', backgroundColor: 'rgba(139, 92, 246, 0.15)' },
{ borderColor: '#06b6d4', backgroundColor: 'rgba(6, 182, 212, 0.15)' },
{ borderColor: '#ec4899', backgroundColor: 'rgba(236, 72, 153, 0.15)' },
{ borderColor: '#84cc16', backgroundColor: 'rgba(132, 204, 22, 0.15)' },
{ borderColor: '#f97316', backgroundColor: 'rgba(249, 115, 22, 0.15)' },
];
const clamp = (value: number, min: number, max: number) => Math.min(Math.max(value, min), max);
const hexToRgb = (hex: string): { r: number; g: number; b: number } | null => {
const normalized = hex.trim().replace('#', '');
if (normalized.length !== 6) {
return null;
}
const r = Number.parseInt(normalized.slice(0, 2), 16);
const g = Number.parseInt(normalized.slice(2, 4), 16);
const b = Number.parseInt(normalized.slice(4, 6), 16);
if (![r, g, b].every((channel) => Number.isFinite(channel))) {
return null;
}
return { r, g, b };
};
const withAlpha = (hex: string, alpha: number) => {
const rgb = hexToRgb(hex);
if (!rgb) {
return hex;
}
const clamped = clamp(alpha, 0, 1);
return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${clamped})`;
};
const buildAreaGradient = (context: ScriptableContext<'line'>, baseHex: string, fallback: string) => {
const chart = context.chart;
const ctx = chart.ctx;
const area = chart.chartArea;
if (!area) {
return fallback;
}
const gradient = ctx.createLinearGradient(0, area.top, 0, area.bottom);
gradient.addColorStop(0, withAlpha(baseHex, 0.28));
gradient.addColorStop(0.6, withAlpha(baseHex, 0.12));
gradient.addColorStop(1, withAlpha(baseHex, 0.02));
return gradient;
};
/**
* 构建图表数据
*/
export function buildChartData(
usageData: any,
period: 'hour' | 'day' = 'day',
metric: 'requests' | 'tokens' = 'requests',
selectedModels: string[] = []
): ChartData {
const baseSeries = period === 'hour'
? buildHourlySeriesByModel(usageData, metric)
: buildDailySeriesByModel(usageData, metric);
const { labels, dataByModel } = baseSeries;
// Build "All" series as sum of all models
const getAllSeries = (): number[] => {
const summed = new Array(labels.length).fill(0);
dataByModel.forEach(values => {
values.forEach((value, idx) => {
summed[idx] = (summed[idx] || 0) + value;
});
});
return summed;
};
// Determine which models to show
const modelsToShow = selectedModels.length > 0 ? selectedModels : ['all'];
const datasets: ChartDataset[] = modelsToShow.map((model, index) => {
const isAll = model === 'all';
const data = isAll ? getAllSeries() : (dataByModel.get(model) || new Array(labels.length).fill(0));
const colorIndex = index % CHART_COLORS.length;
const style = CHART_COLORS[colorIndex];
const shouldFill = modelsToShow.length === 1 || (isAll && modelsToShow.length > 1);
return {
label: isAll ? 'All Models' : model,
data,
borderColor: style.borderColor,
backgroundColor: shouldFill
? (ctx) => buildAreaGradient(ctx, style.borderColor, style.backgroundColor)
: style.backgroundColor,
fill: shouldFill,
tension: 0.35
};
});
return { labels, datasets };
}
/**
* 依据 usage 数据计算密钥使用统计
*/
/**
* 状态栏单个格子的状态
*/
export type StatusBlockState = 'success' | 'failure' | 'mixed' | 'idle';
/**
* 状态栏数据
*/
export interface StatusBarData {
blocks: StatusBlockState[];
successRate: number;
totalSuccess: number;
totalFailure: number;
}
/**
* 计算状态栏数据最近1小时分为20个5分钟的时间块
* 注意20个块 × 5分钟 = 100分钟但我们只使用最近60分钟的数据
* 所以实际只有最后12个块可能有数据前8个块将始终为 idle
*/
export function calculateStatusBarData(
usageDetails: UsageDetail[],
sourceFilter?: string,
authIndexFilter?: number
): StatusBarData {
const BLOCK_COUNT = 20;
const BLOCK_DURATION_MS = 5 * 60 * 1000; // 5 minutes
const HOUR_MS = 60 * 60 * 1000;
const now = Date.now();
const hourAgo = now - HOUR_MS;
// Initialize blocks
const blockStats: Array<{ success: number; failure: number }> = Array.from(
{ length: BLOCK_COUNT },
() => ({ success: 0, failure: 0 })
);
let totalSuccess = 0;
let totalFailure = 0;
// Filter and bucket the usage details
usageDetails.forEach((detail) => {
const timestamp = Date.parse(detail.timestamp);
if (Number.isNaN(timestamp) || timestamp < hourAgo || timestamp > now) {
return;
}
// Apply filters if provided
if (sourceFilter !== undefined && detail.source !== sourceFilter) {
return;
}
if (authIndexFilter !== undefined && detail.auth_index !== authIndexFilter) {
return;
}
// Calculate which block this falls into (0 = oldest, 19 = newest)
const ageMs = now - timestamp;
const blockIndex = BLOCK_COUNT - 1 - Math.floor(ageMs / BLOCK_DURATION_MS);
if (blockIndex >= 0 && blockIndex < BLOCK_COUNT) {
if (detail.failed) {
blockStats[blockIndex].failure += 1;
totalFailure += 1;
} else {
blockStats[blockIndex].success += 1;
totalSuccess += 1;
}
}
});
// Convert stats to block states
const blocks: StatusBlockState[] = blockStats.map((stat) => {
if (stat.success === 0 && stat.failure === 0) {
return 'idle';
}
if (stat.failure === 0) {
return 'success';
}
if (stat.success === 0) {
return 'failure';
}
return 'mixed';
});
// Calculate success rate
const total = totalSuccess + totalFailure;
const successRate = total > 0 ? (totalSuccess / total) * 100 : 100;
return {
blocks,
successRate,
totalSuccess,
totalFailure
};
}
export function computeKeyStats(usageData: any, masker: (val: string) => string = maskApiKey): KeyStats {
if (!usageData) {
return { bySource: {}, byAuthIndex: {} };
}
const sourceStats: Record<string, KeyStatBucket> = {};
const authIndexStats: Record<string, KeyStatBucket> = {};
const ensureBucket = (bucket: Record<string, KeyStatBucket>, key: string) => {
if (!bucket[key]) {
bucket[key] = { success: 0, failure: 0 };
}
return bucket[key];
};
const apis = usageData.apis || {};
Object.values(apis as any).forEach((apiEntry: any) => {
const models = apiEntry?.models || {};
Object.values(models as any).forEach((modelEntry: any) => {
const details = modelEntry?.details || [];
details.forEach((detail: any) => {
const source = maskUsageSensitiveValue(detail?.source, masker);
const authIndexKey = normalizeAuthIndex(detail?.auth_index);
const isFailed = detail?.failed === true;
if (source) {
const bucket = ensureBucket(sourceStats, source);
if (isFailed) {
bucket.failure += 1;
} else {
bucket.success += 1;
}
}
if (authIndexKey) {
const bucket = ensureBucket(authIndexStats, authIndexKey);
if (isFailed) {
bucket.failure += 1;
} else {
bucket.success += 1;
}
}
});
});
});
return {
bySource: sourceStats,
byAuthIndex: authIndexStats
};
}