mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-22 07:02:16 +03:00
T-19 — Domain Layer: - modelAvailability.js: Model availability tracking with TTL cooldowns - costRules.js: Per-API-key budget management with daily/monthly limits - fallbackPolicy.js: Declarative fallback chain routing T-22 — Error Codes Catalog: - errorCodes.js: 24 codes in 6 categories + createErrorResponse helper T-23 — Correlation ID: - requestId.js: AsyncLocalStorage-based x-request-id propagation T-25 — Fetch Timeout: - fetchTimeout.js: AbortController wrapper with FETCH_TIMEOUT_MS env var T-27 — JSDoc + @ts-check: - Added @ts-check to 8 critical files TASKS.md updated: 37/46 tasks Concluído, 9 remaining Tests: 119/119 pass (88 existing + 31 new)
251 lines
7.4 KiB
JavaScript
251 lines
7.4 KiB
JavaScript
// @ts-check
|
|
/**
|
|
* 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 [];
|
|
}
|
|
}
|