/** * 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 { encrypt, decrypt } from "./encryption"; import { getProxyRegistryGeneration, resolveProxyForScopeFromRegistry } from "./proxies"; import { getComboModelProvider as getComboEntryProvider } from "@/lib/combos/steps"; import { requestBodyLimitMbFromEnv } from "@/shared/constants/bodySize"; import { DEFAULT_RESPONSES_PREVIOUS_RESPONSE_ID_MODE } from "@/shared/constants/responsesPreviousResponseId"; import { type JsonRecord, toRecord } from "./settings/shared"; import { resolveNoAuthSharedProviderProxy } from "./settings/noAuthProxyFallback"; 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; function isTruthyEnvFlag(value: string | undefined): boolean { return typeof value === "string" && /^(1|true|yes|on)$/i.test(value.trim()); } 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 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; } // Legacy proxyConfig store (key_value namespace 'proxyConfig') predates the // IPv6-only `family` directive, so its object configs have no family field. // Default to "auto" so the family marker rides along the cascade end-to-end // (consumed by proxyConfigToUrl). String configs are returned unchanged. function withFamilyDefault(value: ProxyValue): ProxyValue { if (value && typeof value === "object" && !Array.isArray(value)) { const record = value as JsonRecord; if (typeof record.family === "string") return record; return { ...record, family: "auto" }; } return value; } // ──────────────── Settings ──────────────── /** Internal key_value row — not exposed as a user-facing settings field. */ export const SETTINGS_REVISION_KEY = "_settingsRevision"; export class SettingsRevisionConflictError extends Error { readonly code = "SETTINGS_REVISION_CONFLICT" as const; constructor(public readonly currentRevision: number) { super("Settings revision mismatch"); this.name = "SettingsRevisionConflictError"; } } function readSettingsRevision(db: ReturnType): number { const row = db .prepare("SELECT value FROM key_value WHERE namespace = 'settings' AND key = ?") .get(SETTINGS_REVISION_KEY) as { value?: string } | undefined; if (!row?.value) return 0; try { const parsed = JSON.parse(row.value) as unknown; return typeof parsed === "number" && Number.isInteger(parsed) && parsed >= 0 ? parsed : 0; } catch { return 0; } } export async function getSettingsRevision(): Promise { return readSettingsRevision(getDbInstance()); } /** * #7274: read-fallback for the codexSessionAffinityTtlMs -> sessionAffinityTtlMs * rename. Migration 124 already backfills the new key from any pre-existing * old-key row for the common case, but this covers callers reading settings * before that migration has had a chance to run (or any drift between the * two). Only applies when the generic key was never explicitly persisted — * an operator-set `sessionAffinityTtlMs` (including an explicit 0) always wins. * Extracted to a leaf helper so `getSettings()` stays under the max-lines-per- * function ratchet. */ function applySessionAffinityLegacyFallback(settings: Record): void { if (settings.sessionAffinityTtlMs === undefined) { settings.sessionAffinityTtlMs = typeof settings.codexSessionAffinityTtlMs === "number" ? settings.codexSessionAffinityTtlMs : 0; } } 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, disableSessionStickiness: false, promptCacheAffinityEnabled: true, comboStrategy: "fallback", comboStickyRoundRobinLimit: null, // null = inherit stickyRoundRobinLimit (a literal default here shadows the documented batched-rotation default of 3 — #6678 regression caught by the v3.8.47 release CI) providerStrategies: {}, // Per-operator quota row visibility (dashboard usage tab). Keyed by // provider id → { hidden: [] }. Independent of the // model catalog's isHidden/isDeleted flags (collectHiddenQuotaModelIds in // ProviderLimits/utils.tsx) — this is a personal view preference, not an // admin model-catalog edit. Ported from upstream decolua/9router#2371. quotaVisibility: {}, requestRetry: 3, maxRetryIntervalSec: 30, antigravitySignatureCacheMode: "enabled", requireLogin: true, oidcEnabled: false, oidcIssuer: "", oidcClientId: "", oidcClientSecret: "", oidcScopes: ["openid", "profile", "email"], oidcRedirectPath: "/api/auth/oidc/callback", oidcAllowedSubjects: [], // optional sub or email whitelist mcpEnabled: false, a2aEnabled: false, hiddenSidebarItems: [], hiddenSidebarGroupLabels: [], sidebarSectionOrder: [], sidebarItemOrder: {}, sidebarActivePreset: null, hideEndpointCloudflaredTunnel: false, hideEndpointTailscaleFunnel: false, hideEndpointNgrokTunnel: false, preferClaudeCodeForUnprefixedClaudeModels: isTruthyEnvFlag( process.env.OMNIROUTE_PREFER_CLAUDE_CODE_FOR_UNPREFIXED_CLAUDE_MODELS ), // Opt-in (default "off"): short-circuits Claude Code's `--permission-mode auto` // internal security-classifier request with a synthetic `no` ALLOW // response, without calling the upstream provider. See // open-sse/handlers/chatCore/claudeClassifierCompat.ts for the detector + builder. claudeClassifierCompat: "off", autoRefreshProviderQuota: false, credentialRedactionEnabled: false, autoRefreshProviderQuotaInterval: 180, comboConfigMode: "guided", comboAutoPromoteEnabled: false, codexServiceTier: { enabled: false }, claudeFastMode: { enabled: false, supportedModels: [ "claude-fable-5", "claude-opus-5", "claude-opus-4-8", "claude-opus-4-7", "claude-opus-4-6", ], }, // #7274: renamed from codexSessionAffinityTtlMs — session affinity now // applies to any provider, not just Codex. No default here on purpose: // the read-fallback below only kicks in while `sessionAffinityTtlMs` has // never been explicitly persisted, so it can tell "never configured" // apart from "operator explicitly set 0" on the new key. responsesPreviousResponseIdMode: DEFAULT_RESPONSES_PREVIOUS_RESPONSE_ID_MODE, alwaysPreserveClientCache: "auto", idempotencyWindowMs: 5000, wsAuth: false, maxBodySizeMb: requestBodyLimitMbFromEnv(process.env.MAX_BODY_SIZE_BYTES), debugMode: true, // Opt-in diagnostic: when true, the chat handler emits a `log.debug("TOOLS", …)` // line per request summarizing tool count + MCP/hosted/client source breakdown. logToolSources: false, // 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/"], customBannedSignals: [], proxyEnabled: true, perKeyProxyEnabled: false, customSystemPromptEnabled: false, customSystemPrompt: "", // #6316: Opt-in filter that hides paid-only models from the /v1/models catalog. // Uses isFreeModel() from src/shared/utils/freeModels.ts to detect free entries // (`:free` suffix, zero-price pricing, or FREE_MODEL_BUDGETS membership). Default // false preserves prior behaviour; opt-in only. hidePaidModels: false, // #9418: Opt-in filter that hides auto/* virtual combos from the /v1/models catalog. // User-defined combos are unaffected; routing still works for hidden ids sent explicitly. hideAutoCombos: false, // #9418: Opt-in filter that hides no-think/* gateway variants from the /v1/models catalog. // Routing still works for hidden ids sent explicitly. hideNoThinkVariants: false, // #6977: Opt-in per-connection auto-ping that warms a Codex OAuth connection's // quota window right after it resets, so the first real request doesn't land in // a cold window. `connections` maps connection id -> enabled. Default empty map // (nobody opted in) — the scheduler is a no-op until an operator flips a // connection on, since pinging burns a small amount of real quota (Hard Rule #20 // spirit: never mutate/consume on the operator's behalf by default). codexAutoPing: { connections: {} }, // #8848: opt-in per-connection Claude proactive warmup (empty = off for everyone). claudeWarmup: { connections: {} }, }; 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 || key.startsWith("_")) continue; try { settings[key] = JSON.parse(rawValue); } catch { settings[key] = rawValue; } } if (typeof settings.oidcClientSecret === "string") { settings.oidcClientSecret = decrypt(settings.oidcClientSecret) ?? ""; } applySessionAffinityLegacyFallback(settings); // 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, options?: { expectedRevision?: number } ) { // Detect first-time setup completion before we overwrite settings. let setupJustCompleted = false; if (updates.setupComplete === true) { try { const prev = await getSettings(); setupJustCompleted = prev.setupComplete !== true; } catch { setupJustCompleted = true; } } const db = getDbInstance(); const insert = db.prepare( "INSERT OR REPLACE INTO key_value (namespace, key, value) VALUES ('settings', ?, ?)" ); const tx = db.transaction(() => { const currentRevision = readSettingsRevision(db); if (options?.expectedRevision !== undefined && options.expectedRevision !== currentRevision) { throw new SettingsRevisionConflictError(currentRevision); } for (const [key, value] of Object.entries(updates)) { const toStore = key === "oidcClientSecret" ? encrypt(value as string) : value; insert.run(key, JSON.stringify(toStore)); } insert.run(SETTINGS_REVISION_KEY, JSON.stringify(currentRevision + 1)); }); tx(); backupDbFile("pre-write"); invalidateDbCache("settings"); // Bust the read cache immediately // Bust proxy resolution cache when proxy toggle settings change const PROXY_TOGGLE_KEYS = ["proxyEnabled", "perKeyProxyEnabled"]; if (Object.keys(updates).some((k) => PROXY_TOGGLE_KEYS.includes(k))) { bumpProxyConfigGeneration(); } 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 ); } // Onboarding / setup finished → one-shot Codex catalog revalidation (init case). if (setupJustCompleted) { void import("@/shared/services/codexCatalogRevalidation") .then(({ scheduleCodexCatalogRevalidationAfterInit }) => { scheduleCodexCatalogRevalidationAfterInit(); }) .catch(() => { // non-fatal }); } return nextSettings; } export async function isCloudEnabled() { const settings = await getSettings(); return settings.cloudEnabled === true; } // ──────────────── 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, apiKeyId?: string, providerId?: string ) { const cacheKey = providerId ? `${connectionId}:${apiKeyId || ""}:${providerId}` : apiKeyId ? `${connectionId}:${apiKeyId}` : connectionId; const startGeneration = proxyConfigGeneration; const startRegistryGeneration = getProxyRegistryGeneration(); const cached = proxyResolutionCache.get(cacheKey); if ( cached && cached.generation === startGeneration && cached.registryGeneration === startRegistryGeneration ) { return cached.result; } const db = getDbInstance(); // Step 1: Check global proxyEnabled setting // Read only the proxyEnabled key for performance instead of loading all settings. let globalProxyEnabled = true; try { const proxyEnabledRow = db .prepare("SELECT value FROM key_value WHERE namespace = 'settings' AND key = 'proxyEnabled'") .get() as { value?: string } | undefined; if (proxyEnabledRow?.value) { globalProxyEnabled = JSON.parse(proxyEnabledRow.value) !== false; } } catch { // Default to true on read error } if (!globalProxyEnabled) { const result: ProxyResolutionResult = { proxy: null, level: "direct", levelId: null }; // Do not cache the "direct" result when global toggle is off so that // toggling it back on takes effect immediately without a generation bump. return result; } let connectionRecord: JsonRecord | null = null; let connectionProvider: string | null = null; let connectionProxyEnabled = true; let connectionPerKeyProxyEnabled = false; const row = db .prepare( "SELECT provider, proxy_enabled, per_key_proxy_enabled FROM provider_connections WHERE id = ?" ) .get(connectionId); if (row) { connectionRecord = toRecord(row); connectionProvider = typeof connectionRecord.provider === "string" ? connectionRecord.provider : null; connectionProxyEnabled = connectionRecord.proxy_enabled !== 0; connectionPerKeyProxyEnabled = connectionRecord.per_key_proxy_enabled === 1; } // A connection-level Proxy Off is explicit: it must bypass every stored proxy // source for this connection, including account, provider, global, and automatic // fallback candidates from the proxy pool. if (connectionRecord && !connectionProxyEnabled) { const result: ProxyResolutionResult = { proxy: null, level: "direct", levelId: null }; cacheProxyResolution(cacheKey, startGeneration, startRegistryGeneration, result); return result; } // Step 1.5: Check global perKeyProxyEnabled setting let globalPerKeyProxyEnabled = false; try { const perKeyRow = db .prepare( "SELECT value FROM key_value WHERE namespace = 'settings' AND key = 'perKeyProxyEnabled'" ) .get() as { value?: string } | undefined; if (perKeyRow?.value) { globalPerKeyProxyEnabled = JSON.parse(perKeyRow.value) !== false; } } catch { // Default to false on read error } const config = await getProxyConfig(); // Step 2: API key-level proxy (only if per-key proxy is enabled globally or per-connection) if (apiKeyId) { // Check if per-key proxy is allowed: the global toggle is a true override — // when it is off, no connection's per-key assignment may apply, regardless // of that connection's own per_key_proxy_enabled flag (#8385). const perKeyEnabled = globalPerKeyProxyEnabled && connectionPerKeyProxyEnabled; if (perKeyEnabled) { try { const apiKeyRow = db.prepare("SELECT proxy_id FROM api_keys WHERE id = ?").get(apiKeyId) as { proxy_id?: string | null } | undefined; if (apiKeyRow?.proxy_id) { const proxyRow = db .prepare( "SELECT p.type, p.host, p.port, p.username, p.password, p.family FROM proxy_registry p WHERE p.id = ?" ) .get(apiKeyRow.proxy_id) as | { type: string; host: string; port: number; username: string; password: string; family?: string; } | undefined; if (proxyRow) { const result = { proxy: { type: proxyRow.type, host: proxyRow.host, port: proxyRow.port, username: proxyRow.username, password: proxyRow.password, family: typeof proxyRow.family === "string" ? proxyRow.family : "auto", }, level: "apiKey" as const, levelId: apiKeyId, source: "api_key" as const, }; cacheProxyResolution(cacheKey, startGeneration, startRegistryGeneration, result); return result; } } } catch { // Fall through to existing resolution } } } // Step 3: Account-level registry const registryAccount = await resolveProxyForScopeFromRegistry("account", connectionId); if (registryAccount?.proxy) { cacheProxyResolution(cacheKey, startGeneration, startRegistryGeneration, registryAccount); return registryAccount; } // Step 4: Legacy key-level if (connectionId && config.keys?.[connectionId]) { const result = { proxy: withFamilyDefault(config.keys[connectionId]), level: "key", levelId: connectionId, }; cacheProxyResolution(cacheKey, startGeneration, startRegistryGeneration, result); return result; } // Step 5: Use the connection's provider for provider/combo scoped proxies. if (connectionRecord) { // Step 6: Provider-level registry (only if proxy_enabled) if (connectionProvider && connectionProxyEnabled) { const registryProvider = await resolveProxyForScopeFromRegistry( "provider", connectionProvider ); if (registryProvider?.proxy) { cacheProxyResolution(cacheKey, startGeneration, startRegistryGeneration, registryProvider); return registryProvider; } } // Step 7: Combo-level (only if proxy_enabled). For every combo whose model // list references this connection's provider, check the modern registry // (proxy_assignments, scope='combo') first — this is the assignment the // dashboard's Combo "Set Proxy" modal actually writes to (#7149, where the // registry write path and this read path had diverged, leaving combo-level // proxy assignment completely inert). Fall back to the legacy in-memory // combos map for any pre-existing legacy data. if (connectionProvider && connectionProxyEnabled) { 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) continue; 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) === connectionProvider ); if (!usesProvider) continue; const registryCombo = await resolveProxyForScopeFromRegistry("combo", comboId); if (registryCombo?.proxy) { cacheProxyResolution(cacheKey, startGeneration, startRegistryGeneration, registryCombo); return registryCombo; } if (config.combos?.[comboId]) { const result = { proxy: withFamilyDefault(config.combos[comboId]), level: "combo", levelId: comboId, }; cacheProxyResolution(cacheKey, startGeneration, startRegistryGeneration, result); return result; } } catch { // Ignore malformed combo records during proxy resolution. } } } // Step 8: Legacy provider-level (only if proxy_enabled) if (connectionProvider && connectionProxyEnabled && config.providers?.[connectionProvider]) { const result = { proxy: withFamilyDefault(config.providers[connectionProvider]), level: "provider", levelId: connectionProvider, }; cacheProxyResolution(cacheKey, startGeneration, startRegistryGeneration, result); return result; } } // Step 8.5 (#6272): no-auth providers (mimocode, opencode, ...) share a single // synthetic connectionId that never matches a `provider_connections` row, so // `connectionRecord` above is null and Steps 5-8 (which require it) never run for // them — a provider-level proxy assigned to a no-auth provider was silently // ignored. Best-effort fallback: scan the known no-auth provider ids directly. if (!connectionRecord) { const noAuthFallback = await resolveNoAuthSharedProviderProxy(config.providers, providerId); if (noAuthFallback) { cacheProxyResolution(cacheKey, startGeneration, startRegistryGeneration, noAuthFallback); return noAuthFallback; } } // Step 9: Global registry const registryGlobal = await resolveProxyForScopeFromRegistry("global"); if (registryGlobal?.proxy) { cacheProxyResolution(cacheKey, startGeneration, startRegistryGeneration, registryGlobal); return registryGlobal; } // Step 10: Legacy global if (config.global) { const result = { proxy: withFamilyDefault(config.global), level: "global", levelId: null }; cacheProxyResolution(cacheKey, startGeneration, startRegistryGeneration, result); return result; } // Step 11: Auto-selection fallback (only when global proxy is enabled) try { const { selectWorkingProxyFallback } = await import("@omniroute/open-sse/utils/proxyFallback"); const fallback = await selectWorkingProxyFallback(connectionId); if (fallback) { // Auto-selected proxies are probed via a URL roundtrip that drops any // per-registry family policy, so default the family marker to "auto" // (no IPv6-only enforcement) when the fallback object omits it. const normalizedFallback = fallback.proxy && typeof fallback.proxy === "object" ? { ...fallback, proxy: withFamilyDefault(fallback.proxy as ProxyValue) } : fallback; cacheProxyResolution( cacheKey, startGeneration, startRegistryGeneration, normalizedFallback as ProxyResolutionResult ); return normalizedFallback; } } catch (err) { console.warn({ err, connectionId }, "Proxy fallback auto-selection failed"); } // Step 12: Return direct 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; } // ──────────────── Re-exports from leaf modules ──────────────── export { type PricingSource, type PricingSourceMap, getPricing, getPricingWithSources, getPricingForModel, updatePricing, resetPricing, resetAllPricing, } from "./settings/pricing"; export { type LKGPRecord, getLKGP, setLKGP, clearAllLKGP } from "./settings/lkgp"; export { type CacheTrendPoint, getCacheMetrics, updateCacheMetrics, getCacheTrend, resetCacheMetrics, } from "./settings/cacheMetrics"; export { getCachedSettings } from "./readCache";