mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-03 05:45:04 +03:00
refactor(types): Wave 3b — usage, CLI runtime, SSE auth/logger typed
- callLogs.ts: typed all CRUD params + error catches - usageHistory.ts: typed pending requests, filter/entry params - cliRuntime.ts: typed CLI_TOOLS map + all arrow function params - sse/auth.ts: typed credentials/fallback/mutex params + Date.getTime() - sse/utils/logger.ts: typed all logger functions + made data optional TS errors: 578 → 490 (-88) Total reduction: 984 → 490 (-494, 50.2%) Build: ✅ Tests: 368/368 ✅
This commit is contained in:
@@ -24,7 +24,7 @@ function generateLogId() {
|
||||
/**
|
||||
* Save a structured call log entry.
|
||||
*/
|
||||
export async function saveCallLog(entry) {
|
||||
export async function saveCallLog(entry: any) {
|
||||
if (!shouldPersistToDisk) return;
|
||||
|
||||
try {
|
||||
@@ -38,7 +38,7 @@ export async function saveCallLog(entry) {
|
||||
} catch {}
|
||||
|
||||
// Truncate large payloads for DB storage (keep under 8KB each)
|
||||
const truncatePayload = (obj) => {
|
||||
const truncatePayload = (obj: any) => {
|
||||
if (!obj) return null;
|
||||
const str = JSON.stringify(obj);
|
||||
if (str.length <= 8192) return str;
|
||||
@@ -107,7 +107,7 @@ export async function saveCallLog(entry) {
|
||||
entry.requestBody,
|
||||
entry.responseBody
|
||||
);
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
console.error("[callLogs] Failed to save call log:", error.message);
|
||||
}
|
||||
}
|
||||
@@ -115,7 +115,7 @@ export async function saveCallLog(entry) {
|
||||
/**
|
||||
* Write call log as JSON file to disk (full payloads, not truncated).
|
||||
*/
|
||||
function writeCallLogToDisk(logEntry, requestBody, responseBody) {
|
||||
function writeCallLogToDisk(logEntry: any, requestBody: any, responseBody: any) {
|
||||
if (!CALL_LOGS_DIR) return;
|
||||
|
||||
try {
|
||||
@@ -136,7 +136,7 @@ function writeCallLogToDisk(logEntry, requestBody, responseBody) {
|
||||
};
|
||||
|
||||
fs.writeFileSync(path.join(dir, filename), JSON.stringify(fullEntry, null, 2));
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
console.error("[callLogs] Failed to write disk log:", err.message);
|
||||
}
|
||||
}
|
||||
@@ -160,7 +160,7 @@ export function rotateCallLogs() {
|
||||
console.log(`[callLogs] Rotated old logs: ${entry}`);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
console.error("[callLogs] Failed to rotate logs:", err.message);
|
||||
}
|
||||
}
|
||||
@@ -175,11 +175,11 @@ if (shouldPersistToDisk) {
|
||||
/**
|
||||
* Get call logs with optional filtering.
|
||||
*/
|
||||
export async function getCallLogs(filter = {}) {
|
||||
export async function getCallLogs(filter: any = {}) {
|
||||
const db = getDbInstance();
|
||||
let sql = "SELECT * FROM call_logs";
|
||||
const conditions = [];
|
||||
const params = {};
|
||||
const conditions: string[] = [];
|
||||
const params: Record<string, unknown> = {};
|
||||
|
||||
if (filter.status) {
|
||||
if (filter.status === "error") {
|
||||
@@ -257,7 +257,7 @@ export async function getCallLogs(filter = {}) {
|
||||
/**
|
||||
* Get a single call log by ID (with full payloads from disk when available).
|
||||
*/
|
||||
export async function getCallLogById(id) {
|
||||
export async function getCallLogById(id: string) {
|
||||
const db = getDbInstance();
|
||||
const row = db.prepare("SELECT * FROM call_logs WHERE id = ?").get(id);
|
||||
if (!row) return null;
|
||||
@@ -296,7 +296,7 @@ export async function getCallLogById(id) {
|
||||
responseBody: diskEntry.responseBody ?? entry.responseBody,
|
||||
};
|
||||
}
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
console.error("[callLogs] Failed to read full log from disk:", err.message);
|
||||
}
|
||||
}
|
||||
@@ -307,7 +307,7 @@ export async function getCallLogById(id) {
|
||||
/**
|
||||
* Read the full (untruncated) log entry from disk.
|
||||
*/
|
||||
function readFullLogFromDisk(entry) {
|
||||
function readFullLogFromDisk(entry: any) {
|
||||
if (!CALL_LOGS_DIR || !entry.timestamp) return null;
|
||||
|
||||
try {
|
||||
@@ -332,7 +332,7 @@ function readFullLogFromDisk(entry) {
|
||||
if (files.length > 0) {
|
||||
return JSON.parse(fs.readFileSync(path.join(dir, files[0]), "utf8"));
|
||||
}
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
console.error("[callLogs] Disk log read error:", err.message);
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,10 @@ import { shouldPersistToDisk } from "./migrations";
|
||||
|
||||
// ──────────────── Pending Requests (in-memory) ────────────────
|
||||
|
||||
const pendingRequests = {
|
||||
const pendingRequests: {
|
||||
byModel: Record<string, number>;
|
||||
byAccount: Record<string, Record<string, number>>;
|
||||
} = {
|
||||
byModel: {},
|
||||
byAccount: {},
|
||||
};
|
||||
@@ -21,7 +24,7 @@ const pendingRequests = {
|
||||
/**
|
||||
* Track a pending request.
|
||||
*/
|
||||
export function trackPendingRequest(model, provider, connectionId, started) {
|
||||
export function trackPendingRequest(model: string, provider: string, connectionId: string | null, started: boolean) {
|
||||
const modelKey = provider ? `${model} (${provider})` : model;
|
||||
|
||||
if (!pendingRequests.byModel[modelKey]) pendingRequests.byModel[modelKey] = 0;
|
||||
@@ -84,7 +87,7 @@ export async function getUsageDb() {
|
||||
/**
|
||||
* Save request usage entry to SQLite.
|
||||
*/
|
||||
export async function saveRequestUsage(entry) {
|
||||
export async function saveRequestUsage(entry: any) {
|
||||
if (!shouldPersistToDisk) return;
|
||||
|
||||
try {
|
||||
@@ -122,11 +125,11 @@ export async function saveRequestUsage(entry) {
|
||||
/**
|
||||
* Get usage history with optional filters.
|
||||
*/
|
||||
export async function getUsageHistory(filter = {}) {
|
||||
export async function getUsageHistory(filter: any = {}) {
|
||||
const db = getDbInstance();
|
||||
let sql = "SELECT * FROM usage_history";
|
||||
const conditions = [];
|
||||
const params = {};
|
||||
const conditions: string[] = [];
|
||||
const params: Record<string, unknown> = {};
|
||||
|
||||
if (filter.provider) {
|
||||
conditions.push("provider = @provider");
|
||||
@@ -175,7 +178,7 @@ import fs from "fs";
|
||||
import { LOG_FILE } from "./migrations";
|
||||
|
||||
function formatLogDate(date = new Date()) {
|
||||
const pad = (n) => String(n).padStart(2, "0");
|
||||
const pad = (n: number) => String(n).padStart(2, "0");
|
||||
const d = pad(date.getDate());
|
||||
const m = pad(date.getMonth() + 1);
|
||||
const y = date.getFullYear();
|
||||
@@ -188,7 +191,7 @@ function formatLogDate(date = new Date()) {
|
||||
/**
|
||||
* Append to log.txt.
|
||||
*/
|
||||
export async function appendRequestLog({ model, provider, connectionId, tokens, status }) {
|
||||
export async function appendRequestLog({ model, provider, connectionId, tokens, status }: { model?: string; provider?: string; connectionId?: string; tokens?: any; status?: string | number }) {
|
||||
if (!shouldPersistToDisk) return;
|
||||
|
||||
try {
|
||||
@@ -225,7 +228,7 @@ export async function appendRequestLog({ model, provider, connectionId, tokens,
|
||||
if (lines.length > 200) {
|
||||
fs.writeFileSync(LOG_FILE, lines.slice(-200).join("\n") + "\n");
|
||||
}
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
console.error("Failed to append to log.txt:", error.message);
|
||||
}
|
||||
}
|
||||
@@ -243,7 +246,7 @@ export async function getRecentLogs(limit = 200) {
|
||||
const content = fs.readFileSync(LOG_FILE, "utf-8");
|
||||
const lines = content.trim().split("\n");
|
||||
return lines.slice(-limit).reverse();
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
console.error("[usageDb] Failed to read log.txt:", error.message);
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { spawn } from "child_process";
|
||||
const VALID_RUNTIME_MODES = new Set(["auto", "host", "container"]);
|
||||
const FALSE_VALUES = new Set(["0", "false", "no", "off"]);
|
||||
|
||||
const CLI_TOOLS = {
|
||||
const CLI_TOOLS: Record<string, any> = {
|
||||
claude: {
|
||||
defaultCommand: "claude",
|
||||
envBinKey: "CLI_CLAUDE_BIN",
|
||||
@@ -90,12 +90,12 @@ const CLI_TOOLS = {
|
||||
|
||||
const isWindows = () => process.platform === "win32";
|
||||
|
||||
const parseBoolean = (value, defaultValue = true) => {
|
||||
const parseBoolean = (value: unknown, defaultValue = true) => {
|
||||
if (value == null || value === "") return defaultValue;
|
||||
return !FALSE_VALUES.has(String(value).trim().toLowerCase());
|
||||
};
|
||||
|
||||
const runProcess = (command, args, { env, timeoutMs = 3000 } = {}) =>
|
||||
const runProcess = (command: string, args: string[], { env, timeoutMs = 3000 }: { env?: Record<string, string | undefined>; timeoutMs?: number } = {}): Promise<any> =>
|
||||
new Promise((resolve) => {
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
@@ -108,7 +108,7 @@ const runProcess = (command, args, { env, timeoutMs = 3000 } = {}) =>
|
||||
child.kill("SIGKILL");
|
||||
}, timeoutMs);
|
||||
|
||||
const done = (result) => {
|
||||
const done = (result: any) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
clearTimeout(timer);
|
||||
@@ -168,7 +168,7 @@ const getLookupEnv = () => {
|
||||
return env;
|
||||
};
|
||||
|
||||
const resolveToolCommands = (toolId) => {
|
||||
const resolveToolCommands = (toolId: string): string[] => {
|
||||
const tool = CLI_TOOLS[toolId];
|
||||
if (!tool) return [];
|
||||
const envCommand = String(process.env[tool.envBinKey] || "").trim();
|
||||
@@ -179,7 +179,7 @@ const resolveToolCommands = (toolId) => {
|
||||
return tool.defaultCommand ? [tool.defaultCommand] : [];
|
||||
};
|
||||
|
||||
const checkExplicitPath = async (commandPath) => {
|
||||
const checkExplicitPath = async (commandPath: string) => {
|
||||
try {
|
||||
await fs.access(commandPath, fs.constants.F_OK);
|
||||
} catch {
|
||||
@@ -194,7 +194,7 @@ const checkExplicitPath = async (commandPath) => {
|
||||
}
|
||||
};
|
||||
|
||||
const locateCommand = async (command, env) => {
|
||||
const locateCommand = async (command: string, env: Record<string, string | undefined>) => {
|
||||
if (!command) {
|
||||
return { installed: false, commandPath: null, reason: "missing_command" };
|
||||
}
|
||||
@@ -231,7 +231,7 @@ const locateCommand = async (command, env) => {
|
||||
return { installed: !!first, commandPath: first, reason: first ? null : "not_found" };
|
||||
};
|
||||
|
||||
const locateCommandCandidate = async (commands, env) => {
|
||||
const locateCommandCandidate = async (commands: string[], env: Record<string, string | undefined>) => {
|
||||
if (!Array.isArray(commands) || commands.length === 0) {
|
||||
return { command: null, installed: false, commandPath: null, reason: "missing_command" };
|
||||
}
|
||||
@@ -246,7 +246,7 @@ const locateCommandCandidate = async (commands, env) => {
|
||||
return { command: commands[0], installed: false, commandPath: null, reason: "not_found" };
|
||||
};
|
||||
|
||||
const checkRunnable = async (commandPath, env, timeoutMs = 4000) => {
|
||||
const checkRunnable = async (commandPath: string, env: Record<string, string | undefined>, timeoutMs = 4000) => {
|
||||
for (const args of [["--version"], ["-v"]]) {
|
||||
const result = await runProcess(commandPath, args, { env, timeoutMs });
|
||||
if (result.ok) {
|
||||
@@ -267,23 +267,23 @@ export const ensureCliConfigWriteAllowed = () => {
|
||||
export const getCliConfigHome = () =>
|
||||
String(process.env.CLI_CONFIG_HOME || "").trim() || os.homedir();
|
||||
|
||||
export const getCliConfigPaths = (toolId) => {
|
||||
export const getCliConfigPaths = (toolId: string) => {
|
||||
const tool = CLI_TOOLS[toolId];
|
||||
if (!tool) return null;
|
||||
const home = getCliConfigHome();
|
||||
return Object.fromEntries(
|
||||
Object.entries(tool.paths).map(([key, relativePath]) => [key, path.join(home, relativePath)])
|
||||
Object.entries(tool.paths).map(([key, relativePath]) => [key, path.join(home, relativePath as string)])
|
||||
);
|
||||
};
|
||||
|
||||
export const getCliPrimaryConfigPath = (toolId) => {
|
||||
export const getCliPrimaryConfigPath = (toolId: string) => {
|
||||
const paths = getCliConfigPaths(toolId);
|
||||
if (!paths) return null;
|
||||
const firstKey = Object.keys(paths)[0];
|
||||
return firstKey ? paths[firstKey] : null;
|
||||
};
|
||||
|
||||
export const getCliRuntimeStatus = async (toolId) => {
|
||||
export const getCliRuntimeStatus = async (toolId: string) => {
|
||||
const tool = CLI_TOOLS[toolId];
|
||||
const runtimeMode = getRuntimeMode();
|
||||
if (!tool) {
|
||||
|
||||
@@ -21,7 +21,7 @@ let selectionMutex = Promise.resolve();
|
||||
// ─── Anti-Thundering Herd: per-connection mutex for markAccountUnavailable ───
|
||||
// Prevents multiple concurrent requests from marking the same connection
|
||||
// unavailable in parallel, which was the root cause of cascading 502 lockouts.
|
||||
const markMutexes = new Map();
|
||||
const markMutexes = new Map<string, Promise<void>>();
|
||||
|
||||
/**
|
||||
* Get provider credentials from localDb
|
||||
@@ -29,10 +29,10 @@ const markMutexes = new Map();
|
||||
* @param {string} provider - Provider name
|
||||
* @param {string|null} excludeConnectionId - Connection ID to exclude (for retry with next account)
|
||||
*/
|
||||
export async function getProviderCredentials(provider, excludeConnectionId = null) {
|
||||
export async function getProviderCredentials(provider: string, excludeConnectionId: string | null = null) {
|
||||
// Acquire mutex to prevent race conditions
|
||||
const currentMutex = selectionMutex;
|
||||
let resolveMutex;
|
||||
let resolveMutex: (() => void) | undefined;
|
||||
selectionMutex = new Promise((resolve) => {
|
||||
resolveMutex = resolve;
|
||||
});
|
||||
@@ -105,7 +105,7 @@ export async function getProviderCredentials(provider, excludeConnectionId = nul
|
||||
(c) => c.rateLimitedUntil && new Date(c.rateLimitedUntil).getTime() > Date.now()
|
||||
);
|
||||
const earliestConn = rateLimitedConns.sort(
|
||||
(a, b) => new Date(a.rateLimitedUntil) - new Date(b.rateLimitedUntil)
|
||||
(a: any, b: any) => new Date(a.rateLimitedUntil).getTime() - new Date(b.rateLimitedUntil).getTime()
|
||||
)[0];
|
||||
log.warn(
|
||||
"AUTH",
|
||||
@@ -131,11 +131,11 @@ export async function getProviderCredentials(provider, excludeConnectionId = nul
|
||||
const stickyLimit = settings.stickyRoundRobinLimit || 3;
|
||||
|
||||
// Sort by lastUsed (most recent first) to find current candidate
|
||||
const byRecency = [...availableConnections].sort((a, b) => {
|
||||
const byRecency = [...availableConnections].sort((a: any, b: any) => {
|
||||
if (!a.lastUsedAt && !b.lastUsedAt) return (a.priority || 999) - (b.priority || 999);
|
||||
if (!a.lastUsedAt) return 1;
|
||||
if (!b.lastUsedAt) return -1;
|
||||
return new Date(b.lastUsedAt) - new Date(a.lastUsedAt);
|
||||
return new Date(b.lastUsedAt).getTime() - new Date(a.lastUsedAt).getTime();
|
||||
});
|
||||
|
||||
const current = byRecency[0];
|
||||
@@ -151,11 +151,11 @@ export async function getProviderCredentials(provider, excludeConnectionId = nul
|
||||
});
|
||||
} else {
|
||||
// Pick the least recently used (excluding current if possible)
|
||||
const sortedByOldest = [...availableConnections].sort((a, b) => {
|
||||
const sortedByOldest = [...availableConnections].sort((a: any, b: any) => {
|
||||
if (!a.lastUsedAt && !b.lastUsedAt) return (a.priority || 999) - (b.priority || 999);
|
||||
if (!a.lastUsedAt) return -1;
|
||||
if (!b.lastUsedAt) return 1;
|
||||
return new Date(a.lastUsedAt) - new Date(b.lastUsedAt);
|
||||
return new Date(a.lastUsedAt).getTime() - new Date(b.lastUsedAt).getTime();
|
||||
});
|
||||
|
||||
connection = sortedByOldest[0];
|
||||
@@ -200,18 +200,14 @@ export async function getProviderCredentials(provider, excludeConnectionId = nul
|
||||
* @returns {{ shouldFallback: boolean, cooldownMs: number }}
|
||||
*/
|
||||
export async function markAccountUnavailable(
|
||||
connectionId,
|
||||
status,
|
||||
errorText,
|
||||
provider = null,
|
||||
model = null
|
||||
connectionId: string,
|
||||
status: number,
|
||||
errorText: string,
|
||||
provider: string | null = null,
|
||||
model: string | null = null
|
||||
) {
|
||||
// ─── Anti-Thundering Herd Mutex ─────────────────────────────────
|
||||
// Wait for any in-flight markAccountUnavailable call for the SAME connection.
|
||||
// This prevents 5 concurrent 502s from each independently marking the account
|
||||
// with backoffLevel 0 → 1, when only the first should.
|
||||
const currentMutex = markMutexes.get(connectionId) || Promise.resolve();
|
||||
let resolveMutex;
|
||||
let resolveMutex: (() => void) | undefined;
|
||||
markMutexes.set(
|
||||
connectionId,
|
||||
new Promise((resolve) => {
|
||||
@@ -284,7 +280,7 @@ export async function markAccountUnavailable(
|
||||
* Clear account error status (only if currently has error)
|
||||
* Optimized to avoid unnecessary DB updates
|
||||
*/
|
||||
export async function clearAccountError(connectionId, currentConnection) {
|
||||
export async function clearAccountError(connectionId: string, currentConnection: any) {
|
||||
// Only update if currently has error status
|
||||
const hasError =
|
||||
currentConnection.testStatus === "unavailable" ||
|
||||
@@ -306,7 +302,7 @@ export async function clearAccountError(connectionId, currentConnection) {
|
||||
/**
|
||||
* Extract API key from request headers
|
||||
*/
|
||||
export function extractApiKey(request) {
|
||||
export function extractApiKey(request: Request) {
|
||||
const authHeader = request.headers.get("Authorization");
|
||||
if (authHeader?.startsWith("Bearer ")) {
|
||||
return authHeader.slice(7);
|
||||
@@ -317,7 +313,7 @@ export function extractApiKey(request) {
|
||||
/**
|
||||
* Validate API key (optional - for local use can skip)
|
||||
*/
|
||||
export async function isValidApiKey(apiKey) {
|
||||
export async function isValidApiKey(apiKey: string) {
|
||||
if (!apiKey) return false;
|
||||
return await validateApiKey(apiKey);
|
||||
}
|
||||
|
||||
@@ -8,27 +8,27 @@ import { createLogger, logger as rootLogger } from "@/shared/utils/logger";
|
||||
|
||||
const log = createLogger("sse");
|
||||
|
||||
export function debug(tag, message, data) {
|
||||
export function debug(tag: string, message: string, data?: unknown) {
|
||||
log.debug({ tag, ...spreadData(data) }, message);
|
||||
}
|
||||
|
||||
export function info(tag, message, data) {
|
||||
export function info(tag: string, message: string, data?: unknown) {
|
||||
log.info({ tag, ...spreadData(data) }, message);
|
||||
}
|
||||
|
||||
export function warn(tag, message, data) {
|
||||
export function warn(tag: string, message: string, data?: unknown) {
|
||||
log.warn({ tag, ...spreadData(data) }, message);
|
||||
}
|
||||
|
||||
export function error(tag, message, data) {
|
||||
export function error(tag: string, message: string, data?: unknown) {
|
||||
log.error({ tag, ...spreadData(data) }, message);
|
||||
}
|
||||
|
||||
export function request(method, path, extra) {
|
||||
export function request(method: string, path: string, extra?: unknown) {
|
||||
log.info({ tag: "HTTP", method, path, ...spreadData(extra) }, `📥 ${method} ${path}`);
|
||||
}
|
||||
|
||||
export function response(status, duration, extra) {
|
||||
export function response(status: number, duration: number, extra?: unknown) {
|
||||
const level = status < 400 ? "info" : "error";
|
||||
log[level](
|
||||
{ tag: "HTTP", status, duration, ...spreadData(extra) },
|
||||
@@ -36,7 +36,7 @@ export function response(status, duration, extra) {
|
||||
);
|
||||
}
|
||||
|
||||
export function stream(event, data) {
|
||||
export function stream(event: string, data?: unknown) {
|
||||
log.debug({ tag: "STREAM", event, ...spreadData(data) }, `🌊 ${event}`);
|
||||
}
|
||||
|
||||
@@ -44,9 +44,9 @@ export function stream(event, data) {
|
||||
export { maskKey } from "@/shared/utils/formatting";
|
||||
|
||||
// Helper to spread data into structured fields
|
||||
function spreadData(data) {
|
||||
function spreadData(data: unknown): Record<string, unknown> {
|
||||
if (!data) return {};
|
||||
if (typeof data === "string") return { detail: data };
|
||||
if (typeof data === "object") return data;
|
||||
if (typeof data === "object") return data as Record<string, unknown>;
|
||||
return { detail: String(data) };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user