Files
OmniRoute/src/lib/usage/usageStats.ts
diegosouzapw fceb9d4145 fix(core): resolve runtime edge cases and TypeScript regressions
Tighten request and provider typing across the SSE pipeline to fix
nullability and inference issues in Claude compatibility, wildcard
routing, usage tracking, proxy fetch, and response sanitization.

Address runtime edge cases by normalizing Bailian hosts, guarding TLS
session creation, preserving custom provider base URLs, and using safer
OAuth form param construction during token refresh flows.

Update dashboard data path exports and usage stats typing, and align
E2E/unit tests with paginated API responses, internal model sync auth,
and current response payload shapes.
2026-04-17 20:02:45 -03:00

266 lines
8.5 KiB
TypeScript

/**
* Usage Stats — extracted from usageDb.js (T-15)
*
* Aggregates usage data into stats for the dashboard:
* totals, by provider/model/account/apiKey, 10-minute buckets.
*
* @module lib/usage/usageStats
*/
import { getDbInstance } from "../db/core";
import { getPendingRequests } from "./usageHistory";
import { getAccountDisplayName } from "@/lib/display/names";
import { calculateCost } from "./costCalculator";
type JsonRecord = Record<string, unknown>;
type UsageBucket = {
requests: number;
promptTokens: number;
completionTokens: number;
cost: number;
};
type UsageBreakdown = UsageBucket & {
rawModel?: string;
provider?: string;
lastUsed?: string;
connectionId?: string;
accountName?: string;
apiKeyId?: string | null;
apiKeyName?: string;
};
type ActiveRequest = {
model: string;
provider: string;
account: string;
count: number;
};
function asRecord(value: unknown): JsonRecord {
return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonRecord) : {};
}
function toNumber(value: unknown): number {
if (typeof value === "number" && Number.isFinite(value)) return value;
if (typeof value === "string" && value.trim().length > 0) {
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : 0;
}
return 0;
}
function toStringOrEmpty(value: unknown): string {
return typeof value === "string" ? value : "";
}
/**
* Get aggregated usage stats.
*/
export async function getUsageStats() {
const db = getDbInstance();
const rows = db.prepare("SELECT * FROM usage_history ORDER BY timestamp ASC").all() as unknown[];
const { getProviderConnections } = await import("@/lib/localDb");
let allConnections: unknown[] = [];
try {
const loadedConnections = await getProviderConnections();
allConnections = Array.isArray(loadedConnections) ? loadedConnections : [];
} catch {}
const connectionMap: Record<string, string> = {};
for (const connRaw of allConnections) {
const conn = asRecord(connRaw);
const connectionId = toStringOrEmpty(conn.id);
if (!connectionId) continue;
connectionMap[connectionId] =
toStringOrEmpty(conn.name) || toStringOrEmpty(conn.email) || connectionId;
}
const pendingRequests = getPendingRequests();
const stats: {
totalRequests: number;
totalPromptTokens: number;
totalCompletionTokens: number;
totalCost: number;
byProvider: Record<string, UsageBreakdown>;
byModel: Record<string, UsageBreakdown>;
byAccount: Record<string, UsageBreakdown>;
byApiKey: Record<string, UsageBreakdown>;
last10Minutes: UsageBucket[];
pending: ReturnType<typeof getPendingRequests>;
activeRequests: ActiveRequest[];
} = {
totalRequests: rows.length,
totalPromptTokens: 0,
totalCompletionTokens: 0,
totalCost: 0,
byProvider: {},
byModel: {},
byAccount: {},
byApiKey: {},
last10Minutes: [],
pending: pendingRequests,
activeRequests: [],
};
// Build active requests
for (const [connectionId, models] of Object.entries(pendingRequests.byAccount)) {
for (const [modelKey, count] of Object.entries(models)) {
if (count > 0) {
const accountName =
connectionMap[connectionId] || getAccountDisplayName({ id: connectionId });
const match = modelKey.match(/^(.*) \((.*)\)$/);
stats.activeRequests.push({
model: match ? match[1] : modelKey,
provider: match ? match[2] : "unknown",
account: accountName,
count,
});
}
}
}
// 10-minute buckets
const now = new Date();
const currentMinuteStart = new Date(Math.floor(now.getTime() / 60000) * 60000);
const bucketMap: Record<number, UsageBucket> = {};
for (let i = 0; i < 10; i++) {
const bucketTime = new Date(currentMinuteStart.getTime() - (9 - i) * 60 * 1000);
const bucketKey = bucketTime.getTime();
bucketMap[bucketKey] = { requests: 0, promptTokens: 0, completionTokens: 0, cost: 0 };
stats.last10Minutes.push(bucketMap[bucketKey]);
}
const tenMinutesAgo = new Date(currentMinuteStart.getTime() - 9 * 60 * 1000);
for (const rowRaw of rows) {
const row = asRecord(rowRaw);
const provider = toStringOrEmpty(row.provider) || "unknown";
const model = toStringOrEmpty(row.model) || "unknown";
const timestamp = toStringOrEmpty(row.timestamp) || new Date(0).toISOString();
const connectionId = toStringOrEmpty(row.connection_id) || null;
const apiKeyId = toStringOrEmpty(row.api_key_id) || null;
const apiKeyName = toStringOrEmpty(row.api_key_name) || null;
const promptTokens = toNumber(row.tokens_input);
const completionTokens = toNumber(row.tokens_output);
const entryTime = new Date(timestamp);
const entryTokens = {
input: toNumber(row.tokens_input),
output: toNumber(row.tokens_output),
cacheRead: toNumber(row.tokens_cache_read),
cacheCreation: toNumber(row.tokens_cache_creation),
reasoning: toNumber(row.tokens_reasoning),
};
const entryCost = await calculateCost(provider, model, entryTokens);
stats.totalPromptTokens += promptTokens;
stats.totalCompletionTokens += completionTokens;
stats.totalCost += entryCost;
// 10-min buckets
if (entryTime >= tenMinutesAgo && entryTime <= now) {
const entryMinuteStart = Math.floor(entryTime.getTime() / 60000) * 60000;
if (bucketMap[entryMinuteStart]) {
bucketMap[entryMinuteStart].requests++;
bucketMap[entryMinuteStart].promptTokens += promptTokens;
bucketMap[entryMinuteStart].completionTokens += completionTokens;
bucketMap[entryMinuteStart].cost += entryCost;
}
}
// By Provider
if (!stats.byProvider[provider]) {
stats.byProvider[provider] = {
requests: 0,
promptTokens: 0,
completionTokens: 0,
cost: 0,
};
}
stats.byProvider[provider].requests++;
stats.byProvider[provider].promptTokens += promptTokens;
stats.byProvider[provider].completionTokens += completionTokens;
stats.byProvider[provider].cost += entryCost;
// By Model
const modelKey = provider ? `${model} (${provider})` : model;
if (!stats.byModel[modelKey]) {
stats.byModel[modelKey] = {
requests: 0,
promptTokens: 0,
completionTokens: 0,
cost: 0,
rawModel: model,
provider,
lastUsed: timestamp,
};
}
stats.byModel[modelKey].requests++;
stats.byModel[modelKey].promptTokens += promptTokens;
stats.byModel[modelKey].completionTokens += completionTokens;
stats.byModel[modelKey].cost += entryCost;
if (new Date(timestamp) > new Date(stats.byModel[modelKey].lastUsed || timestamp)) {
stats.byModel[modelKey].lastUsed = timestamp;
}
// By Account
if (connectionId) {
const accountName =
connectionMap[connectionId] || getAccountDisplayName({ id: connectionId });
const accountKey = `${model} (${provider} - ${accountName})`;
if (!stats.byAccount[accountKey]) {
stats.byAccount[accountKey] = {
requests: 0,
promptTokens: 0,
completionTokens: 0,
cost: 0,
rawModel: model,
provider,
connectionId,
accountName,
lastUsed: timestamp,
};
}
stats.byAccount[accountKey].requests++;
stats.byAccount[accountKey].promptTokens += promptTokens;
stats.byAccount[accountKey].completionTokens += completionTokens;
stats.byAccount[accountKey].cost += entryCost;
if (new Date(timestamp) > new Date(stats.byAccount[accountKey].lastUsed || timestamp)) {
stats.byAccount[accountKey].lastUsed = timestamp;
}
}
// By API key
if (apiKeyId || apiKeyName) {
const keyName = apiKeyName || apiKeyId || "unknown";
const keyId = apiKeyId || null;
const apiKey = keyId ? `${keyName} (${keyId})` : keyName;
if (!stats.byApiKey[apiKey]) {
stats.byApiKey[apiKey] = {
requests: 0,
promptTokens: 0,
completionTokens: 0,
cost: 0,
apiKeyId: keyId,
apiKeyName: keyName,
lastUsed: timestamp,
};
}
stats.byApiKey[apiKey].requests++;
stats.byApiKey[apiKey].promptTokens += promptTokens;
stats.byApiKey[apiKey].completionTokens += completionTokens;
stats.byApiKey[apiKey].cost += entryCost;
if (new Date(timestamp) > new Date(stats.byApiKey[apiKey].lastUsed || timestamp)) {
stats.byApiKey[apiKey].lastUsed = timestamp;
}
}
}
return stats;
}