mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-22 07:02:16 +03:00
refactor: decompose usageDb, handleSingleModelChat, UI components (T-15, T-28, T-29)
T-15 — Decompose usageDb.js (969→40 lines): - Extract src/lib/usage/migrations.js (legacy + JSON→SQLite migration) - Extract src/lib/usage/usageHistory.js (tracking, pending, log.txt) - Extract src/lib/usage/costCalculator.js (pure cost calculation) - Extract src/lib/usage/usageStats.js (dashboard aggregation) - Extract src/lib/usage/callLogs.js (structured logs, CRUD, rotation) - usageDb.js is now a thin facade re-exporting all functions T-28 — Decompose handleSingleModelChat (183→80 lines): - Extract handleNoCredentials() — credential error responses - Extract safeResolveProxy() — proxy resolution with error handling - Extract safeLogEvents() — fire-and-forget proxy + translation logging - Also created chatHelpers.js with standalone helper exports T-29 — Extract shared UI primitives (3230 total lines): - FilterBar.js — search input + filter chips dropdown - ColumnToggle.js — table column visibility toggle - DataTable.js — generic data table with sticky header, loading/empty Tests: 88/88 pass (no regressions)
This commit is contained in:
249
src/lib/usage/usageHistory.js
Normal file
249
src/lib/usage/usageHistory.js
Normal file
@@ -0,0 +1,249 @@
|
||||
/**
|
||||
* Usage History — extracted from usageDb.js (T-15)
|
||||
*
|
||||
* Usage tracking: saving, querying, and analytics shim for
|
||||
* the usage_history SQLite table.
|
||||
*
|
||||
* @module lib/usage/usageHistory
|
||||
*/
|
||||
|
||||
import { getDbInstance } from "../db/core.js";
|
||||
import { shouldPersistToDisk } from "./migrations.js";
|
||||
|
||||
// ──────────────── Pending Requests (in-memory) ────────────────
|
||||
|
||||
const pendingRequests = {
|
||||
byModel: {},
|
||||
byAccount: {},
|
||||
};
|
||||
|
||||
/**
|
||||
* Track a pending request.
|
||||
*/
|
||||
export function trackPendingRequest(model, provider, connectionId, started) {
|
||||
const modelKey = provider ? `${model} (${provider})` : model;
|
||||
|
||||
if (!pendingRequests.byModel[modelKey]) pendingRequests.byModel[modelKey] = 0;
|
||||
pendingRequests.byModel[modelKey] = Math.max(
|
||||
0,
|
||||
pendingRequests.byModel[modelKey] + (started ? 1 : -1)
|
||||
);
|
||||
|
||||
if (connectionId) {
|
||||
if (!pendingRequests.byAccount[connectionId]) pendingRequests.byAccount[connectionId] = {};
|
||||
if (!pendingRequests.byAccount[connectionId][modelKey])
|
||||
pendingRequests.byAccount[connectionId][modelKey] = 0;
|
||||
pendingRequests.byAccount[connectionId][modelKey] = Math.max(
|
||||
0,
|
||||
pendingRequests.byAccount[connectionId][modelKey] + (started ? 1 : -1)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the pending requests state (for usageStats).
|
||||
* @returns {{ byModel: Object, byAccount: Object }}
|
||||
*/
|
||||
export function getPendingRequests() {
|
||||
return pendingRequests;
|
||||
}
|
||||
|
||||
// ──────────────── getUsageDb Shim (backward compat) ────────────────
|
||||
|
||||
/**
|
||||
* Returns an object compatible with the old LowDB interface.
|
||||
* Only `api/usage/analytics/route.js` uses this — it reads `db.data.history`.
|
||||
*/
|
||||
export async function getUsageDb() {
|
||||
const db = getDbInstance();
|
||||
const rows = db.prepare("SELECT * FROM usage_history ORDER BY timestamp ASC").all();
|
||||
|
||||
const history = rows.map((r) => ({
|
||||
provider: r.provider,
|
||||
model: r.model,
|
||||
connectionId: r.connection_id,
|
||||
apiKeyId: r.api_key_id,
|
||||
apiKeyName: r.api_key_name,
|
||||
tokens: {
|
||||
input: r.tokens_input,
|
||||
output: r.tokens_output,
|
||||
cacheRead: r.tokens_cache_read,
|
||||
cacheCreation: r.tokens_cache_creation,
|
||||
reasoning: r.tokens_reasoning,
|
||||
},
|
||||
status: r.status,
|
||||
timestamp: r.timestamp,
|
||||
}));
|
||||
|
||||
return { data: { history } };
|
||||
}
|
||||
|
||||
// ──────────────── Save Request Usage ────────────────
|
||||
|
||||
/**
|
||||
* Save request usage entry to SQLite.
|
||||
*/
|
||||
export async function saveRequestUsage(entry) {
|
||||
if (!shouldPersistToDisk) return;
|
||||
|
||||
try {
|
||||
const db = getDbInstance();
|
||||
const timestamp = entry.timestamp || new Date().toISOString();
|
||||
|
||||
db.prepare(
|
||||
`
|
||||
INSERT INTO usage_history (provider, model, connection_id, api_key_id, api_key_name,
|
||||
tokens_input, tokens_output, tokens_cache_read, tokens_cache_creation, tokens_reasoning,
|
||||
status, timestamp)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`
|
||||
).run(
|
||||
entry.provider || null,
|
||||
entry.model || null,
|
||||
entry.connectionId || null,
|
||||
entry.apiKeyId || null,
|
||||
entry.apiKeyName || null,
|
||||
entry.tokens?.input ?? entry.tokens?.prompt_tokens ?? 0,
|
||||
entry.tokens?.output ?? entry.tokens?.completion_tokens ?? 0,
|
||||
entry.tokens?.cacheRead ?? entry.tokens?.cached_tokens ?? 0,
|
||||
entry.tokens?.cacheCreation ?? entry.tokens?.cache_creation_input_tokens ?? 0,
|
||||
entry.tokens?.reasoning ?? entry.tokens?.reasoning_tokens ?? 0,
|
||||
entry.status || null,
|
||||
timestamp
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Failed to save usage stats:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────────── Get Usage History ────────────────
|
||||
|
||||
/**
|
||||
* Get usage history with optional filters.
|
||||
*/
|
||||
export async function getUsageHistory(filter = {}) {
|
||||
const db = getDbInstance();
|
||||
let sql = "SELECT * FROM usage_history";
|
||||
const conditions = [];
|
||||
const params = {};
|
||||
|
||||
if (filter.provider) {
|
||||
conditions.push("provider = @provider");
|
||||
params.provider = filter.provider;
|
||||
}
|
||||
if (filter.model) {
|
||||
conditions.push("model = @model");
|
||||
params.model = filter.model;
|
||||
}
|
||||
if (filter.startDate) {
|
||||
conditions.push("timestamp >= @startDate");
|
||||
params.startDate = new Date(filter.startDate).toISOString();
|
||||
}
|
||||
if (filter.endDate) {
|
||||
conditions.push("timestamp <= @endDate");
|
||||
params.endDate = new Date(filter.endDate).toISOString();
|
||||
}
|
||||
|
||||
if (conditions.length > 0) {
|
||||
sql += " WHERE " + conditions.join(" AND ");
|
||||
}
|
||||
sql += " ORDER BY timestamp ASC";
|
||||
|
||||
const rows = db.prepare(sql).all(params);
|
||||
return rows.map((r) => ({
|
||||
provider: r.provider,
|
||||
model: r.model,
|
||||
connectionId: r.connection_id,
|
||||
apiKeyId: r.api_key_id,
|
||||
apiKeyName: r.api_key_name,
|
||||
tokens: {
|
||||
input: r.tokens_input,
|
||||
output: r.tokens_output,
|
||||
cacheRead: r.tokens_cache_read,
|
||||
cacheCreation: r.tokens_cache_creation,
|
||||
reasoning: r.tokens_reasoning,
|
||||
},
|
||||
status: r.status,
|
||||
timestamp: r.timestamp,
|
||||
}));
|
||||
}
|
||||
|
||||
// ──────────────── Request Log (log.txt) ────────────────
|
||||
|
||||
import fs from "fs";
|
||||
import { LOG_FILE } from "./migrations.js";
|
||||
|
||||
function formatLogDate(date = new Date()) {
|
||||
const pad = (n) => String(n).padStart(2, "0");
|
||||
const d = pad(date.getDate());
|
||||
const m = pad(date.getMonth() + 1);
|
||||
const y = date.getFullYear();
|
||||
const h = pad(date.getHours());
|
||||
const min = pad(date.getMinutes());
|
||||
const s = pad(date.getSeconds());
|
||||
return `${d}-${m}-${y} ${h}:${min}:${s}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Append to log.txt.
|
||||
*/
|
||||
export async function appendRequestLog({ model, provider, connectionId, tokens, status }) {
|
||||
if (!shouldPersistToDisk) return;
|
||||
|
||||
try {
|
||||
const timestamp = formatLogDate();
|
||||
const p = provider?.toUpperCase() || "-";
|
||||
const m = model || "-";
|
||||
|
||||
let account = connectionId ? connectionId.slice(0, 8) : "-";
|
||||
try {
|
||||
const { getProviderConnections } = await import("@/lib/localDb.js");
|
||||
const connections = await getProviderConnections();
|
||||
const conn = connections.find((c) => c.id === connectionId);
|
||||
if (conn) account = conn.name || conn.email || account;
|
||||
} catch {}
|
||||
|
||||
const sent =
|
||||
tokens?.input !== undefined
|
||||
? tokens.input
|
||||
: tokens?.prompt_tokens !== undefined
|
||||
? tokens.prompt_tokens
|
||||
: "-";
|
||||
const received =
|
||||
tokens?.output !== undefined
|
||||
? tokens.output
|
||||
: tokens?.completion_tokens !== undefined
|
||||
? tokens.completion_tokens
|
||||
: "-";
|
||||
|
||||
const line = `${timestamp} | ${m} | ${p} | ${account} | ${sent} | ${received} | ${status}\n`;
|
||||
fs.appendFileSync(LOG_FILE, line);
|
||||
|
||||
const content = fs.readFileSync(LOG_FILE, "utf-8");
|
||||
const lines = content.trim().split("\n");
|
||||
if (lines.length > 200) {
|
||||
fs.writeFileSync(LOG_FILE, lines.slice(-200).join("\n") + "\n");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to append to log.txt:", error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get last N lines of log.txt.
|
||||
*/
|
||||
export async function getRecentLogs(limit = 200) {
|
||||
if (!shouldPersistToDisk) return [];
|
||||
if (!fs || typeof fs.existsSync !== "function") return [];
|
||||
if (!LOG_FILE) return [];
|
||||
if (!fs.existsSync(LOG_FILE)) return [];
|
||||
|
||||
try {
|
||||
const content = fs.readFileSync(LOG_FILE, "utf-8");
|
||||
const lines = content.trim().split("\n");
|
||||
return lines.slice(-limit).reverse();
|
||||
} catch (error) {
|
||||
console.error("[usageDb] Failed to read log.txt:", error.message);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user