/** * Usage Migrations — extracted from usageDb.js (T-15) * * Handles legacy file migration (.data → data/) and JSON → SQLite migration. * Runs automatically on module load when shouldPersistToDisk is true. * * @module lib/usage/migrations */ import path from "path"; import fs from "fs"; import { getDbInstance, isCloud, isBuildPhase, DATA_DIR } from "../db/core"; import { getLegacyDotDataDir, isSamePath } from "../dataPaths"; export const shouldPersistToDisk = !isCloud && !isBuildPhase; // ──────────────── File Paths ──────────────── const LEGACY_DATA_DIR = isCloud ? null : getLegacyDotDataDir(); export const LOG_FILE = isCloud ? null : path.join(DATA_DIR, "log.txt"); export const CALL_LOGS_DIR = isCloud ? null : path.join(DATA_DIR, "call_logs"); // Legacy paths const LEGACY_DB_FILE = isCloud || !LEGACY_DATA_DIR ? null : path.join(LEGACY_DATA_DIR, "usage.json"); const LEGACY_LOG_FILE = isCloud || !LEGACY_DATA_DIR ? null : path.join(LEGACY_DATA_DIR, "log.txt"); const LEGACY_CALL_LOGS_DB_FILE = isCloud || !LEGACY_DATA_DIR ? null : path.join(LEGACY_DATA_DIR, "call_logs.json"); const LEGACY_CALL_LOGS_DIR = isCloud || !LEGACY_DATA_DIR ? null : path.join(LEGACY_DATA_DIR, "call_logs"); // Current-location JSON files (for migration into SQLite) const USAGE_JSON_FILE = isCloud ? null : path.join(DATA_DIR, "usage.json"); const CALL_LOGS_JSON_FILE = isCloud ? null : path.join(DATA_DIR, "call_logs.json"); // ──────────────── Legacy File Migration ──────────────── function copyIfMissing(fromPath, toPath, label) { if (!fromPath || !toPath) return; if (!fs.existsSync(fromPath) || fs.existsSync(toPath)) return; if (fs.statSync(fromPath).isDirectory()) { fs.cpSync(fromPath, toPath, { recursive: true }); } else { fs.copyFileSync(fromPath, toPath); } console.log(`[usageDb] Migrated ${label}: ${fromPath} -> ${toPath}`); } export function migrateLegacyUsageFiles() { if (!shouldPersistToDisk || !LEGACY_DATA_DIR) return; if (isSamePath(DATA_DIR, LEGACY_DATA_DIR)) return; try { copyIfMissing(LEGACY_DB_FILE, USAGE_JSON_FILE, "usage history"); copyIfMissing(LEGACY_LOG_FILE, LOG_FILE, "request log"); copyIfMissing(LEGACY_CALL_LOGS_DB_FILE, CALL_LOGS_JSON_FILE, "call log index"); copyIfMissing(LEGACY_CALL_LOGS_DIR, CALL_LOGS_DIR, "call log files"); } catch (error) { console.error("[usageDb] Legacy migration failed:", error.message); } } // ──────────────── JSON → SQLite Migration ──────────────── export function migrateUsageJsonToSqlite() { if (!shouldPersistToDisk) return; const db = getDbInstance(); // 1. Migrate usage.json if (USAGE_JSON_FILE && fs.existsSync(USAGE_JSON_FILE)) { try { const raw = fs.readFileSync(USAGE_JSON_FILE, "utf-8"); const data = JSON.parse(raw); const history = data.history || []; if (history.length > 0) { console.log(`[usageDb] Migrating ${history.length} usage entries from JSON → SQLite...`); const insert = 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 (@provider, @model, @connectionId, @apiKeyId, @apiKeyName, @tokensInput, @tokensOutput, @tokensCacheRead, @tokensCacheCreation, @tokensReasoning, @status, @timestamp) `); const tx = db.transaction(() => { for (const entry of history) { insert.run({ provider: entry.provider || null, model: entry.model || null, connectionId: entry.connectionId || null, apiKeyId: entry.apiKeyId || null, apiKeyName: entry.apiKeyName || null, tokensInput: entry.tokens?.input ?? entry.tokens?.prompt_tokens ?? 0, tokensOutput: entry.tokens?.output ?? entry.tokens?.completion_tokens ?? 0, tokensCacheRead: entry.tokens?.cacheRead ?? entry.tokens?.cached_tokens ?? 0, tokensCacheCreation: entry.tokens?.cacheCreation ?? entry.tokens?.cache_creation_input_tokens ?? 0, tokensReasoning: entry.tokens?.reasoning ?? entry.tokens?.reasoning_tokens ?? 0, status: entry.status || null, timestamp: entry.timestamp || new Date().toISOString(), }); } }); tx(); console.log(`[usageDb] ✓ Migrated ${history.length} usage entries`); } fs.renameSync(USAGE_JSON_FILE, USAGE_JSON_FILE + ".migrated"); } catch (err) { console.error("[usageDb] Failed to migrate usage.json:", err.message); } } // 2. Migrate call_logs.json if (CALL_LOGS_JSON_FILE && fs.existsSync(CALL_LOGS_JSON_FILE)) { try { const raw = fs.readFileSync(CALL_LOGS_JSON_FILE, "utf-8"); const data = JSON.parse(raw); const logs = data.logs || []; if (logs.length > 0) { console.log(`[usageDb] Migrating ${logs.length} call log entries from JSON → SQLite...`); const insert = db.prepare(` INSERT OR IGNORE INTO call_logs (id, timestamp, method, path, status, model, provider, account, connection_id, duration, tokens_in, tokens_out, source_format, target_format, api_key_id, api_key_name, combo_name, request_body, response_body, error) VALUES (@id, @timestamp, @method, @path, @status, @model, @provider, @account, @connectionId, @duration, @tokensIn, @tokensOut, @sourceFormat, @targetFormat, @apiKeyId, @apiKeyName, @comboName, @requestBody, @responseBody, @error) `); const tx = db.transaction(() => { for (const log of logs) { insert.run({ id: log.id || `${Date.now()}-${Math.random().toString(36).slice(2, 6)}`, timestamp: log.timestamp || new Date().toISOString(), method: log.method || "POST", path: log.path || null, status: log.status || 0, model: log.model || null, provider: log.provider || null, account: log.account || null, connectionId: log.connectionId || null, duration: log.duration || 0, tokensIn: log.tokens?.in ?? 0, tokensOut: log.tokens?.out ?? 0, sourceFormat: log.sourceFormat || null, targetFormat: log.targetFormat || null, apiKeyId: log.apiKeyId || null, apiKeyName: log.apiKeyName || null, comboName: log.comboName || null, requestBody: log.requestBody ? JSON.stringify(log.requestBody) : null, responseBody: log.responseBody ? JSON.stringify(log.responseBody) : null, error: log.error || null, }); } }); tx(); console.log(`[usageDb] ✓ Migrated ${logs.length} call log entries`); } fs.renameSync(CALL_LOGS_JSON_FILE, CALL_LOGS_JSON_FILE + ".migrated"); } catch (err) { console.error("[usageDb] Failed to migrate call_logs.json:", err.message); } } } // ──────────────── Run on load ──────────────── migrateLegacyUsageFiles(); if (shouldPersistToDisk) { try { migrateUsageJsonToSqlite(); } catch { /* ok */ } }