/** * db/settings.js — Settings, pricing, and proxy config. */ import { getDbInstance } from "./core"; import { backupDbFile } from "./backup"; import { PROVIDER_ID_TO_ALIAS } from "@omniroute/open-sse/config/providerModels.ts"; import { invalidateDbCache } from "./readCache"; import { getProxyRegistryGeneration, resolveProxyForScopeFromRegistry } from "./proxies"; import { getComboModelProvider as getComboEntryProvider } from "@/lib/combos/steps"; import { requestBodyLimitMbFromEnv } from "@/shared/constants/bodySize"; type JsonRecord = Record; type PricingModels = Record; type PricingByProvider = Record; export type PricingSource = "default" | "litellm" | "modelsDev" | "user"; export type PricingSourceMap = Record>; type ProxyValue = JsonRecord | string | null; type ProxyResolutionResult = { proxy: ProxyValue; level: string; levelId: string | null; source?: string; }; type ProxyResolutionCacheEntry = { generation: number; registryGeneration: number; result: ProxyResolutionResult; }; const PROXY_RESOLUTION_CACHE_MAX_ENTRIES = 100; let proxyConfigGeneration = 0; const proxyResolutionCache = new Map(); export function bumpProxyConfigGeneration() { proxyConfigGeneration++; proxyResolutionCache.clear(); } function cacheProxyResolution( connectionId: string, generation: number, registryGeneration: number, result: ProxyResolutionResult ) { if (generation !== proxyConfigGeneration) return; if (registryGeneration !== getProxyRegistryGeneration()) return; if (proxyResolutionCache.size >= PROXY_RESOLUTION_CACHE_MAX_ENTRIES) { const oldestKey = proxyResolutionCache.keys().next().value; if (oldestKey) proxyResolutionCache.delete(oldestKey); } proxyResolutionCache.set(connectionId, { generation, registryGeneration, result }); } type ProxyMap = Record; interface ProxyConfig { global: ProxyValue; providers: ProxyMap; combos: ProxyMap; keys: ProxyMap; [key: string]: unknown; } function toRecord(value: unknown): JsonRecord { return value && typeof value === "object" ? (value as JsonRecord) : {}; } function toProxyMap(value: unknown): ProxyMap { return value && typeof value === "object" ? (value as ProxyMap) : {}; } function toProxyValue(value: unknown): ProxyValue { if (value === null || typeof value === "string") return value as string | null; if (value && typeof value === "object") return value as JsonRecord; return null; } // ──────────────── Settings ──────────────── export async function getSettings() { const db = getDbInstance(); const rows = db.prepare("SELECT key, value FROM key_value WHERE namespace = 'settings'").all(); const settings: Record = { cloudEnabled: true, tailscaleEnabled: false, tailscaleUrl: "", stickyRoundRobinLimit: 3, requestRetry: 3, maxRetryIntervalSec: 30, antigravitySignatureCacheMode: "enabled", requireLogin: true, mcpEnabled: false, a2aEnabled: false, hiddenSidebarItems: [], sidebarSectionOrder: [], sidebarItemOrder: {}, sidebarActivePreset: null, hideEndpointCloudflaredTunnel: false, hideEndpointTailscaleFunnel: false, hideEndpointNgrokTunnel: false, autoRefreshProviderQuota: false, autoRefreshProviderQuotaInterval: 180, comboConfigMode: "guided", codexServiceTier: { enabled: false }, claudeFastMode: { enabled: false, supportedModels: ["claude-opus-4-8", "claude-opus-4-7", "claude-opus-4-6"], }, codexSessionAffinityTtlMs: 0, alwaysPreserveClientCache: "auto", idempotencyWindowMs: 5000, wsAuth: false, maxBodySizeMb: requestBodyLimitMbFromEnv(process.env.MAX_BODY_SIZE_BYTES), debugMode: true, // LOCAL_ONLY manage-scope bypass policy defaults (T-011 / spec §Data Model). // Preserves PR #2473 behaviour on migration — the bypass starts ENABLED // for `/api/mcp/` so existing manage-scope Bearer clients keep working. // Operators flip the kill-switch to false (or drop the prefix) via the // Settings UI; the change hot-reloads through `applyRuntimeSettings` → // `applyAuthzBypassSection` → `getAuthzBypassSnapshot()`. localOnlyManageScopeBypassEnabled: true, localOnlyManageScopeBypassPrefixes: ["/api/mcp/"], }; for (const row of rows) { const record = toRecord(row); const key = typeof record.key === "string" ? record.key : null; const rawValue = typeof record.value === "string" ? record.value : null; if (!key || rawValue === null) continue; settings[key] = JSON.parse(rawValue); } // Auto-complete onboarding for pre-configured deployments (Docker/VM) // If INITIAL_PASSWORD is set via env, this is a headless deploy — skip the wizard if (!settings.setupComplete && process.env.INITIAL_PASSWORD) { settings.setupComplete = true; settings.requireLogin = true; db.prepare( "INSERT OR REPLACE INTO key_value (namespace, key, value) VALUES ('settings', 'setupComplete', 'true')" ).run(); db.prepare( "INSERT OR REPLACE INTO key_value (namespace, key, value) VALUES ('settings', 'requireLogin', 'true')" ).run(); } return settings; } export async function updateSettings(updates: Record) { const db = getDbInstance(); const insert = db.prepare( "INSERT OR REPLACE INTO key_value (namespace, key, value) VALUES ('settings', ?, ?)" ); const tx = db.transaction(() => { for (const [key, value] of Object.entries(updates)) { insert.run(key, JSON.stringify(value)); } }); tx(); backupDbFile("pre-write"); invalidateDbCache("settings"); // Bust the read cache immediately const nextSettings = await getSettings(); try { const { applyRuntimeSettings } = await import("@/lib/config/runtimeSettings"); await applyRuntimeSettings(nextSettings, { source: "settings:update" }); } catch (error) { console.warn( "[HOT_RELOAD] Failed to apply runtime settings after update:", error instanceof Error ? error.message : error ); } return nextSettings; } export async function isCloudEnabled() { const settings = await getSettings(); return settings.cloudEnabled === true; } // ──────────────── Pricing ──────────────── function readPricingNamespace( db: ReturnType, namespace: string ): PricingByProvider { const rows = db.prepare("SELECT key, value FROM key_value WHERE namespace = ?").all(namespace); const pricing: PricingByProvider = {}; for (const row of rows) { const record = toRecord(row); const key = typeof record.key === "string" ? record.key : null; const rawValue = typeof record.value === "string" ? record.value : null; if (!key || rawValue === null) continue; try { pricing[key] = toRecord(JSON.parse(rawValue)) as PricingModels; } catch { // Corrupted data — skip silently, fallback to lower layers } } return pricing; } function mergePricingLayers(layers: PricingByProvider[]): PricingByProvider { const mergedPricing: PricingByProvider = {}; for (const layer of layers) { for (const [provider, models] of Object.entries(layer)) { if (!mergedPricing[provider]) { mergedPricing[provider] = { ...models }; continue; } for (const [model, pricing] of Object.entries(models)) { mergedPricing[provider][model] = mergedPricing[provider][model] ? { ...(mergedPricing[provider][model] || {}), ...toRecord(pricing) } : pricing; } } } return mergedPricing; } function buildPricingSourceMap(layers: { defaults: PricingByProvider; litellm: PricingByProvider; modelsDev: PricingByProvider; user: PricingByProvider; }): PricingSourceMap { const sourceMap: PricingSourceMap = {}; const mergedPricing = mergePricingLayers([ layers.defaults, layers.litellm, layers.modelsDev, layers.user, ]); for (const [provider, models] of Object.entries(mergedPricing)) { sourceMap[provider] = {}; for (const model of Object.keys(models)) { if (layers.user[provider]?.[model]) { sourceMap[provider][model] = "user"; } else if (layers.modelsDev[provider]?.[model]) { sourceMap[provider][model] = "modelsDev"; } else if (layers.litellm[provider]?.[model]) { sourceMap[provider][model] = "litellm"; } else { sourceMap[provider][model] = "default"; } } } return sourceMap; } async function getPricingLayers() { const db = getDbInstance(); // Layer 1: Hardcoded defaults (lowest priority) const { getDefaultPricing } = await import("@/shared/constants/pricing"); return { defaults: getDefaultPricing(), litellm: readPricingNamespace(db, "pricing_synced"), modelsDev: readPricingNamespace(db, "models_dev_pricing"), user: readPricingNamespace(db, "pricing"), }; } export async function getPricing() { const layers = await getPricingLayers(); // Merge: defaults → LiteLLM → models.dev → user (each layer overrides the previous) return mergePricingLayers([layers.defaults, layers.litellm, layers.modelsDev, layers.user]); } export async function getPricingWithSources(): Promise<{ pricing: PricingByProvider; sourceMap: PricingSourceMap; }> { const layers = await getPricingLayers(); return { pricing: mergePricingLayers([layers.defaults, layers.litellm, layers.modelsDev, layers.user]), sourceMap: buildPricingSourceMap(layers), }; } export async function getPricingForModel(provider: string, model: string) { const pricing = await getPricing(); const findKeyInsensitive = ( obj: Record | undefined | null, key: string ): T | undefined => { if (!obj || !key) return undefined; const lowerKey = key.toLowerCase(); for (const [k, v] of Object.entries(obj)) { if (k.toLowerCase() === lowerKey) return v; } return undefined; }; const pLower = (provider || "").toLowerCase(); let providerPricing = findKeyInsensitive(pricing, pLower); if (!providerPricing) { const alias = findKeyInsensitive(PROVIDER_ID_TO_ALIAS, pLower); if (alias) providerPricing = findKeyInsensitive(pricing, alias); } if (!providerPricing) { for (const [id, mappedAlias] of Object.entries(PROVIDER_ID_TO_ALIAS)) { if (typeof mappedAlias === "string" && mappedAlias.toLowerCase() === pLower) { providerPricing = findKeyInsensitive(pricing, id); if (providerPricing) break; } } } if (!providerPricing) { const np = pLower.replace(/-cn$/, ""); if (np && np !== pLower) { providerPricing = findKeyInsensitive(pricing, np); } } if (!providerPricing) return null; const mLower = (model || "").toLowerCase(); let modelPricing = findKeyInsensitive(providerPricing, mLower); if (!modelPricing) { const hyphenModel = mLower.replace(/\./g, "-"); modelPricing = findKeyInsensitive(providerPricing, hyphenModel); } return modelPricing || null; } export async function updatePricing(pricingData: PricingByProvider) { 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: PricingByProvider = {}; for (const row of rows) { const record = toRecord(row); const key = typeof record.key === "string" ? record.key : null; const rawValue = typeof record.value === "string" ? record.value : null; if (!key || rawValue === null) continue; existing[key] = toRecord(JSON.parse(rawValue)) as PricingModels; } const tx = db.transaction(() => { for (const [provider, models] of Object.entries(pricingData)) { insert.run(provider, JSON.stringify({ ...(existing[provider] || {}), ...models })); } }); tx(); backupDbFile("pre-write"); invalidateDbCache("pricing"); // Bust the pricing read cache const updated: PricingByProvider = {}; const allRows = db.prepare("SELECT key, value FROM key_value WHERE namespace = 'pricing'").all(); for (const row of allRows) { const record = toRecord(row); const key = typeof record.key === "string" ? record.key : null; const rawValue = typeof record.value === "string" ? record.value : null; if (!key || rawValue === null) continue; updated[key] = toRecord(JSON.parse(rawValue)) as PricingModels; } return updated; } export async function resetPricing(provider: string, model?: string) { const db = getDbInstance(); if (model) { const row = db .prepare("SELECT value FROM key_value WHERE namespace = 'pricing' AND key = ?") .get(provider); if (row) { const rowRecord = toRecord(row); const value = typeof rowRecord.value === "string" ? rowRecord.value : "{}"; const models = toRecord(JSON.parse(value)); delete models[model]; if (Object.keys(models).length === 0) { db.prepare("DELETE FROM key_value WHERE namespace = 'pricing' AND key = ?").run(provider); } else { db.prepare("UPDATE key_value SET value = ? WHERE namespace = 'pricing' AND key = ?").run( JSON.stringify(models), provider ); } } } else { db.prepare("DELETE FROM key_value WHERE namespace = 'pricing' AND key = ?").run(provider); } backupDbFile("pre-write"); const allRows = db.prepare("SELECT key, value FROM key_value WHERE namespace = 'pricing'").all(); const result: Record = {}; for (const row of allRows) { const record = toRecord(row); const key = typeof record.key === "string" ? record.key : null; const rawValue = typeof record.value === "string" ? record.value : null; if (!key || rawValue === null) continue; result[key] = JSON.parse(rawValue); } return result; } export async function resetAllPricing() { const db = getDbInstance(); db.prepare("DELETE FROM key_value WHERE namespace = 'pricing'").run(); backupDbFile("pre-write"); return {}; } // ──────────────── LKGP (Last Known Good Provider) ──────────────── export interface LKGPRecord { provider: string; connectionId?: string; } export async function getLKGP(comboName: string, modelId: string): Promise { const db = getDbInstance(); const key = `${comboName}:${modelId}`; const row = db .prepare("SELECT value FROM key_value WHERE namespace = 'lkgp' AND key = ?") .get(key) as { value?: string } | undefined; if (!row?.value) return null; try { const parsed = JSON.parse(row.value); if (typeof parsed === "object" && parsed !== null && "provider" in parsed) { return parsed as LKGPRecord; } return { provider: String(parsed) }; } catch { return { provider: row.value }; } } export async function setLKGP( comboName: string, modelId: string, providerId: string, connectionId?: string ) { const db = getDbInstance(); const key = `${comboName}:${modelId}`; const value: LKGPRecord = { provider: providerId }; if (connectionId) value.connectionId = connectionId; db.prepare("INSERT OR REPLACE INTO key_value (namespace, key, value) VALUES ('lkgp', ?, ?)").run( key, JSON.stringify(value) ); } export function clearAllLKGP(): void { const db = getDbInstance(); db.prepare("DELETE FROM key_value WHERE namespace = 'lkgp'").run(); } // ──────────────── Proxy Config ──────────────── const DEFAULT_PROXY_CONFIG: ProxyConfig = { global: null, providers: {}, combos: {}, keys: {} }; const ALIAS_TO_PROVIDER_ID = Object.entries(PROVIDER_ID_TO_ALIAS).reduce( (acc, [providerId, alias]) => { if (alias) acc[alias] = providerId; acc[providerId] = providerId; return acc; }, {} as Record ); function resolveProviderAliasOrId(providerOrAlias: string): string { if (typeof providerOrAlias !== "string") return providerOrAlias; return ALIAS_TO_PROVIDER_ID[providerOrAlias] || providerOrAlias; } function getComboModelProvider(modelEntry: unknown): string | null { const providerOrAlias = getComboEntryProvider(modelEntry); return providerOrAlias ? resolveProviderAliasOrId(providerOrAlias) : null; } function migrateProxyEntry(value: unknown): JsonRecord | null { if (!value) return null; if (typeof value === "object") { const record = toRecord(value); if (record.type) return record; } if (typeof value !== "string") return null; try { const url = new URL(value); return { type: url.protocol.replace(":", "") || "http", host: url.hostname, port: url.port || (url.protocol === "socks5:" ? "1080" : url.protocol === "https:" ? "443" : "8080"), username: url.username ? decodeURIComponent(url.username) : "", password: url.password ? decodeURIComponent(url.password) : "", }; } catch { const parts = value.split(":"); return { type: "http", host: parts[0] || value, port: parts[1] || "8080", username: "", password: "", }; } } export async function getProxyConfig() { const db = getDbInstance(); const rows = db.prepare("SELECT key, value FROM key_value WHERE namespace = 'proxyConfig'").all(); const raw: ProxyConfig = { ...DEFAULT_PROXY_CONFIG }; for (const row of rows) { const record = toRecord(row); const key = typeof record.key === "string" ? record.key : null; const rawValue = typeof record.value === "string" ? record.value : null; if (!key || rawValue === null) continue; raw[key] = JSON.parse(rawValue); } let migrated = false; if (raw.global && typeof raw.global === "string") { raw.global = migrateProxyEntry(raw.global); migrated = true; } if (raw.providers) { for (const [k, v] of Object.entries(raw.providers)) { if (typeof v === "string") { raw.providers[k] = migrateProxyEntry(v); migrated = true; } } } if (migrated) { const insert = db.prepare( "INSERT OR REPLACE INTO key_value (namespace, key, value) VALUES ('proxyConfig', ?, ?)" ); if (raw.global !== undefined) insert.run("global", JSON.stringify(raw.global)); if (raw.providers) insert.run("providers", JSON.stringify(raw.providers)); } return raw; } export async function getProxyForLevel(level: string, id?: string | null) { const config = await getProxyConfig(); if (level === "global") return config.global || null; const map = toProxyMap(config[level + "s"] || config[level] || {}); return (id ? map[id] : null) || null; } export async function setProxyForLevel(level: string, id: string | null, proxy: ProxyValue) { const db = getDbInstance(); const config = await getProxyConfig(); if (level === "global") { config.global = proxy || null; db.prepare( "INSERT OR REPLACE INTO key_value (namespace, key, value) VALUES ('proxyConfig', 'global', ?)" ).run(JSON.stringify(config.global)); } else { const mapKey = level + "s"; const map = toProxyMap(config[mapKey] || {}); if (proxy && id) { map[id] = proxy; } else { if (id) delete map[id]; } config[mapKey] = map; db.prepare( "INSERT OR REPLACE INTO key_value (namespace, key, value) VALUES ('proxyConfig', ?, ?)" ).run(mapKey, JSON.stringify(map)); } backupDbFile("pre-write"); bumpProxyConfigGeneration(); return config; } export async function deleteProxyForLevel(level: string, id: string | null) { return setProxyForLevel(level, id, null); } export async function resolveProxyForConnection(connectionId: string) { const startGeneration = proxyConfigGeneration; const startRegistryGeneration = getProxyRegistryGeneration(); const cached = proxyResolutionCache.get(connectionId); if ( cached && cached.generation === startGeneration && cached.registryGeneration === startRegistryGeneration ) { return cached.result; } const config = await getProxyConfig(); // Resolve by specificity across both proxy storage backends. The dashboard // Custom tab still writes account/provider proxies to the legacy config, // while Saved Proxy writes registry assignments. Do not let a registry-global // fallback shadow a more-specific legacy account/provider proxy (#2601). const registryAccount = await resolveProxyForScopeFromRegistry("account", connectionId); if (registryAccount?.proxy) { cacheProxyResolution(connectionId, startGeneration, startRegistryGeneration, registryAccount); return registryAccount; } if (connectionId && config.keys?.[connectionId]) { const result = { proxy: config.keys[connectionId], level: "key", levelId: connectionId }; cacheProxyResolution(connectionId, startGeneration, startRegistryGeneration, result); return result; } const db = getDbInstance(); const connection = db .prepare("SELECT provider FROM provider_connections WHERE id = ?") .get(connectionId); if (connection) { const connectionRecord = toRecord(connection); const provider = typeof connectionRecord.provider === "string" ? connectionRecord.provider : null; if (provider) { const registryProvider = await resolveProxyForScopeFromRegistry("provider", provider); if (registryProvider?.proxy) return registryProvider; } if (config.combos && Object.keys(config.combos).length > 0) { const combos = db.prepare("SELECT id, data FROM combos").all(); for (const comboRow of combos) { const comboRecord = toRecord(comboRow); const comboId = typeof comboRecord.id === "string" ? comboRecord.id : null; if (comboId && config.combos[comboId]) { try { const comboRaw = typeof comboRecord.data === "string" ? comboRecord.data : null; if (!comboRaw) continue; const combo = toRecord(JSON.parse(comboRaw)); const comboModels = Array.isArray(combo.models) ? combo.models : []; const usesProvider = comboModels.some( (entry) => getComboModelProvider(entry) === provider ); if (usesProvider) { return { proxy: config.combos[comboId], level: "combo", levelId: comboId }; } } catch { // Ignore malformed combo records during proxy resolution. } } } } if (provider && config.providers?.[provider]) { return { proxy: config.providers[provider], level: "provider", levelId: provider, }; } } const registryGlobal = await resolveProxyForScopeFromRegistry("global"); if (registryGlobal?.proxy) return registryGlobal; if (config.global) { return { proxy: config.global, level: "global", levelId: null }; } return { proxy: null, level: "direct", levelId: null }; } export async function setProxyConfig(config: Record) { if (config.level !== undefined) { const level = typeof config.level === "string" ? config.level : "global"; const id = typeof config.id === "string" ? config.id : null; const proxy = (config.proxy as ProxyValue) || null; return setProxyForLevel(level, id, proxy); } const db = getDbInstance(); const current = await getProxyConfig(); const insert = db.prepare( "INSERT OR REPLACE INTO key_value (namespace, key, value) VALUES ('proxyConfig', ?, ?)" ); const tx = db.transaction(() => { if (config.global !== undefined) { current.global = toProxyValue(config.global); insert.run("global", JSON.stringify(current.global)); } for (const mapKey of ["providers", "combos", "keys"]) { if (config[mapKey]) { const merged = { ...toProxyMap(current[mapKey]), ...toProxyMap(config[mapKey]) }; for (const [k, v] of Object.entries(merged)) { if (!v) delete merged[k]; } current[mapKey] = merged; insert.run(mapKey, JSON.stringify(merged)); } } }); tx(); backupDbFile("pre-write"); bumpProxyConfigGeneration(); return current; } // ──────────────── Cache Control Metrics ──────────────── // Cache metrics are now computed from usage_history table on-the-fly // This avoids race conditions and keeps a single source of truth for token data export async function getCacheMetrics() { const db = getDbInstance(); try { // Aggregate totals from usage_history const totalsRow = db .prepare( ` SELECT COUNT(*) as totalRequests, SUM(tokens_input) as totalInputTokens, SUM(tokens_cache_read) as totalCachedTokens, SUM(tokens_cache_creation) as totalCacheCreationTokens FROM usage_history WHERE tokens_cache_read > 0 OR tokens_cache_creation > 0 ` ) .get() as | { totalRequests: number; totalInputTokens: number | null; totalCachedTokens: number | null; totalCacheCreationTokens: number | null; } | undefined; // Get all requests count (including those without cache activity) const allRequestsRow = db .prepare( ` SELECT COUNT(*) as totalRequests FROM usage_history ` ) .get() as { totalRequests: number } | undefined; // Aggregate by provider const byProviderRows = db .prepare( ` SELECT provider, COUNT(*) as totalRequests, SUM(CASE WHEN tokens_cache_read > 0 OR tokens_cache_creation > 0 THEN 1 ELSE 0 END) as cachedRequests, SUM(CASE WHEN tokens_cache_read > 0 OR tokens_cache_creation > 0 THEN tokens_input ELSE 0 END) as inputTokens, SUM(tokens_cache_read) as cachedTokens, SUM(tokens_cache_creation) as cacheCreationTokens FROM usage_history WHERE provider IS NOT NULL GROUP BY provider HAVING cachedRequests > 0 ` ) .all() as Array<{ provider: string; totalRequests: number; cachedRequests: number; inputTokens: number | null; cachedTokens: number | null; cacheCreationTokens: number | null; }>; // Aggregate by combo strategy (direct requests stored as 'direct') const byStrategyRows = db .prepare( ` SELECT COALESCE(combo_strategy, 'direct') as strategy, COUNT(*) as requests, SUM(tokens_input) as inputTokens, SUM(tokens_cache_read) as cachedTokens, SUM(tokens_cache_creation) as cacheCreationTokens FROM usage_history WHERE (tokens_cache_read > 0 OR tokens_cache_creation > 0) GROUP BY combo_strategy ` ) .all() as Array<{ strategy: string; requests: number; inputTokens: number | null; cachedTokens: number | null; cacheCreationTokens: number | null; }>; const tokensSaved = totalsRow?.totalCachedTokens || 0; const AVG_INPUT_PRICE_PER_MILLION = 3; const CACHE_DISCOUNT = 0.9; const estimatedCostSaved = Math.round((tokensSaved / 1_000_000) * AVG_INPUT_PRICE_PER_MILLION * CACHE_DISCOUNT * 100) / 100; // Build byProvider object const byProvider: Record< string, { requests: number; totalRequests: number; cachedRequests: number; inputTokens: number; cachedTokens: number; cacheCreationTokens: number; } > = {}; for (const row of byProviderRows) { byProvider[row.provider] = { requests: row.cachedRequests, totalRequests: row.totalRequests, cachedRequests: row.cachedRequests, inputTokens: row.inputTokens || 0, cachedTokens: row.cachedTokens || 0, cacheCreationTokens: row.cacheCreationTokens || 0, }; } // Build byStrategy object const byStrategy: Record< string, { requests: number; inputTokens: number; cachedTokens: number; cacheCreationTokens: number; } > = {}; for (const row of byStrategyRows) { byStrategy[row.strategy] = { requests: row.requests, inputTokens: row.inputTokens || 0, cachedTokens: row.cachedTokens || 0, cacheCreationTokens: row.cacheCreationTokens || 0, }; } return { totalRequests: allRequestsRow?.totalRequests || totalsRow?.totalRequests || 0, requestsWithCacheControl: totalsRow?.totalRequests || 0, totalInputTokens: totalsRow?.totalInputTokens || 0, totalCachedTokens: totalsRow?.totalCachedTokens || 0, totalCacheCreationTokens: totalsRow?.totalCacheCreationTokens || 0, tokensSaved, estimatedCostSaved, byProvider, byStrategy, lastUpdated: new Date().toISOString(), }; } catch (error) { console.error("Failed to fetch cache metrics from usage_history:", error); return { totalRequests: 0, requestsWithCacheControl: 0, totalInputTokens: 0, totalCachedTokens: 0, totalCacheCreationTokens: 0, tokensSaved: 0, estimatedCostSaved: 0, byProvider: {}, byStrategy: {}, lastUpdated: new Date().toISOString(), }; } } export async function updateCacheMetrics(_metrics: Record) { // No-op: metrics are now computed from usage_history on-the-fly // The usage_history table is the single source of truth return getCacheMetrics(); } export interface CacheTrendPoint { timestamp: string; requests: number; cachedRequests: number; inputTokens: number; cachedTokens: number; cacheCreationTokens: number; } export async function getCacheTrend(hours = 24): Promise { const db = getDbInstance(); try { const rows = db .prepare( ` SELECT strftime('%Y-%m-%dT%H:00:00Z', timestamp) as hour, COUNT(*) as requests, SUM(CASE WHEN tokens_cache_read > 0 OR tokens_cache_creation > 0 THEN 1 ELSE 0 END) as cachedRequests, SUM(tokens_input) as inputTokens, SUM(tokens_cache_read) as cachedTokens, SUM(tokens_cache_creation) as cacheCreationTokens FROM usage_history WHERE timestamp >= datetime('now', ?) GROUP BY hour ORDER BY hour ASC ` ) .all(`-${hours} hours`) as Array<{ hour: string; requests: number; cachedRequests: number; inputTokens: number | null; cachedTokens: number | null; cacheCreationTokens: number | null; }>; return rows.map((r) => ({ timestamp: r.hour, requests: r.requests, cachedRequests: r.cachedRequests, inputTokens: r.inputTokens || 0, cachedTokens: r.cachedTokens || 0, cacheCreationTokens: r.cacheCreationTokens || 0, })); } catch (error) { console.error("Failed to fetch cache trend:", error); return []; } } export async function resetCacheMetrics() { // No-op: cannot delete historical usage data // Cache metrics are computed from usage_history, so they reflect actual request history console.warn( "resetCacheMetrics is deprecated - cache metrics are now computed from usage_history" ); return getCacheMetrics(); }