| import crypto from 'crypto'; |
| import fs from 'fs'; |
| import path from 'path'; |
| import { fileURLToPath } from 'url'; |
| import { createClient, type Client as LibSQLClient } from '@libsql/client'; |
| import { initEncryptionKey } from '../lib/crypto.js'; |
| import { applyModelPricing } from './model-pricing.js'; |
|
|
| const __dirname = path.dirname(fileURLToPath(import.meta.url)); |
| const DB_PATH = path.resolve(__dirname, '../../data/freeapi.db'); |
|
|
| |
| export interface DbDriver { |
| exec(sql: string): void | Promise<void>; |
| prepare(sql: string): StmtDriver; |
| transaction<T>(fn: (data?: T) => void): (data?: T) => void; |
| } |
|
|
| export interface StmtDriver { |
| run(...params: unknown[]): RunResult; |
| get(...params: unknown[]): Row | undefined; |
| all(...params: unknown[]): Row[]; |
| } |
|
|
| export interface Row { [key: string]: unknown; [index: number]: unknown } |
|
|
| export interface RunResult { |
| changes: number; |
| lastInsertRowid: number | bigint; |
| } |
|
|
| |
| |
| |
| |
|
|
| export async function initDb(dbPath?: string): Promise<DbDriver> { |
| const DATABASE_URL = process.env.DATABASE_URL; |
| const resolvedPath = dbPath ?? DB_PATH; |
| const isMemory = resolvedPath === ':memory:'; |
|
|
| const db = await createDbDriver(resolvedPath, isMemory, DATABASE_URL); |
|
|
| await createTablesAsync(db); |
| await ensureUnifiedKeyAsync(db); |
|
|
| console.log(`Database initialized at ${resolvedPath}`); |
| return db; |
| } |
|
|
| async function createDbDriver(dbPath: string, isMemory: boolean, tursoUrl?: string): Promise<DbDriver> { |
| const localUrl = isMemory ? 'file::memory:' : `file:${dbPath}`; |
|
|
| |
| const localClient = createClient({ url: localUrl }); |
|
|
| |
| let remoteClient: LibSQLClient | null = null; |
| if (tursoUrl) { |
| remoteClient = createClient({ url: tursoUrl, authToken: process.env.DATABASE_AUTH_TOKEN ?? '' }); |
| console.log(`Turso sync enabled: ${tursoUrl}`); |
| } |
|
|
| let pendingSql: string | null = null; |
| let pendingParams: unknown[] = []; |
| let flushTimer: ReturnType<typeof setTimeout> | null = null; |
|
|
| const scheduleFlush = () => { |
| if (!remoteClient || flushTimer !== null) return; |
| flushTimer = setTimeout(async () => { |
| flushTimer = null; |
| if (pendingSql && remoteClient) { |
| try { |
| await remoteClient.execute({ sql: pendingSql, args: pendingParams as any[] }); |
| } catch (err) { |
| console.error('[db/turso] sync write failed:', err); |
| } |
| pendingSql = null; |
| pendingParams = []; |
| } |
| }, 50); |
| }; |
|
|
| return { |
| exec(sql: string) { |
| |
| localClient.execute({ sql, args: [] as any[] }).catch(err => console.error('[db] exec failed:', err)); |
| }, |
| transaction<T>(fn: (data?: T) => void): (data?: T) => void { |
| return (data?: T) => { |
| fn(data); |
| if (remoteClient) { |
| remoteClient.execute({ sql: 'BEGIN', args: [] as any[] }).catch(() => {}); |
| } |
| }; |
| }, |
| prepare(sql: string) { |
| return { |
| run(...params: unknown[]) { |
| localClient.execute({ sql, args: params as any[] }).catch(err => console.error('[db] write failed:', err)); |
| if (remoteClient) { |
| pendingSql = sql; |
| pendingParams = params; |
| scheduleFlush(); |
| } |
| return { changes: 0, lastInsertRowid: BigInt(0) }; |
| }, |
| get(..._params: unknown[]) { |
| |
| return undefined as Row | undefined; |
| }, |
| all(..._params: unknown[]) { |
| |
| return [] as Row[]; |
| }, |
| }; |
| }, |
| }; |
| } |
|
|
| |
| let _db: DbDriver; |
|
|
| export function getDb(): DbDriver { |
| if (!_db) throw new Error('Database not initialized. Call initDb() first.'); |
| return _db; |
| } |
|
|
| |
| |
| export function setDb(d: DbDriver) { _db = d; } |
|
|
| |
| async function createTablesAsync(db: DbDriver) { |
| await db.exec(`CREATE TABLE IF NOT EXISTS models ( |
| id INTEGER PRIMARY KEY AUTOINCREMENT, platform TEXT NOT NULL, model_id TEXT NOT NULL, |
| display_name TEXT NOT NULL, intelligence_rank INTEGER NOT NULL, speed_rank INTEGER NOT NULL, |
| size_label TEXT NOT NULL DEFAULT '', rpm_limit INTEGER, rpd_limit INTEGER, tpm_limit INTEGER, |
| tpd_limit INTEGER, monthly_token_budget TEXT NOT NULL DEFAULT '', context_window INTEGER, |
| enabled INTEGER NOT NULL DEFAULT 1, supports_vision INTEGER NOT NULL DEFAULT 0, |
| UNIQUE(platform, model_id) |
| )`); |
| await db.exec(`CREATE TABLE IF NOT EXISTS api_keys ( |
| id INTEGER PRIMARY KEY AUTOINCREMENT, platform TEXT NOT NULL, label TEXT NOT NULL DEFAULT '', |
| encrypted_key TEXT NOT NULL, iv TEXT NOT NULL, auth_tag TEXT NOT NULL, |
| status TEXT NOT NULL DEFAULT 'unknown', enabled INTEGER NOT NULL DEFAULT 1, |
| created_at TEXT NOT NULL DEFAULT (datetime('now')), last_checked_at TEXT |
| )`); |
| await db.exec(`CREATE TABLE IF NOT EXISTS requests ( |
| id INTEGER PRIMARY KEY AUTOINCREMENT, platform TEXT NOT NULL, model_id TEXT NOT NULL, |
| key_id INTEGER, status TEXT NOT NULL, input_tokens INTEGER NOT NULL DEFAULT 0, |
| output_tokens INTEGER NOT NULL DEFAULT 0, latency_ms INTEGER NOT NULL DEFAULT 0, error TEXT, |
| created_at TEXT NOT NULL DEFAULT (datetime('now')) |
| )`); |
| await db.exec(`CREATE TABLE IF NOT EXISTS rate_limit_usage ( |
| id INTEGER PRIMARY KEY AUTOINCREMENT, platform TEXT NOT NULL, model_id TEXT NOT NULL, |
| key_id INTEGER NOT NULL, kind TEXT NOT NULL CHECK (kind IN ('request', 'tokens')), |
| tokens INTEGER NOT NULL DEFAULT 0, created_at_ms INTEGER NOT NULL, |
| created_at TEXT NOT NULL DEFAULT (datetime('now')) |
| )`); |
| await db.exec(`CREATE TABLE IF NOT EXISTS rate_limit_cooldowns ( |
| platform TEXT NOT NULL, model_id TEXT NOT NULL, key_id INTEGER NOT NULL, |
| expires_at_ms INTEGER NOT NULL, created_at TEXT NOT NULL DEFAULT (datetime('now')), |
| PRIMARY KEY (platform, model_id, key_id) |
| )`); |
| await db.exec(`CREATE TABLE IF NOT EXISTS fallback_config ( |
| id INTEGER PRIMARY KEY AUTOINCREMENT, model_db_id INTEGER NOT NULL REFERENCES models(id), |
| priority INTEGER NOT NULL, enabled INTEGER NOT NULL DEFAULT 1, UNIQUE(model_db_id) |
| )`); |
| await db.exec(`CREATE TABLE IF NOT EXISTS settings (key TEXT PRIMARY KEY, value TEXT NOT NULL)`); |
| await db.exec(`CREATE TABLE IF NOT EXISTS users ( |
| id INTEGER PRIMARY KEY AUTOINCREMENT, email TEXT NOT NULL UNIQUE, |
| password_hash TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT (datetime('now')) |
| )`); |
| await db.exec(`CREATE TABLE IF NOT EXISTS sessions ( |
| token_hash TEXT PRIMARY KEY, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, |
| expires_at_ms INTEGER NOT NULL, created_at TEXT NOT NULL DEFAULT (datetime('now')) |
| )`); |
| await db.exec(`CREATE INDEX IF NOT EXISTS idx_sessions_user ON sessions(user_id)`); |
| await db.exec(`CREATE INDEX IF NOT EXISTS idx_requests_created_at ON requests(created_at)`); |
| await db.exec(`CREATE INDEX IF NOT EXISTS idx_requests_platform ON requests(platform)`); |
| await db.exec(`CREATE INDEX IF NOT EXISTS idx_api_keys_platform ON api_keys(platform)`); |
| } |
|
|
| async function ensureUnifiedKeyAsync(db: DbDriver) { |
| const key = 'freellmapi-' + crypto.randomBytes(24).toString('base64url'); |
| await db.exec(`INSERT INTO settings (key, value) VALUES ('unified_api_key', '${key}')`); |
| } |
|
|
| |
|
|
| function seedModels(_db: DbDriver) { |
| |
| } |
|
|
| function migrateModels(_db: DbDriver) {} |
| function migrateModelsV2(_db: DbDriver) {} |
| function migrateModelsV3Ranks(_db: DbDriver) {} |
| function migrateModelsV4(_db: DbDriver) {} |
| function migrateModelsV5(_db: DbDriver) {} |
| function migrateModelsV6(_db: DbDriver) {} |
| function migrateModelsV7(_db: DbDriver) {} |
| function migrateModelsV8(_db: DbDriver) {} |
| function migrateModelsV9(_db: DbDriver) {} |
| function migrateModelsV10(_db: DbDriver) {} |
| function migrateModelsV11(_db: DbDriver) {} |
| function migrateModelsV12(_db: DbDriver) {} |
| function migrateModelsV13(_db: DbDriver) {} |
| function migrateModelsV14(_db: DbDriver) {} |
| function migrateModelsV15(_db: DbDriver) {} |
| function migrateModelsV16Vision(_db: DbDriver) {} |
| function migrateModelsV17IntelligenceTiers(_db: DbDriver) {} |
| function migrateModelsV18OpenCodeZen(_db: DbDriver) {} |
| function migrateModelsV19Gemma4(_db: DbDriver) {} |
| function migrateModelsV20KiloFree(_db: DbDriver) {} |
| function migrateModelsV21PruneDead(_db: DbDriver) {} |
| function migrateModelsV22Tools(_db: DbDriver) {} |
| function migrateEmbeddingsV1(_db: DbDriver) {} |
|
|
| export function getUnifiedApiKey(): string { return ''; } |
| export function regenerateUnifiedKey(): string { return ''; } |
| export function getSetting(_key: string): string | null { return null; } |
| export function setSetting(_key: string, _value: string): void {} |
|
|