mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-03 05:45:04 +03:00
refactor(types): Wave 3a — lib layer, db, compliance, domain typed
- proxyLogger.ts: ProxyLogEntry/ProxyLogFilters interfaces + typed params - errorCodes.ts: ErrorCodeDef/ErrorDetails interfaces - usageAnalytics.ts: typed computeAnalytics + helper function params - policyEngine.ts: PolicyRequest/PolicyVerdict/Policy interfaces + typed PolicyEngine class - db/providers.ts: typed all CRUD function params - db/settings.ts: typed pricing/proxy/settings params + Record<> index types - compliance/index.ts: typed audit log and noLog params - domain/responses.ts: typed all response factory params TS errors: 654 → 578 (-76) Total reduction: 984 → 578 (-406, 41.3%) Build: ✅ Tests: 368/368 ✅
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
// @ts-check
|
||||
/**
|
||||
* Policy Engine — FASE-06 Architecture Refactoring
|
||||
*
|
||||
@@ -6,9 +5,6 @@
|
||||
* fallback, cost, lockout, and circuit-breaker modules into a single
|
||||
* verdict before forwarding a request to a provider.
|
||||
*
|
||||
* Usage: Call `evaluateRequest(request)` before executing a chat request.
|
||||
* The function returns `{ allowed, reason, adjustments }`.
|
||||
*
|
||||
* @module domain/policyEngine
|
||||
*/
|
||||
|
||||
@@ -16,36 +12,39 @@ import { checkLockout } from "./lockoutPolicy";
|
||||
import { checkBudget } from "./costRules";
|
||||
import { resolveFallbackChain } from "./fallbackPolicy";
|
||||
|
||||
/**
|
||||
* @typedef {Object} PolicyRequest
|
||||
* @property {string} model - Requested model
|
||||
* @property {string} [apiKeyId] - API key identifier for budget checks
|
||||
* @property {string} [clientIp] - Client IP for lockout checks
|
||||
* @property {string} [provider] - Target provider
|
||||
*/
|
||||
interface PolicyRequest {
|
||||
model: string;
|
||||
apiKeyId?: string;
|
||||
clientIp?: string;
|
||||
provider?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} PolicyVerdict
|
||||
* @property {boolean} allowed - Whether the request is permitted
|
||||
* @property {string|null} reason - Human-readable denial reason (null if allowed)
|
||||
* @property {Object} adjustments - Optional flight-path adjustments
|
||||
* @property {string} [adjustments.model] - Replaced model (from combo/fallback)
|
||||
* @property {Array} [adjustments.fallbackChain] - Available fallbacks
|
||||
* @property {string} policyPhase - Which policy phase determined the outcome
|
||||
*/
|
||||
interface PolicyVerdict {
|
||||
allowed: boolean;
|
||||
reason: string | null;
|
||||
adjustments: Record<string, unknown>;
|
||||
policyPhase: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate a request against all domain policies.
|
||||
*
|
||||
* Evaluation order (short-circuits on first denial):
|
||||
* 1. Lockout — is the client/IP locked out?
|
||||
* 2. Budget — is the API key within budget?
|
||||
* 3. Fallback — is there a fallback chain for the model?
|
||||
*
|
||||
* @param {PolicyRequest} request
|
||||
* @returns {PolicyVerdict}
|
||||
*/
|
||||
export function evaluateRequest(request) {
|
||||
interface Policy {
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
enabled: boolean;
|
||||
priority: number;
|
||||
conditions?: {
|
||||
model_pattern?: string;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
actions?: {
|
||||
prefer_provider?: string[];
|
||||
block_model?: string[];
|
||||
max_tokens?: number;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
}
|
||||
|
||||
export function evaluateRequest(request: PolicyRequest): PolicyVerdict {
|
||||
const { model, apiKeyId, clientIp } = request;
|
||||
|
||||
// ── 1. Lockout Policy ──────────────────────────────
|
||||
@@ -88,15 +87,7 @@ export function evaluateRequest(request) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate a set of models against policies and return the first allowed one.
|
||||
* Useful for combo/fallback scenarios where multiple models may be tried.
|
||||
*
|
||||
* @param {string[]} models - Models to evaluate in order
|
||||
* @param {Omit<PolicyRequest, 'model'>} baseRequest - Base request without model
|
||||
* @returns {{ model: string, verdict: PolicyVerdict } | { model: null, verdict: PolicyVerdict }}
|
||||
*/
|
||||
export function evaluateFirstAllowed(models, baseRequest) {
|
||||
export function evaluateFirstAllowed(models: string[], baseRequest: Omit<PolicyRequest, "model">) {
|
||||
for (const model of models) {
|
||||
const verdict = evaluateRequest({ ...baseRequest, model });
|
||||
if (verdict.allowed) {
|
||||
@@ -111,59 +102,42 @@ export function evaluateFirstAllowed(models, baseRequest) {
|
||||
|
||||
// ─── Class-Based Policy Engine ───────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Matches a value against a glob pattern (supports * wildcard).
|
||||
* @param {string} pattern - Glob pattern (e.g. "gpt-*")
|
||||
* @param {string} value - Value to test
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function globMatch(pattern, value) {
|
||||
function globMatch(pattern: string, value: string): boolean {
|
||||
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
||||
return new RegExp(`^${escaped}$`).test(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Declarative Policy Engine — supports routing, access, and budget policies
|
||||
* with glob-based model matching and priority ordering.
|
||||
*
|
||||
* @example
|
||||
* const engine = new PolicyEngine();
|
||||
* engine.loadPolicies([{ id: "1", name: "prefer-openai", type: "routing", enabled: true, priority: 1, conditions: { model_pattern: "gpt-*" }, actions: { prefer_provider: ["openai"] } }]);
|
||||
* const result = engine.evaluate({ model: "gpt-4o" });
|
||||
*/
|
||||
export class PolicyEngine {
|
||||
_policies: Policy[];
|
||||
|
||||
constructor() {
|
||||
/** @type {Array} */
|
||||
this._policies = [];
|
||||
}
|
||||
|
||||
/** Load a full set of policies (replaces existing). */
|
||||
loadPolicies(policies) {
|
||||
loadPolicies(policies: Policy[]) {
|
||||
this._policies = [...policies];
|
||||
}
|
||||
|
||||
/** Add a single policy. */
|
||||
addPolicy(policy) {
|
||||
addPolicy(policy: Policy) {
|
||||
this._policies.push(policy);
|
||||
}
|
||||
|
||||
/** Remove a policy by id. */
|
||||
removePolicy(id) {
|
||||
removePolicy(id: string) {
|
||||
this._policies = this._policies.filter((p) => p.id !== id);
|
||||
}
|
||||
|
||||
/** Get current policies. */
|
||||
getPolicies() {
|
||||
getPolicies(): Policy[] {
|
||||
return [...this._policies];
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate a request context against all loaded policies.
|
||||
* @param {{ model: string }} context
|
||||
* @returns {{ allowed: boolean, reason?: string, preferredProviders: string[], appliedPolicies: string[], maxTokens?: number }}
|
||||
*/
|
||||
evaluate(context) {
|
||||
const result = {
|
||||
evaluate(context: { model: string }) {
|
||||
const result: {
|
||||
allowed: boolean;
|
||||
reason: string | undefined;
|
||||
preferredProviders: string[];
|
||||
appliedPolicies: string[];
|
||||
maxTokens: number | undefined;
|
||||
} = {
|
||||
allowed: true,
|
||||
reason: undefined,
|
||||
preferredProviders: [],
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
* @param {Object} [headers={}] - Additional headers
|
||||
* @returns {Response}
|
||||
*/
|
||||
export function successResponse(data, status = 200, headers = {}) {
|
||||
export function successResponse(data: unknown, status = 200, headers: Record<string, string> = {}) {
|
||||
return new Response(JSON.stringify(data), {
|
||||
status,
|
||||
headers: {
|
||||
@@ -34,7 +34,7 @@ export function successResponse(data, status = 200, headers = {}) {
|
||||
* @param {Object} [details] - Additional error details
|
||||
* @returns {Response}
|
||||
*/
|
||||
export function apiErrorResponse(status, code, message, details) {
|
||||
export function apiErrorResponse(status: number, code: string, message: string, details?: unknown) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: {
|
||||
@@ -58,7 +58,7 @@ export function apiErrorResponse(status, code, message, details) {
|
||||
* @param {Object} [details] - Validation details
|
||||
* @returns {Response}
|
||||
*/
|
||||
export function badRequest(message, details) {
|
||||
export function badRequest(message: string, details?: unknown) {
|
||||
return apiErrorResponse(400, "BAD_REQUEST", message, details);
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ export function notFound(resource = "Resource") {
|
||||
* @param {string} message - Conflict description
|
||||
* @returns {Response}
|
||||
*/
|
||||
export function conflict(message) {
|
||||
export function conflict(message: string) {
|
||||
return apiErrorResponse(409, "CONFLICT", message);
|
||||
}
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ export function initAuditLog() {
|
||||
* @param {Object|string} [entry.details] - Additional details
|
||||
* @param {string} [entry.ipAddress] - Client IP
|
||||
*/
|
||||
export function logAuditEvent(entry) {
|
||||
export function logAuditEvent(entry: { action: string; actor?: string; target?: string; details?: unknown; ipAddress?: string }) {
|
||||
const db = getDb();
|
||||
if (!db) return;
|
||||
|
||||
@@ -83,12 +83,12 @@ export function logAuditEvent(entry) {
|
||||
* @param {number} [filter.offset=0] - Pagination offset
|
||||
* @returns {Array<{ id: number, timestamp: string, action: string, actor: string, target: string, details: any, ip_address: string }>}
|
||||
*/
|
||||
export function getAuditLog(filter = {}) {
|
||||
export function getAuditLog(filter: { action?: string; actor?: string; limit?: number; offset?: number } = {}) {
|
||||
const db = getDb();
|
||||
if (!db) return [];
|
||||
|
||||
const conditions = [];
|
||||
const params = [];
|
||||
const conditions: string[] = [];
|
||||
const params: (string | number)[] = [];
|
||||
|
||||
if (filter.action) {
|
||||
conditions.push("action = ?");
|
||||
@@ -125,7 +125,7 @@ const noLogKeys = new Set();
|
||||
* @param {string} apiKeyId
|
||||
* @param {boolean} noLog
|
||||
*/
|
||||
export function setNoLog(apiKeyId, noLog) {
|
||||
export function setNoLog(apiKeyId: string, noLog: boolean) {
|
||||
if (noLog) {
|
||||
noLogKeys.add(apiKeyId);
|
||||
} else {
|
||||
@@ -139,7 +139,7 @@ export function setNoLog(apiKeyId, noLog) {
|
||||
* @param {string} apiKeyId
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isNoLog(apiKeyId) {
|
||||
export function isNoLog(apiKeyId: string) {
|
||||
return noLogKeys.has(apiKeyId);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,11 +9,11 @@ import { encryptConnectionFields, decryptConnectionFields } from "./encryption";
|
||||
|
||||
// ──────────────── Provider Connections ────────────────
|
||||
|
||||
export async function getProviderConnections(filter = {}) {
|
||||
export async function getProviderConnections(filter: any = {}) {
|
||||
const db = getDbInstance();
|
||||
let sql = "SELECT * FROM provider_connections";
|
||||
const conditions = [];
|
||||
const params = {};
|
||||
const conditions: string[] = [];
|
||||
const params: Record<string, unknown> = {};
|
||||
|
||||
if (filter.provider) {
|
||||
conditions.push("provider = @provider");
|
||||
@@ -33,13 +33,13 @@ export async function getProviderConnections(filter = {}) {
|
||||
return rows.map((r) => decryptConnectionFields(cleanNulls(rowToCamel(r))));
|
||||
}
|
||||
|
||||
export async function getProviderConnectionById(id) {
|
||||
export async function getProviderConnectionById(id: string) {
|
||||
const db = getDbInstance();
|
||||
const row = db.prepare("SELECT * FROM provider_connections WHERE id = ?").get(id);
|
||||
return row ? decryptConnectionFields(cleanNulls(rowToCamel(row))) : null;
|
||||
}
|
||||
|
||||
export async function createProviderConnection(data) {
|
||||
export async function createProviderConnection(data: any) {
|
||||
const db = getDbInstance();
|
||||
const now = new Date().toISOString();
|
||||
|
||||
@@ -98,7 +98,7 @@ export async function createProviderConnection(data) {
|
||||
connectionPriority = (max?.maxP || 0) + 1;
|
||||
}
|
||||
|
||||
const connection = {
|
||||
const connection: Record<string, any> = {
|
||||
id: uuidv4(),
|
||||
provider: data.provider,
|
||||
authType: data.authType || "oauth",
|
||||
@@ -151,7 +151,7 @@ export async function createProviderConnection(data) {
|
||||
return cleanNulls(connection);
|
||||
}
|
||||
|
||||
function _insertConnectionRow(db, conn) {
|
||||
function _insertConnectionRow(db: any, conn: any) {
|
||||
db.prepare(
|
||||
`
|
||||
INSERT INTO provider_connections (
|
||||
@@ -217,7 +217,7 @@ function _insertConnectionRow(db, conn) {
|
||||
});
|
||||
}
|
||||
|
||||
function _updateConnectionRow(db, id, data) {
|
||||
function _updateConnectionRow(db: any, id: string, data: any) {
|
||||
const now = data.updatedAt || new Date().toISOString();
|
||||
db.prepare(
|
||||
`
|
||||
@@ -280,7 +280,7 @@ function _updateConnectionRow(db, id, data) {
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateProviderConnection(id, data) {
|
||||
export async function updateProviderConnection(id: string, data: any) {
|
||||
const db = getDbInstance();
|
||||
const existing = db.prepare("SELECT * FROM provider_connections WHERE id = ?").get(id);
|
||||
if (!existing) return null;
|
||||
@@ -296,7 +296,7 @@ export async function updateProviderConnection(id, data) {
|
||||
return cleanNulls(merged);
|
||||
}
|
||||
|
||||
export async function deleteProviderConnection(id) {
|
||||
export async function deleteProviderConnection(id: string) {
|
||||
const db = getDbInstance();
|
||||
const existing = db.prepare("SELECT provider FROM provider_connections WHERE id = ?").get(id);
|
||||
if (!existing) return false;
|
||||
@@ -307,19 +307,19 @@ export async function deleteProviderConnection(id) {
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function deleteProviderConnectionsByProvider(providerId) {
|
||||
export async function deleteProviderConnectionsByProvider(providerId: string) {
|
||||
const db = getDbInstance();
|
||||
const result = db.prepare("DELETE FROM provider_connections WHERE provider = ?").run(providerId);
|
||||
backupDbFile("pre-write");
|
||||
return result.changes;
|
||||
}
|
||||
|
||||
export async function reorderProviderConnections(providerId) {
|
||||
export async function reorderProviderConnections(providerId: string) {
|
||||
const db = getDbInstance();
|
||||
_reorderConnections(db, providerId);
|
||||
}
|
||||
|
||||
function _reorderConnections(db, providerId) {
|
||||
function _reorderConnections(db: any, providerId: string) {
|
||||
const rows = db
|
||||
.prepare(
|
||||
"SELECT id, priority, updated_at FROM provider_connections WHERE provider = ? ORDER BY priority ASC, updated_at DESC"
|
||||
@@ -338,10 +338,10 @@ export async function cleanupProviderConnections() {
|
||||
|
||||
// ──────────────── Provider Nodes ────────────────
|
||||
|
||||
export async function getProviderNodes(filter = {}) {
|
||||
export async function getProviderNodes(filter: any = {}) {
|
||||
const db = getDbInstance();
|
||||
let sql = "SELECT * FROM provider_nodes";
|
||||
const params = {};
|
||||
const params: Record<string, unknown> = {};
|
||||
|
||||
if (filter.type) {
|
||||
sql += " WHERE type = @type";
|
||||
@@ -351,13 +351,13 @@ export async function getProviderNodes(filter = {}) {
|
||||
return db.prepare(sql).all(params).map(rowToCamel);
|
||||
}
|
||||
|
||||
export async function getProviderNodeById(id) {
|
||||
export async function getProviderNodeById(id: string) {
|
||||
const db = getDbInstance();
|
||||
const row = db.prepare("SELECT * FROM provider_nodes WHERE id = ?").get(id);
|
||||
return row ? rowToCamel(row) : null;
|
||||
}
|
||||
|
||||
export async function createProviderNode(data) {
|
||||
export async function createProviderNode(data: any) {
|
||||
const db = getDbInstance();
|
||||
const now = new Date().toISOString();
|
||||
|
||||
@@ -383,7 +383,7 @@ export async function createProviderNode(data) {
|
||||
return node;
|
||||
}
|
||||
|
||||
export async function updateProviderNode(id, data) {
|
||||
export async function updateProviderNode(id: string, data: any) {
|
||||
const db = getDbInstance();
|
||||
const existing = db.prepare("SELECT * FROM provider_nodes WHERE id = ?").get(id);
|
||||
if (!existing) return null;
|
||||
@@ -410,7 +410,7 @@ export async function updateProviderNode(id, data) {
|
||||
return merged;
|
||||
}
|
||||
|
||||
export async function deleteProviderNode(id) {
|
||||
export async function deleteProviderNode(id: string) {
|
||||
const db = getDbInstance();
|
||||
const existing = db.prepare("SELECT * FROM provider_nodes WHERE id = ?").get(id);
|
||||
if (!existing) return null;
|
||||
|
||||
@@ -11,7 +11,7 @@ import { PROVIDER_ID_TO_ALIAS } from "@omniroute/open-sse/config/providerModels.
|
||||
export async function getSettings() {
|
||||
const db = getDbInstance();
|
||||
const rows = db.prepare("SELECT key, value FROM key_value WHERE namespace = 'settings'").all();
|
||||
const settings = { cloudEnabled: false, stickyRoundRobinLimit: 3, requireLogin: true };
|
||||
const settings: Record<string, any> = { cloudEnabled: false, stickyRoundRobinLimit: 3, requireLogin: true };
|
||||
for (const row of rows) {
|
||||
settings[row.key] = JSON.parse(row.value);
|
||||
}
|
||||
@@ -32,7 +32,7 @@ export async function getSettings() {
|
||||
return settings;
|
||||
}
|
||||
|
||||
export async function updateSettings(updates) {
|
||||
export async function updateSettings(updates: Record<string, unknown>) {
|
||||
const db = getDbInstance();
|
||||
const insert = db.prepare(
|
||||
"INSERT OR REPLACE INTO key_value (namespace, key, value) VALUES ('settings', ?, ?)"
|
||||
@@ -57,7 +57,7 @@ export async function isCloudEnabled() {
|
||||
export async function getPricing() {
|
||||
const db = getDbInstance();
|
||||
const rows = db.prepare("SELECT key, value FROM key_value WHERE namespace = 'pricing'").all();
|
||||
const userPricing = {};
|
||||
const userPricing: Record<string, any> = {};
|
||||
for (const row of rows) {
|
||||
userPricing[row.key] = JSON.parse(row.value);
|
||||
}
|
||||
@@ -65,8 +65,8 @@ export async function getPricing() {
|
||||
const { getDefaultPricing } = await import("@/shared/constants/pricing");
|
||||
const defaultPricing = getDefaultPricing();
|
||||
|
||||
const mergedPricing = {};
|
||||
for (const [provider, models] of Object.entries(defaultPricing)) {
|
||||
const mergedPricing: Record<string, any> = {};
|
||||
for (const [provider, models] of Object.entries(defaultPricing) as [string, any][]) {
|
||||
mergedPricing[provider] = { ...models };
|
||||
if (userPricing[provider]) {
|
||||
for (const [model, pricing] of Object.entries(userPricing[provider])) {
|
||||
@@ -92,7 +92,7 @@ export async function getPricing() {
|
||||
return mergedPricing;
|
||||
}
|
||||
|
||||
export async function getPricingForModel(provider, model) {
|
||||
export async function getPricingForModel(provider: string, model: string) {
|
||||
const pricing = await getPricing();
|
||||
if (pricing[provider]?.[model]) return pricing[provider][model];
|
||||
|
||||
@@ -106,14 +106,14 @@ export async function getPricingForModel(provider, model) {
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function updatePricing(pricingData) {
|
||||
export async function updatePricing(pricingData: Record<string, any>) {
|
||||
const db = getDbInstance();
|
||||
const insert = db.prepare(
|
||||
"INSERT OR REPLACE INTO key_value (namespace, key, value) VALUES ('pricing', ?, ?)"
|
||||
);
|
||||
|
||||
const rows = db.prepare("SELECT key, value FROM key_value WHERE namespace = 'pricing'").all();
|
||||
const existing = {};
|
||||
const existing: Record<string, any> = {};
|
||||
for (const row of rows) existing[row.key] = JSON.parse(row.value);
|
||||
|
||||
const tx = db.transaction(() => {
|
||||
@@ -130,7 +130,7 @@ export async function updatePricing(pricingData) {
|
||||
return updated;
|
||||
}
|
||||
|
||||
export async function resetPricing(provider, model) {
|
||||
export async function resetPricing(provider: string, model?: string) {
|
||||
const db = getDbInstance();
|
||||
|
||||
if (model) {
|
||||
@@ -177,14 +177,14 @@ const ALIAS_TO_PROVIDER_ID = Object.entries(PROVIDER_ID_TO_ALIAS).reduce(
|
||||
return acc;
|
||||
},
|
||||
{}
|
||||
);
|
||||
) as Record<string, string>;
|
||||
|
||||
function resolveProviderAliasOrId(providerOrAlias) {
|
||||
function resolveProviderAliasOrId(providerOrAlias: string): string {
|
||||
if (typeof providerOrAlias !== "string") return providerOrAlias;
|
||||
return ALIAS_TO_PROVIDER_ID[providerOrAlias] || providerOrAlias;
|
||||
}
|
||||
|
||||
function getComboModelProvider(modelEntry) {
|
||||
function getComboModelProvider(modelEntry: any): string | null {
|
||||
if (modelEntry && typeof modelEntry.provider === "string") {
|
||||
return resolveProviderAliasOrId(modelEntry.provider);
|
||||
}
|
||||
@@ -203,7 +203,7 @@ function getComboModelProvider(modelEntry) {
|
||||
return resolveProviderAliasOrId(providerOrAlias);
|
||||
}
|
||||
|
||||
function migrateProxyEntry(value) {
|
||||
function migrateProxyEntry(value: any) {
|
||||
if (!value) return null;
|
||||
if (typeof value === "object" && value.type) return value;
|
||||
if (typeof value !== "string") return null;
|
||||
@@ -261,14 +261,14 @@ export async function getProxyConfig() {
|
||||
return raw;
|
||||
}
|
||||
|
||||
export async function getProxyForLevel(level, id) {
|
||||
export async function getProxyForLevel(level: string, id?: string | null) {
|
||||
const config = await getProxyConfig();
|
||||
if (level === "global") return config.global || null;
|
||||
const map = config[level + "s"] || config[level] || {};
|
||||
return (id ? map[id] : null) || null;
|
||||
}
|
||||
|
||||
export async function setProxyForLevel(level, id, proxy) {
|
||||
export async function setProxyForLevel(level: string, id: string | null, proxy: any) {
|
||||
const db = getDbInstance();
|
||||
const config = await getProxyConfig();
|
||||
|
||||
@@ -298,7 +298,7 @@ export async function deleteProxyForLevel(level, id) {
|
||||
return setProxyForLevel(level, id, null);
|
||||
}
|
||||
|
||||
export async function resolveProxyForConnection(connectionId) {
|
||||
export async function resolveProxyForConnection(connectionId: string) {
|
||||
const config = await getProxyConfig();
|
||||
|
||||
if (connectionId && config.keys?.[connectionId]) {
|
||||
@@ -346,7 +346,7 @@ export async function resolveProxyForConnection(connectionId) {
|
||||
return { proxy: null, level: "direct", levelId: null };
|
||||
}
|
||||
|
||||
export async function setProxyConfig(config) {
|
||||
export async function setProxyConfig(config: any) {
|
||||
if (config.level !== undefined) {
|
||||
return setProxyForLevel(config.level, config.id || null, config.proxy);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,41 @@ import { getDbInstance, isCloud, isBuildPhase } from "./db/core";
|
||||
const shouldPersistToDisk = !isCloud && !isBuildPhase;
|
||||
|
||||
const MAX_ENTRIES = 500;
|
||||
const proxyLogs = [];
|
||||
|
||||
interface ProxyInfo {
|
||||
type: string;
|
||||
host: string;
|
||||
port: number | string;
|
||||
}
|
||||
|
||||
interface ProxyLogEntry {
|
||||
id: string;
|
||||
timestamp: string;
|
||||
status: string;
|
||||
proxy: ProxyInfo | null;
|
||||
level: string;
|
||||
levelId: string | null;
|
||||
provider: string | null;
|
||||
targetUrl: string | null;
|
||||
publicIp: string | null;
|
||||
latencyMs: number;
|
||||
error: string | null;
|
||||
connectionId: string | null;
|
||||
comboId: string | null;
|
||||
account: string | null;
|
||||
tlsFingerprint: boolean;
|
||||
}
|
||||
|
||||
interface ProxyLogFilters {
|
||||
status?: string;
|
||||
type?: string;
|
||||
provider?: string;
|
||||
level?: string;
|
||||
search?: string;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
const proxyLogs: ProxyLogEntry[] = [];
|
||||
|
||||
// ──────────────── Startup: hydrate from DB ────────────────
|
||||
|
||||
@@ -22,7 +56,7 @@ function loadFromDb() {
|
||||
const db = getDbInstance();
|
||||
const rows = db
|
||||
.prepare("SELECT * FROM proxy_logs ORDER BY timestamp DESC LIMIT ?")
|
||||
.all(MAX_ENTRIES);
|
||||
.all(MAX_ENTRIES) as any[];
|
||||
|
||||
for (const row of rows) {
|
||||
proxyLogs.push({
|
||||
@@ -49,7 +83,7 @@ function loadFromDb() {
|
||||
if (proxyLogs.length > 0) {
|
||||
console.log(`[proxyLogger] Loaded ${proxyLogs.length} proxy logs from SQLite`);
|
||||
}
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
console.warn("[proxyLogger] Failed to load from DB:", err.message);
|
||||
}
|
||||
}
|
||||
@@ -58,23 +92,8 @@ loadFromDb();
|
||||
|
||||
// ──────────────── Log a proxy event ────────────────
|
||||
|
||||
/**
|
||||
* @param {Object} entry
|
||||
* @param {"success"|"error"|"timeout"} entry.status
|
||||
* @param {Object} entry.proxy - { type, host, port }
|
||||
* @param {"key"|"combo"|"provider"|"global"|"direct"} entry.level
|
||||
* @param {string} [entry.levelId]
|
||||
* @param {string} [entry.provider]
|
||||
* @param {string} [entry.targetUrl]
|
||||
* @param {string} [entry.publicIp]
|
||||
* @param {number} [entry.latencyMs]
|
||||
* @param {string} [entry.error]
|
||||
* @param {string} [entry.connectionId]
|
||||
* @param {string} [entry.comboId]
|
||||
* @param {boolean} [entry.tlsFingerprint]
|
||||
*/
|
||||
export function logProxyEvent(entry) {
|
||||
const log = {
|
||||
export function logProxyEvent(entry: Partial<ProxyLogEntry>) {
|
||||
const log: ProxyLogEntry = {
|
||||
id: uuidv4(),
|
||||
timestamp: new Date().toISOString(),
|
||||
status: entry.status || "success",
|
||||
@@ -130,7 +149,7 @@ export function logProxyEvent(entry) {
|
||||
});
|
||||
|
||||
// Trim old entries
|
||||
const count = db.prepare("SELECT COUNT(*) as cnt FROM proxy_logs").get()?.cnt || 0;
|
||||
const count = (db.prepare("SELECT COUNT(*) as cnt FROM proxy_logs").get() as any)?.cnt || 0;
|
||||
if (count > MAX_ENTRIES) {
|
||||
db.prepare(
|
||||
`DELETE FROM proxy_logs WHERE id IN (
|
||||
@@ -138,7 +157,7 @@ export function logProxyEvent(entry) {
|
||||
)`
|
||||
).run(count - MAX_ENTRIES);
|
||||
}
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
console.warn("[proxyLogger] Failed to persist:", err.message);
|
||||
}
|
||||
}
|
||||
@@ -152,7 +171,7 @@ export function logProxyEvent(entry) {
|
||||
* Get proxy logs with optional filters.
|
||||
* Reads from in-memory for speed (already hydrated from DB on startup).
|
||||
*/
|
||||
export function getProxyLogs(filters = {}) {
|
||||
export function getProxyLogs(filters: ProxyLogFilters = {}) {
|
||||
let logs = [...proxyLogs];
|
||||
|
||||
if (filters.status) {
|
||||
@@ -202,7 +221,7 @@ export function clearProxyLogs() {
|
||||
try {
|
||||
const db = getDbInstance();
|
||||
db.prepare("DELETE FROM proxy_logs").run();
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
console.warn("[proxyLogger] Failed to clear DB:", err.message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import { calculateCost } from "@/lib/usageDb";
|
||||
* @param {string} range - "1d" | "7d" | "30d" | "90d" | "ytd" | "all"
|
||||
* @returns {{ start: Date, end: Date }}
|
||||
*/
|
||||
function getDateRange(range) {
|
||||
function getDateRange(range: string) {
|
||||
const end = new Date();
|
||||
let start;
|
||||
|
||||
@@ -48,7 +48,7 @@ function getDateRange(range) {
|
||||
/**
|
||||
* Format a Date to "YYYY-MM-DD" string
|
||||
*/
|
||||
function toDateKey(date) {
|
||||
function toDateKey(date: Date) {
|
||||
const y = date.getFullYear();
|
||||
const m = String(date.getMonth() + 1).padStart(2, "0");
|
||||
const d = String(date.getDate()).padStart(2, "0");
|
||||
@@ -58,7 +58,7 @@ function toDateKey(date) {
|
||||
/**
|
||||
* Short model name (strip provider prefix paths)
|
||||
*/
|
||||
function shortModelName(model) {
|
||||
function shortModelName(model: string) {
|
||||
if (!model) return "unknown";
|
||||
// "accounts/fireworks/models/gpt-oss-120b" → "gpt-oss-120b"
|
||||
const parts = model.split("/");
|
||||
@@ -72,7 +72,7 @@ function shortModelName(model) {
|
||||
* @param {Object} connectionMap - Map of connectionId → account name
|
||||
* @returns {Object} Analytics data
|
||||
*/
|
||||
export async function computeAnalytics(history, range = "30d", connectionMap = {}) {
|
||||
export async function computeAnalytics(history: any[], range = "30d", connectionMap: Record<string, string> = {}) {
|
||||
const { start, end } = getDateRange(range);
|
||||
|
||||
// ---- Filtered entries ----
|
||||
@@ -88,25 +88,25 @@ export async function computeAnalytics(history, range = "30d", connectionMap = {
|
||||
completionTokens: 0,
|
||||
totalCost: 0,
|
||||
totalRequests: entries.length,
|
||||
uniqueModels: new Set(),
|
||||
uniqueAccounts: new Set(),
|
||||
uniqueApiKeys: new Set(),
|
||||
uniqueModels: new Set<string>(),
|
||||
uniqueAccounts: new Set<string>(),
|
||||
uniqueApiKeys: new Set<string>(),
|
||||
};
|
||||
|
||||
// ---- Daily trend ----
|
||||
const dailyMap = {}; // "YYYY-MM-DD" → { requests, promptTokens, completionTokens, cost }
|
||||
const dailyByModelMap = {}; // "YYYY-MM-DD" → { modelShort → tokens }
|
||||
const dailyMap: Record<string, any> = {}; // "YYYY-MM-DD" → { requests, promptTokens, completionTokens, cost }
|
||||
const dailyByModelMap: Record<string, Record<string, number>> = {}; // "YYYY-MM-DD" → { modelShort → tokens }
|
||||
|
||||
// ---- Activity heatmap (always last 365 days, regardless of range filter) ----
|
||||
const heatmapStart = new Date();
|
||||
heatmapStart.setDate(heatmapStart.getDate() - 364);
|
||||
const activityMap = {};
|
||||
const activityMap: Record<string, number> = {};
|
||||
|
||||
// ---- By model / account / provider ----
|
||||
const byModelMap = {};
|
||||
const byAccountMap = {};
|
||||
const byProviderMap = {};
|
||||
const byApiKeyMap = {};
|
||||
const byModelMap: Record<string, any> = {};
|
||||
const byAccountMap: Record<string, any> = {};
|
||||
const byProviderMap: Record<string, any> = {};
|
||||
const byApiKeyMap: Record<string, any> = {};
|
||||
|
||||
// ---- Weekly pattern (0=Sun..6=Sat) ----
|
||||
const weeklyTokens = [0, 0, 0, 0, 0, 0, 0];
|
||||
@@ -251,7 +251,7 @@ export async function computeAnalytics(history, range = "30d", connectionMap = {
|
||||
const dailyTrend = Object.values(dailyMap).sort((a, b) => a.date.localeCompare(b.date));
|
||||
|
||||
// Daily by model — collect all unique model names
|
||||
const allModels = new Set();
|
||||
const allModels = new Set<string>();
|
||||
for (const day of Object.values(dailyByModelMap)) {
|
||||
for (const m of Object.keys(day)) allModels.add(m);
|
||||
}
|
||||
|
||||
@@ -11,18 +11,20 @@
|
||||
* @module shared/constants/errorCodes
|
||||
*/
|
||||
|
||||
// @ts-check
|
||||
export interface ErrorCodeDef {
|
||||
code: string;
|
||||
message: string;
|
||||
httpStatus: number;
|
||||
category: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} ErrorCodeDef
|
||||
* @property {string} code - Error code (e.g. "AUTH_001")
|
||||
* @property {string} message - Human-readable message
|
||||
* @property {number} httpStatus - HTTP status code
|
||||
* @property {string} category - Category (AUTH, PROXY, RATE_LIMIT, etc.)
|
||||
*/
|
||||
interface ErrorDetails {
|
||||
detail?: string;
|
||||
requestId?: string;
|
||||
retryAfter?: number;
|
||||
}
|
||||
|
||||
/** @type {Record<string, ErrorCodeDef>} */
|
||||
export const ERROR_CODES = {
|
||||
export const ERROR_CODES: Record<string, ErrorCodeDef> = {
|
||||
// ── Auth ──
|
||||
AUTH_001: { code: "AUTH_001", message: "Authentication required", httpStatus: 401, category: "AUTH" },
|
||||
AUTH_002: { code: "AUTH_002", message: "Invalid API key", httpStatus: 401, category: "AUTH" },
|
||||
@@ -62,17 +64,7 @@ export const ERROR_CODES = {
|
||||
INTERNAL_003: { code: "INTERNAL_003", message: "Circuit breaker open", httpStatus: 503, category: "INTERNAL" },
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a standardized error response.
|
||||
*
|
||||
* @param {string} code - Error code from ERROR_CODES
|
||||
* @param {Object} [details] - Additional error details
|
||||
* @param {string} [details.detail] - Extra detail message
|
||||
* @param {string} [details.requestId] - Correlation request ID
|
||||
* @param {number} [details.retryAfter] - Retry-After seconds
|
||||
* @returns {{ error: { code: string, message: string, category: string, detail?: string, requestId?: string }, status: number, retryAfter?: number }}
|
||||
*/
|
||||
export function createErrorResponse(code, details = {}) {
|
||||
export function createErrorResponse(code: string, details: ErrorDetails = {}) {
|
||||
const def = ERROR_CODES[code];
|
||||
if (!def) {
|
||||
return {
|
||||
@@ -85,7 +77,7 @@ export function createErrorResponse(code, details = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
const response = {
|
||||
const response: any = {
|
||||
error: {
|
||||
code: def.code,
|
||||
message: def.message,
|
||||
@@ -103,12 +95,6 @@ export function createErrorResponse(code, details = {}) {
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all error codes for a category.
|
||||
*
|
||||
* @param {string} category
|
||||
* @returns {ErrorCodeDef[]}
|
||||
*/
|
||||
export function getErrorsByCategory(category) {
|
||||
export function getErrorsByCategory(category: string): ErrorCodeDef[] {
|
||||
return Object.values(ERROR_CODES).filter((e) => e.category === category);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user