diff --git a/open-sse/handlers/chatCore.ts b/open-sse/handlers/chatCore.ts index e134a744f8..57469a7474 100644 --- a/open-sse/handlers/chatCore.ts +++ b/open-sse/handlers/chatCore.ts @@ -148,7 +148,7 @@ import { PROVIDER_ERROR_TYPES, isEmptyContentResponse, } from "../services/errorClassifier.ts"; -import { updateProviderConnection, getProviderConnectionById } from "@/lib/db/providers"; +import { updateProviderConnection } from "@/lib/db/providers"; import { wasRefreshTokenRotated } from "@omniroute/open-sse/services/refreshSerializer.ts"; import { connectionHasExtraKeys } from "../services/apiKeyRotator.ts"; import { recordKeyHealthStatus as recordKeyHealthStatusFor } from "./chatCore/keyHealth.ts"; @@ -228,7 +228,7 @@ import { normalizeExecutorResult, executeWithUpstreamStartTimeout, } from "./chatCore/upstreamTimeouts.ts"; -import { getModelNormalizeToolCallId, getModelPreserveOpenAIDeveloperRole } from "@/lib/localDb"; +import { getModelNormalizeToolCallId, getModelPreserveOpenAIDeveloperRole, getCachedProviderConnectionById } from "@/lib/localDb"; import { getProviderCredentials, extractSessionAffinityKey } from "@/sse/services/auth"; import { deleteSessionAccountAffinity } from "@/lib/db/sessionAccountAffinity"; import { getCacheControlSettings } from "@/lib/cacheControlSettings"; @@ -3074,7 +3074,7 @@ export async function handleChatCore({ typeof credentials?.connectionId === "string" ? credentials.connectionId.trim() : ""; const casReread = casConnectionId ? async () => { - const latest = await getProviderConnectionById(casConnectionId); + const latest = await getCachedProviderConnectionById(casConnectionId); return typeof latest?.refreshToken === "string" ? latest.refreshToken : null; } : null; @@ -3165,7 +3165,7 @@ export async function handleChatCore({ let alreadyRotated = false; if (typeof connectionId === "string" && connectionId && attemptedRefreshToken) { try { - const latest = await getProviderConnectionById(connectionId); + const latest = await getCachedProviderConnectionById(connectionId); if (wasRefreshTokenRotated(attemptedRefreshToken, latest?.refreshToken)) { alreadyRotated = true; log?.warn?.( diff --git a/open-sse/handlers/chatCore/codexFailover.ts b/open-sse/handlers/chatCore/codexFailover.ts index 175f167e9a..f552af7375 100644 --- a/open-sse/handlers/chatCore/codexFailover.ts +++ b/open-sse/handlers/chatCore/codexFailover.ts @@ -1,5 +1,6 @@ import { getCodexModelScope } from "../../config/codexQuotaScopes.ts"; -import { getProviderConnectionById, updateProviderConnection } from "@/lib/db/providers"; +import { updateProviderConnection } from "@/lib/db/providers"; +import { getCachedProviderConnectionById } from "@/lib/localDb"; type CodexFailoverCredentials = { connectionId?: string | null; @@ -16,7 +17,7 @@ export async function markCodexScopeRateLimited(params: { rateLimitedUntil: string; credentials?: CodexFailoverCredentials | null; }): Promise { - const connection = await getProviderConnectionById(params.failedConnectionId).catch(() => null); + const connection = await getCachedProviderConnectionById(params.failedConnectionId).catch(() => null); const existingProviderData = connection ? asProviderData(connection.providerSpecificData) : asProviderData(params.credentials?.providerSpecificData); diff --git a/open-sse/services/combo/concurrencyCaps.ts b/open-sse/services/combo/concurrencyCaps.ts index 329df99069..3384352abb 100644 --- a/open-sse/services/combo/concurrencyCaps.ts +++ b/open-sse/services/combo/concurrencyCaps.ts @@ -19,14 +19,14 @@ * shrinking (Quality Gate / #3501). */ -import { getProviderConnectionById } from "../../../src/lib/db/providers"; +import { getCachedProviderConnectionById } from "@/lib/localDb"; import { effectiveMaxConcurrency } from "./comboPredicates.ts"; import type { ResolvedComboTarget } from "./types.ts"; /** Read a connection's positive `maxConcurrent`, or null when unset / <= 0 / on error. */ export async function lookupPositiveCap(connectionId: string): Promise { try { - const conn = await getProviderConnectionById(connectionId); + const conn = await getCachedProviderConnectionById(connectionId); const raw = (conn as { maxConcurrent?: number | null } | null)?.maxConcurrent; return typeof raw === "number" && Number.isFinite(raw) && raw > 0 ? raw : null; } catch { diff --git a/open-sse/services/combo/quotaExhaustionCutoff.ts b/open-sse/services/combo/quotaExhaustionCutoff.ts index e0b75c1873..016e78ab63 100644 --- a/open-sse/services/combo/quotaExhaustionCutoff.ts +++ b/open-sse/services/combo/quotaExhaustionCutoff.ts @@ -19,7 +19,7 @@ import { type PreflightQuotaThresholds, type QuotaInfo, } from "../quotaPreflight.ts"; -import { getProviderConnectionById } from "../../../src/lib/db/providers"; +import { getCachedProviderConnectionById } from "@/lib/localDb"; import { resolveResilienceSettings, type ResilienceSettings, @@ -108,7 +108,7 @@ export async function resolveQuotaExhaustionCutoffForTarget( let connection: Record | undefined; try { - connection = (await getProviderConnectionById(connectionId)) as + connection = (await getCachedProviderConnectionById(connectionId)) as | Record | undefined; } catch { diff --git a/open-sse/services/tokenRefresh.ts b/open-sse/services/tokenRefresh.ts index b479daf278..a0c3360787 100755 --- a/open-sse/services/tokenRefresh.ts +++ b/open-sse/services/tokenRefresh.ts @@ -1914,8 +1914,8 @@ async function _getAccessTokenWithStalenessCheck(provider, credentials, log, pro // We MUST check if the DB has a newer token before proceeding with a network refresh. if (credentials.connectionId) { try { - const { getProviderConnectionById } = await import("../../src/lib/db/providers"); - const dbConnection = await getProviderConnectionById(credentials.connectionId); + const { getCachedProviderConnectionById } = await import("@/lib/localDb"); + const dbConnection = await getCachedProviderConnectionById(credentials.connectionId); if (dbConnection && dbConnection.refreshToken) { const now = Date.now(); const dbExpiresAt = dbConnection.expiresAt ? new Date(dbConnection.expiresAt).getTime() : 0; diff --git a/src/app/api/provider-nodes/route.ts b/src/app/api/provider-nodes/route.ts index 87bb5330b2..95c765c9a4 100644 --- a/src/app/api/provider-nodes/route.ts +++ b/src/app/api/provider-nodes/route.ts @@ -1,5 +1,6 @@ import { NextResponse } from "next/server"; -import { createProviderNode, getProviderNodes } from "@/models"; +import { createProviderNode } from "@/models"; +import { getCachedProviderNodes } from "@/lib/localDb"; import { OPENAI_COMPATIBLE_PREFIX, ANTHROPIC_COMPATIBLE_PREFIX, @@ -36,7 +37,7 @@ function sanitizeClaudeCodeCompatibleBaseUrl(baseUrl: string) { // GET /api/provider-nodes - List all provider nodes export async function GET() { try { - const nodes = await getProviderNodes(); + const nodes = await getCachedProviderNodes(); return NextResponse.json({ nodes, ccCompatibleProviderEnabled: isCcCompatibleProviderEnabled(), diff --git a/src/app/api/providers/[id]/login/route.ts b/src/app/api/providers/[id]/login/route.ts index ccc0719622..a095cd7f68 100644 --- a/src/app/api/providers/[id]/login/route.ts +++ b/src/app/api/providers/[id]/login/route.ts @@ -7,7 +7,7 @@ */ import { NextRequest, NextResponse } from "next/server"; -import { getProviderConnectionById, updateProviderConnection } from "@/lib/localDb"; +import { getCachedProviderConnectionById, updateProviderConnection } from "@/lib/localDb"; import { requireManagementAuth } from "@/lib/api/requireManagementAuth"; import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error.ts"; @@ -21,7 +21,7 @@ export async function POST( if (auth) return auth; const { id } = await params; - const provider = await getProviderConnectionById(id); + const provider = await getCachedProviderConnectionById(id); if (!provider) { return NextResponse.json({ success: false, error: "Provider not found" }, { status: 404 }); } diff --git a/src/app/api/providers/[id]/models/route.ts b/src/app/api/providers/[id]/models/route.ts index ebca992a15..0fe1c67b76 100755 --- a/src/app/api/providers/[id]/models/route.ts +++ b/src/app/api/providers/[id]/models/route.ts @@ -10,7 +10,7 @@ import { getModelsByProviderId } from "@/shared/constants/models"; import { getStaticModelsForProvider } from "@/lib/providers/staticModels"; import { isProviderBlockedByIdOrAlias } from "@/shared/utils/noAuthProviders"; import { - getProviderConnectionById, + getCachedProviderConnectionById, getSettings, getModelIsHidden, resolveProxyForProvider, @@ -124,7 +124,7 @@ export async function GET( const excludeCustom = searchParams.get("excludeCustom") === "true"; const refresh = searchParams.get("refresh") === "true"; - const connection = await getProviderConnectionById(id); + const connection = await getCachedProviderConnectionById(id); if (!connection) { // #3047 — no-auth providers have no connection rows; serve their catalog by provider id. diff --git a/src/app/api/providers/[id]/refresh/route.ts b/src/app/api/providers/[id]/refresh/route.ts index 30c926270c..a6a0847c5c 100644 --- a/src/app/api/providers/[id]/refresh/route.ts +++ b/src/app/api/providers/[id]/refresh/route.ts @@ -1,5 +1,6 @@ import { NextResponse } from "next/server"; -import { getProviderConnectionById, updateProviderConnection } from "@/lib/db/providers"; +import { getCachedProviderConnectionById } from "@/lib/localDb"; +import { updateProviderConnection } from "@/lib/db/providers"; import { getAccessToken, updateProviderCredentials } from "@/sse/services/tokenRefresh"; import { rotationGroupFor } from "@omniroute/open-sse/services/refreshSerializer.ts"; @@ -21,7 +22,7 @@ export async function POST(_request: Request, { params }: { params: Promise<{ id try { const { id } = await params; - const connection = await getProviderConnectionById(id); + const connection = await getCachedProviderConnectionById(id); if (!connection) { return NextResponse.json({ error: "Connection not found" }, { status: 404 }); } diff --git a/src/app/api/providers/[id]/route.ts b/src/app/api/providers/[id]/route.ts index c92a7a2c67..47767f184a 100644 --- a/src/app/api/providers/[id]/route.ts +++ b/src/app/api/providers/[id]/route.ts @@ -5,11 +5,11 @@ import { summarizeProviderConnectionForAudit, } from "@/lib/compliance/providerAudit"; import { - getProviderConnectionById, + getCachedProviderConnectionById, updateProviderConnection, deleteProviderConnection, isCloudEnabled, -} from "@/models"; +} from "@/lib/localDb"; import { getConsistentMachineId } from "@/shared/utils/machineId"; import { syncToCloud } from "@/lib/cloudSync"; import { updateProviderConnectionSchema } from "@/shared/validation/schemas"; @@ -57,7 +57,7 @@ export async function GET(request: Request, { params }: { params: Promise<{ id: try { const { id } = await params; - const connection = await getProviderConnectionById(id); + const connection = await getCachedProviderConnectionById(id); if (!connection) { return NextResponse.json({ error: "Connection not found" }, { status: 404 }); @@ -140,7 +140,7 @@ export async function PUT(request: Request, { params }: { params: Promise<{ id: rateLimitOverrides, } = body; - const existing = (await getProviderConnectionById(id)) as Record | null; + const existing = (await getCachedProviderConnectionById(id)) as Record | null; if (!existing) { return NextResponse.json({ error: "Connection not found" }, { status: 404 }); } @@ -342,7 +342,7 @@ export async function DELETE(request: Request, { params }: { params: Promise<{ i const { id } = await params; // Fetch connection before deleting to check provider type - const connection = (await getProviderConnectionById(id)) as Record | null; + const connection = (await getCachedProviderConnectionById(id)) as Record | null; if (!connection) { return NextResponse.json({ error: "Connection not found" }, { status: 404 }); } diff --git a/src/app/api/providers/[id]/sync-models/route.ts b/src/app/api/providers/[id]/sync-models/route.ts index 94578e4c4c..11a32c74df 100644 --- a/src/app/api/providers/[id]/sync-models/route.ts +++ b/src/app/api/providers/[id]/sync-models/route.ts @@ -1,5 +1,5 @@ import { NextResponse } from "next/server"; -import { getProviderConnectionById } from "@/models"; +import { getCachedProviderConnectionById } from "@/lib/localDb"; import { getSyncedAvailableModelsForConnection } from "@/lib/db/models"; import { selectModelsForImport } from "@/shared/utils/freeModels"; import { @@ -385,7 +385,7 @@ export async function POST(request: Request, { params }: { params: Promise<{ id: ); } - const connection = await getProviderConnectionById(id); + const connection = await getCachedProviderConnectionById(id); if (!connection) { return NextResponse.json({ error: "Connection not found" }, { status: 404 }); } diff --git a/src/app/api/providers/[id]/test/route.ts b/src/app/api/providers/[id]/test/route.ts index 0424645209..658bd3ebd0 100644 --- a/src/app/api/providers/[id]/test/route.ts +++ b/src/app/api/providers/[id]/test/route.ts @@ -2,7 +2,7 @@ import { NextResponse } from "next/server"; import { z } from "zod"; import { isValidationFailure, validateBody } from "@/shared/validation/helpers"; import { - getProviderConnectionById, + getCachedProviderConnectionById, updateProviderConnection, isCloudEnabled, resolveProxyForConnection, @@ -741,7 +741,7 @@ async function testApiKeyConnection(connection: any) { * @returns {Promise} Test result (same shape as the JSON response) */ export async function testSingleConnection(connectionId: string, validationModelId?: string) { - const connection = await getProviderConnectionById(connectionId); + const connection = await getCachedProviderConnectionById(connectionId); if (!connection) { return { valid: false, error: "Connection not found", diagnosis: null, latencyMs: 0 }; diff --git a/src/app/api/providers/command-code/auth/apply/route.ts b/src/app/api/providers/command-code/auth/apply/route.ts index 9e4cb24f0a..3aee6b1a6e 100644 --- a/src/app/api/providers/command-code/auth/apply/route.ts +++ b/src/app/api/providers/command-code/auth/apply/route.ts @@ -2,9 +2,9 @@ import { requireManagementAuth } from "@/lib/api/requireManagementAuth"; import { consumeCommandCodeAuthSecret } from "@/lib/db/commandCodeAuth"; import { createProviderConnection, - getProviderConnectionById, updateProviderConnection, } from "@/lib/db/providers"; +import { getCachedProviderConnectionById } from "@/lib/localDb"; import { sanitizeProviderSpecificDataForResponse } from "@/lib/providers/requestDefaults"; import { commandCodeApplySchema, noStoreJson, stateHashFromState } from "../shared"; @@ -42,7 +42,7 @@ export async function POST(request: Request) { let existing: Record | null = null; if (parsed.data.connectionId) { - existing = (await getProviderConnectionById(parsed.data.connectionId)) as Record< + existing = (await getCachedProviderConnectionById(parsed.data.connectionId)) as Record< string, unknown > | null; diff --git a/src/app/api/settings/export-json/route.ts b/src/app/api/settings/export-json/route.ts index 942a18caaf..83af8abc26 100644 --- a/src/app/api/settings/export-json/route.ts +++ b/src/app/api/settings/export-json/route.ts @@ -2,7 +2,7 @@ import { NextResponse } from "next/server"; import { getSettings, getProviderConnections, - getProviderNodes, + getCachedProviderNodes, getCombos, getApiKeys, } from "@/lib/localDb"; @@ -69,7 +69,7 @@ export async function GET(request: Request) { const { password: _pw, requireLogin: _rl, ...safeSettings } = rawSettings; const providerConnections = await getProviderConnections(); - const providerNodes = await getProviderNodes(); + const providerNodes = await getCachedProviderNodes(); const combosRaw = await getCombos(); const apiKeys = await getApiKeys(); diff --git a/src/app/api/v1/audio/speech/route.ts b/src/app/api/v1/audio/speech/route.ts index 576721f522..40146f73cb 100644 --- a/src/app/api/v1/audio/speech/route.ts +++ b/src/app/api/v1/audio/speech/route.ts @@ -13,7 +13,7 @@ import { import { errorResponse } from "@omniroute/open-sse/utils/error.ts"; import { HTTP_STATUS } from "@omniroute/open-sse/config/constants.ts"; import { enforceApiKeyPolicy } from "@/shared/utils/apiKeyPolicy"; -import { getProviderNodes } from "@/lib/localDb"; +import { getCachedProviderNodes } from "@/lib/localDb"; import { v1AudioSpeechSchema } from "@/shared/validation/schemas"; import { isValidationFailure, validateBody } from "@/shared/validation/helpers"; import { @@ -62,7 +62,7 @@ async function postHandler(request, context) { // Load local provider_nodes for audio routing (only localhost — prevents auth bypass/SSRF) let dynamicProviders: ReturnType[] = []; try { - const nodes = await getProviderNodes(); + const nodes = await getCachedProviderNodes(); dynamicProviders = (Array.isArray(nodes) ? (nodes as unknown as ProviderNodeRow[]) : []) .filter((n: ProviderNodeRow) => { if (n.apiType !== "chat" && n.apiType !== "responses") return false; diff --git a/src/app/api/v1/audio/transcriptions/route.ts b/src/app/api/v1/audio/transcriptions/route.ts index 3087d260d5..548f3e8993 100644 --- a/src/app/api/v1/audio/transcriptions/route.ts +++ b/src/app/api/v1/audio/transcriptions/route.ts @@ -14,7 +14,7 @@ import { import { errorResponse } from "@omniroute/open-sse/utils/error.ts"; import { HTTP_STATUS } from "@omniroute/open-sse/config/constants.ts"; import { enforceApiKeyPolicy } from "@/shared/utils/apiKeyPolicy"; -import { getProviderNodes } from "@/lib/localDb"; +import { getCachedProviderNodes } from "@/lib/localDb"; import { isAllRateLimitedCredentials, rateLimitedProviderResponse, @@ -60,7 +60,7 @@ export async function POST(request) { // Load local provider_nodes for audio routing (only localhost — prevents auth bypass/SSRF) let dynamicProviders: ReturnType[] = []; try { - const nodes = await getProviderNodes(); + const nodes = await getCachedProviderNodes(); dynamicProviders = (Array.isArray(nodes) ? (nodes as unknown as ProviderNodeRow[]) : []) .filter((n: ProviderNodeRow) => { if (n.apiType !== "chat" && n.apiType !== "responses") return false; diff --git a/src/app/api/v1/audio/translations/route.ts b/src/app/api/v1/audio/translations/route.ts index 622afcfe29..7283dda555 100644 --- a/src/app/api/v1/audio/translations/route.ts +++ b/src/app/api/v1/audio/translations/route.ts @@ -14,7 +14,7 @@ import { import { errorResponse } from "@omniroute/open-sse/utils/error.ts"; import { HTTP_STATUS } from "@omniroute/open-sse/config/constants.ts"; import { enforceApiKeyPolicy } from "@/shared/utils/apiKeyPolicy"; -import { getProviderNodes } from "@/lib/localDb"; +import { getCachedProviderNodes } from "@/lib/localDb"; import { isAllRateLimitedCredentials, rateLimitedProviderResponse, @@ -62,7 +62,7 @@ export async function POST(request) { // Load local provider_nodes for audio routing (only localhost — prevents auth bypass/SSRF) let dynamicProviders: ReturnType[] = []; try { - const nodes = await getProviderNodes(); + const nodes = await getCachedProviderNodes(); dynamicProviders = (Array.isArray(nodes) ? (nodes as unknown as ProviderNodeRow[]) : []) .filter((n: ProviderNodeRow) => { if (n.apiType !== "chat" && n.apiType !== "responses") return false; diff --git a/src/app/api/v1/models/catalog.ts b/src/app/api/v1/models/catalog.ts index f4419da282..49388035eb 100644 --- a/src/app/api/v1/models/catalog.ts +++ b/src/app/api/v1/models/catalog.ts @@ -5,7 +5,7 @@ import { getCombos, getAllCustomModels, getSettings, - getProviderNodes, + getCachedProviderNodes, getModelIsHidden, getModelAliases, } from "@/lib/localDb"; @@ -327,7 +327,7 @@ async function buildUnifiedModelsResponseCore( // Get provider nodes (for compatible providers with custom prefixes) let providerNodes = []; try { - providerNodes = await getProviderNodes(); + providerNodes = await getCachedProviderNodes(); } catch (e) { console.log("Could not fetch provider nodes"); } diff --git a/src/app/api/v1/rerank/route.ts b/src/app/api/v1/rerank/route.ts index a2f60bb623..7130af0d20 100644 --- a/src/app/api/v1/rerank/route.ts +++ b/src/app/api/v1/rerank/route.ts @@ -10,7 +10,7 @@ import { HTTP_STATUS } from "@omniroute/open-sse/config/constants.ts"; import { enforceApiKeyPolicy } from "@/shared/utils/apiKeyPolicy"; import { v1RerankSchema } from "@/shared/validation/schemas"; import { isValidationFailure, validateBody } from "@/shared/validation/helpers"; -import { getProviderNodes } from "@/lib/localDb"; +import { getCachedProviderNodes } from "@/lib/localDb"; import { isAllRateLimitedCredentials, rateLimitedProviderResponse, @@ -73,7 +73,7 @@ async function postHandler(request, context) { // Load local provider_nodes for rerank routing (localhost only) let localProviders: ReturnType[] = []; try { - const nodes = await getProviderNodes(); + const nodes = await getCachedProviderNodes(); localProviders = (Array.isArray(nodes) ? nodes : []) .filter((n: any) => { try { diff --git a/src/domain/quotaCache.ts b/src/domain/quotaCache.ts index b8e5e0f365..d02671d280 100644 --- a/src/domain/quotaCache.ts +++ b/src/domain/quotaCache.ts @@ -14,7 +14,7 @@ */ import { getUsageForProvider } from "@omniroute/open-sse/services/usage.ts"; -import { getProviderConnectionById, resolveProxyForConnection } from "@/lib/localDb"; +import { getCachedProviderConnectionById, resolveProxyForConnection } from "@/lib/localDb"; import { runWithProxyContext } from "@omniroute/open-sse/utils/proxyFetch.ts"; import { safePercentage } from "@/shared/utils/formatting"; import { @@ -453,7 +453,7 @@ async function refreshEntry(entry: QuotaCacheEntry) { refreshingSet.add(entry.connectionId); try { - const connection = await getProviderConnectionById(entry.connectionId); + const connection = await getCachedProviderConnectionById(entry.connectionId); if (!connection || connection.authType !== "oauth" || !connection.isActive) { cache.delete(entry.connectionId); return; diff --git a/src/lib/db/providers/nodes.ts b/src/lib/db/providers/nodes.ts index 667f00900c..0569a4527c 100644 --- a/src/lib/db/providers/nodes.ts +++ b/src/lib/db/providers/nodes.ts @@ -6,6 +6,7 @@ import { v4 as uuidv4 } from "uuid"; import { getDbInstance, rowToCamel } from "../core"; import { selectProviderNodeForConnection } from "../providerNodeSelect"; import { backupDbFile } from "../backup"; +import { invalidateDbCache } from "../readCache"; import { toRecord, type JsonRecord } from "./columns"; interface StatementLike { @@ -79,6 +80,7 @@ export async function createProviderNode(data: JsonRecord) { ).run(node); backupDbFile("pre-write"); + invalidateDbCache("nodes"); const result: JsonRecord = { ...node }; if (customHeadersJson) { @@ -142,6 +144,7 @@ export async function updateProviderNode(id: string, data: JsonRecord) { }); backupDbFile("pre-write"); + invalidateDbCache("nodes"); const result: JsonRecord = { ...merged }; const storedJson = merged["customHeadersJson"] as string | null; @@ -165,5 +168,6 @@ export async function deleteProviderNode(id: string) { db.prepare("DELETE FROM provider_nodes WHERE id = ?").run(id); backupDbFile("pre-write"); + invalidateDbCache("nodes"); return rowToCamel(existing); } diff --git a/src/lib/db/readCache.ts b/src/lib/db/readCache.ts index 8ce10f1460..8bb03fee15 100644 --- a/src/lib/db/readCache.ts +++ b/src/lib/db/readCache.ts @@ -107,6 +107,43 @@ export async function getCachedProviderConnections( connectionsCache.set("all", value); return value; } +const connectionByIdCache = new TTLCache | null>(CONNECTIONS_TTL_MS); +const nodesCache = new TTLCache(CONNECTIONS_TTL_MS); + +/** + * Cached wrapper for getProviderConnectionById. + * Keyed by connection ID, shared 5s TTL. + * Invalidated on every provider_connections write. + */ +export async function getCachedProviderConnectionById( + id: string +): Promise | null> { + const cached = connectionByIdCache.get(id); + if (cached !== undefined) return cached; + + const { getProviderConnectionById } = await import("@/lib/db/providers"); + const value = await getProviderConnectionById(id); + connectionByIdCache.set(id, value); + return value; +} + +/** + * Cached wrapper for getProviderNodes. + * Keyed by JSON-serialized filter, shared 5s TTL. + * Invalidated on every provider_nodes write. + */ +export async function getCachedProviderNodes( + filter?: Record +): Promise { + const cacheKey = filter ? JSON.stringify(filter) : "all"; + const cached = nodesCache.get(cacheKey); + if (cached) return cached; + + const { getProviderNodes } = await import("@/lib/db/providers"); + const value = await getProviderNodes(filter); + nodesCache.set(cacheKey, value); + return value; +} // ──────────────── LKGP Cache Wrappers ──────────────── @@ -188,12 +225,18 @@ export function getModelCatalogCacheVersion(): number { /** * Invalidate all caches (call after writes to any of: settings, pricing, - * connections, combos). + * connections, combos, nodes). */ -export function invalidateDbCache(scope?: "settings" | "pricing" | "connections" | "combos"): void { +export function invalidateDbCache( + scope?: "settings" | "pricing" | "connections" | "combos" | "nodes" +): void { if (!scope || scope === "settings") settingsCache.invalidate(); if (!scope || scope === "pricing") pricingCache.invalidate(); - if (!scope || scope === "connections") connectionsCache.invalidate(); + if (!scope || scope === "connections") { + connectionsCache.invalidate(); + connectionByIdCache.invalidate(); + } + if (!scope || scope === "nodes") nodesCache.invalidate(); if (!scope || scope === "combos") combosCacheVersion++; // Settings/connections/combos all feed the unified model catalog builder // (blockedProviders + hidePaidModels, provider connections + excludedModels, diff --git a/src/lib/embeddings/service.ts b/src/lib/embeddings/service.ts index 19c5da6fa6..ba3d0bb43d 100644 --- a/src/lib/embeddings/service.ts +++ b/src/lib/embeddings/service.ts @@ -11,7 +11,7 @@ import { HTTP_STATUS } from "@omniroute/open-sse/config/constants.ts"; import * as log from "@/sse/utils/logger"; import { toJsonErrorPayload } from "@/shared/utils/upstreamError"; import { getProviderCredentials, clearRecoveredProviderState } from "@/sse/services/auth"; -import { getProviderNodes, getComboByName, getCombos, getDatabaseSettings } from "@/lib/localDb"; +import { getCachedProviderNodes, getComboByName, getCombos, getDatabaseSettings } from "@/lib/localDb"; import { resolveProxyForConnection } from "@/lib/db/settings"; import { runWithProxyContext } from "@omniroute/open-sse/utils/proxyFetch.ts"; import { handleComboChat } from "@omniroute/open-sse/services/combo.ts"; @@ -124,7 +124,7 @@ export async function createEmbeddingResponse( } let dynamicProviders: ReturnType[] = []; try { - const nodes = (await getProviderNodes()) as unknown as EmbeddingProviderNodeRow[]; + const nodes = (await getCachedProviderNodes()) as unknown as EmbeddingProviderNodeRow[]; dynamicProviders = (Array.isArray(nodes) ? nodes : []) .filter((n) => { const validTypes = ["chat", "responses", "embeddings"]; @@ -163,7 +163,7 @@ export async function createEmbeddingResponse( if (!providerConfig) { try { - const allNodes = (await getProviderNodes()) as unknown as EmbeddingProviderNodeRow[]; + const allNodes = (await getCachedProviderNodes()) as unknown as EmbeddingProviderNodeRow[]; const matchingNode = (Array.isArray(allNodes) ? allNodes : []).find( (n) => n.prefix === provider && diff --git a/src/lib/images/imageRouteModel.ts b/src/lib/images/imageRouteModel.ts index 669c27b567..0203b102d1 100644 --- a/src/lib/images/imageRouteModel.ts +++ b/src/lib/images/imageRouteModel.ts @@ -18,7 +18,7 @@ import { parseImageModel } from "@omniroute/open-sse/config/imageRegistry.ts"; import { resolveComboTargets } from "@omniroute/open-sse/services/combo.ts"; import { getComboByName, getCombos } from "@/lib/db/combos"; -import { getProviderNodes } from "@/lib/db/providers"; +import { getCachedProviderNodes } from "@/lib/localDb"; /** * Rewrite a `prefix/model` custom image model to its internal `/` form. @@ -36,7 +36,7 @@ export async function resolveImageModelPrefix(modelStr: string): Promise if (!rest) return modelStr; try { - const nodes = await getProviderNodes({ type: "openai-compatible" }); + const nodes = await getCachedProviderNodes({ type: "openai-compatible" }); // node.id (internal UUID) is already a valid internal id; only rewrite when a // user-defined prefix differs from the node id. const matched = nodes.find((node: { prefix?: unknown }) => node.prefix === prefixPart); diff --git a/src/lib/localDb.ts b/src/lib/localDb.ts index 44075cabe2..74e65fff68 100755 --- a/src/lib/localDb.ts +++ b/src/lib/localDb.ts @@ -29,10 +29,6 @@ export { // T05: Rate-limit DB persistence (survives token refresh) setConnectionRateLimitUntil, - isConnectionRateLimited, - getRateLimitedConnections, - - // T05 startup recovery: clear stale transient cooldowns left by a prior crash clearStaleCrashCooldowns, // T13: Stale quota display fix (zero out usage after window resets) @@ -238,6 +234,8 @@ export { getCachedSettings, getCachedPricing, getCachedProviderConnections, + getCachedProviderConnectionById, + getCachedProviderNodes, getCachedLKGP, setCachedLKGP, invalidateDbCache, diff --git a/src/lib/localHealthCheck.ts b/src/lib/localHealthCheck.ts index e61a35aac4..244f8d3add 100644 --- a/src/lib/localHealthCheck.ts +++ b/src/lib/localHealthCheck.ts @@ -11,7 +11,7 @@ * Uses Promise.allSettled so one slow/down node doesn't block others. */ -import { getProviderNodes } from "@/lib/localDb"; +import { getCachedProviderNodes } from "@/lib/localDb"; // ── Types ──────────────────────────────────────────────────────────────── @@ -159,7 +159,7 @@ export async function sweep(): Promise { try { let nodes: Array<{ id: string; prefix: string; baseUrl: string }>; try { - const raw = await getProviderNodes(); + const raw = await getCachedProviderNodes(); nodes = (Array.isArray(raw) ? raw : []).filter( (n: Record) => typeof n.baseUrl === "string" && isLocalhostUrl(n.baseUrl as string) diff --git a/src/lib/memory/embedding/index.ts b/src/lib/memory/embedding/index.ts index aaf645ddbc..0033e6c9e6 100644 --- a/src/lib/memory/embedding/index.ts +++ b/src/lib/memory/embedding/index.ts @@ -4,7 +4,7 @@ import { type EmbeddingProviderNodeRow, } from "@omniroute/open-sse/config/embeddingRegistry.ts"; import { getProviderCredentials } from "@/sse/services/auth"; -import { getProviderNodes } from "@/lib/localDb"; +import { getCachedProviderNodes } from "@/lib/localDb"; import type { MemorySettingsExtended } from "@/shared/schemas/memory"; import type { EmbeddingResolution, @@ -217,7 +217,7 @@ export async function listEmbeddingProviders(): Promise[] = []; try { - const nodes = (await getProviderNodes()) as unknown as EmbeddingProviderNodeRow[]; + const nodes = (await getCachedProviderNodes()) as unknown as EmbeddingProviderNodeRow[]; dynamicProviders = (Array.isArray(nodes) ? nodes : []) .filter((n) => { const validTypes = ["chat", "responses", "embeddings"]; diff --git a/src/lib/monitoring/providerHealthAutopilot.ts b/src/lib/monitoring/providerHealthAutopilot.ts index c69b8ab7c1..8fc54e2d86 100644 --- a/src/lib/monitoring/providerHealthAutopilot.ts +++ b/src/lib/monitoring/providerHealthAutopilot.ts @@ -1,10 +1,10 @@ import { createHash } from "crypto"; import { - getProviderConnectionById, getProviderConnections, updateProviderConnection, } from "@/lib/db/providers"; +import { getCachedProviderConnectionById } from "@/lib/localDb"; import { clearProviderFailure, clearModelLock } from "@omniroute/open-sse/services/accountFallback"; type JsonRecord = Record; @@ -672,7 +672,7 @@ export async function executeProviderHealthAutopilotAction( if (!connectionId) { return { status: 400, body: { success: false, error: "connectionId is required" } }; } - const connection = (await getProviderConnectionById(connectionId)) as JsonRecord | null; + const connection = (await getCachedProviderConnectionById(connectionId)) as JsonRecord | null; if (!connection || connection.provider !== provider) { return { status: 404, body: { success: false, error: "connection not found" } }; } @@ -692,7 +692,7 @@ export async function executeProviderHealthAutopilotAction( body: { success: false, error: "connectionId and model are required" }, }; } - const connection = (await getProviderConnectionById(connectionId)) as JsonRecord | null; + const connection = (await getCachedProviderConnectionById(connectionId)) as JsonRecord | null; if (!connection || connection.provider !== provider) { return { status: 404, body: { success: false, error: "connection not found" } }; } @@ -715,7 +715,7 @@ export async function executeProviderHealthAutopilotAction( if (!connectionId) { return { status: 400, body: { success: false, error: "connectionId is required" } }; } - const connection = (await getProviderConnectionById(connectionId)) as JsonRecord | null; + const connection = (await getCachedProviderConnectionById(connectionId)) as JsonRecord | null; if (!connection || connection.provider !== provider) { return { status: 404, body: { success: false, error: "connection not found" } }; } diff --git a/src/lib/oauth/utils/claudeAuthFile.ts b/src/lib/oauth/utils/claudeAuthFile.ts index d6c853f1e1..681c0eaab0 100644 --- a/src/lib/oauth/utils/claudeAuthFile.ts +++ b/src/lib/oauth/utils/claudeAuthFile.ts @@ -1,6 +1,6 @@ import fs from "fs/promises"; import path from "path"; -import { getProviderConnectionById } from "@/lib/localDb"; +import { getCachedProviderConnectionById } from "@/lib/localDb"; import { createBackup } from "@/shared/services/backupService"; import { getCliConfigPaths } from "@/shared/services/cliRuntime"; import { @@ -172,7 +172,7 @@ export function buildClaudeAuthPayload(connection: ClaudeConnectionLike): Claude } async function resolveFreshClaudeConnection(connectionId: string): Promise { - const connection = (await getProviderConnectionById(connectionId)) as ClaudeConnectionLike | null; + const connection = (await getCachedProviderConnectionById(connectionId)) as ClaudeConnectionLike | null; if (!connection) { throw new ClaudeAuthFileError("Connection not found", 404, "not_found"); } diff --git a/src/lib/oauth/utils/codexAuthFile.ts b/src/lib/oauth/utils/codexAuthFile.ts index eec0841e50..83e107489c 100644 --- a/src/lib/oauth/utils/codexAuthFile.ts +++ b/src/lib/oauth/utils/codexAuthFile.ts @@ -1,6 +1,6 @@ import fs from "fs/promises"; import path from "path"; -import { getProviderConnectionById } from "@/lib/localDb"; +import { getCachedProviderConnectionById } from "@/lib/localDb"; import { createBackup } from "@/shared/services/backupService"; import { getCliConfigPaths } from "@/shared/services/cliRuntime"; import { @@ -197,7 +197,7 @@ function buildCodexAuthPayload(connection: CodexConnectionLike): CodexAuthFilePa } async function resolveFreshCodexConnection(connectionId: string): Promise { - const connection = (await getProviderConnectionById(connectionId)) as CodexConnectionLike | null; + const connection = (await getCachedProviderConnectionById(connectionId)) as CodexConnectionLike | null; if (!connection) { throw new CodexAuthFileError("Connection not found", 404, "not_found"); } diff --git a/src/lib/quota/connectionProvider.ts b/src/lib/quota/connectionProvider.ts index 5078105a10..3255bffad6 100644 --- a/src/lib/quota/connectionProvider.ts +++ b/src/lib/quota/connectionProvider.ts @@ -18,9 +18,9 @@ export async function resolveConnectionProvider(connectionId: string): Promise { try { // Lazy import — avoids circular deps and keeps the module loadable without a full DB. - const { getProviderConnectionById } = await import("@/lib/localDb"); - if (typeof getProviderConnectionById === "function") { - const conn = await getProviderConnectionById(connectionId); + const { getCachedProviderConnectionById } = await import("@/lib/localDb"); + if (typeof getCachedProviderConnectionById === "function") { + const conn = await getCachedProviderConnectionById(connectionId); if (conn && typeof (conn as { provider?: string }).provider === "string") { return (conn as { provider: string }).provider; } diff --git a/src/lib/quota/quotaCombos.ts b/src/lib/quota/quotaCombos.ts index 4d4c17863b..25be9cca00 100644 --- a/src/lib/quota/quotaCombos.ts +++ b/src/lib/quota/quotaCombos.ts @@ -12,7 +12,7 @@ import { getPool } from "@/lib/db/quotaPools"; import { getGroupName } from "@/lib/db/quotaGroups"; -import { getProviderConnectionById } from "@/lib/db/providers"; +import { getCachedProviderConnectionById } from "@/lib/localDb"; import { getCombos, createCombo, @@ -152,7 +152,7 @@ export async function syncQuotaCombos(poolId: string): Promise { for (const connId of pool.connectionIds) { let connection: Record | null = null; try { - connection = (await getProviderConnectionById(connId)) as Record | null; + connection = (await getCachedProviderConnectionById(connId)) as Record | null; } catch { // Connection lookup failure — skip this connection. continue; @@ -361,7 +361,7 @@ export async function removeQuotaCombosForPool(poolId: string): Promise { let poolProvider: string | undefined; for (const connId of pool.connectionIds) { try { - const connection = (await getProviderConnectionById(connId)) as Record< + const connection = (await getCachedProviderConnectionById(connId)) as Record< string, unknown > | null; diff --git a/src/lib/quota/quotaKey.ts b/src/lib/quota/quotaKey.ts index ff4d3636b1..7207798bcc 100644 --- a/src/lib/quota/quotaKey.ts +++ b/src/lib/quota/quotaKey.ts @@ -8,7 +8,7 @@ */ import { getPool, getPoolsByGroup } from "@/lib/db/quotaPools"; -import { getProviderConnectionById } from "@/lib/db/providers"; +import { getCachedProviderConnectionById } from "@/lib/localDb"; import { getApiKeyById, updateApiKeyPermissions } from "@/lib/db/apiKeys"; import { quotaGroupSlug } from "./quotaModelNaming"; import { getGroupName } from "@/lib/db/quotaGroups"; @@ -114,7 +114,7 @@ export async function resolveQuotaKeyScope( : [groupPool.connectionId]; for (const connId of connIds) { - const connection = await getProviderConnectionById(connId); + const connection = await getCachedProviderConnectionById(connId); if (!connection) continue; // missing connection contributes nothing const provider = (connection as Record).provider; diff --git a/src/lib/quota/saturationSignals.ts b/src/lib/quota/saturationSignals.ts index 4ec9a8baf1..50b6b467da 100644 --- a/src/lib/quota/saturationSignals.ts +++ b/src/lib/quota/saturationSignals.ts @@ -368,13 +368,13 @@ export function __setAnthropicSaturationDepsForTests( } async function defaultAnthropicDeps(): Promise { - const [providersMod, usageMod] = await Promise.all([ - import("@/lib/db/providers"), + const [localDbMod, usageMod] = await Promise.all([ + import("@/lib/localDb"), import("@omniroute/open-sse/services/usage"), ]); return { loadConnection: (connectionId) => - providersMod.getProviderConnectionById(connectionId) as Promise | null>, diff --git a/src/lib/sync/bundle.ts b/src/lib/sync/bundle.ts index 1780948050..674105f552 100644 --- a/src/lib/sync/bundle.ts +++ b/src/lib/sync/bundle.ts @@ -4,7 +4,7 @@ import { getCombos, getModelAliases, getProviderConnections, - getProviderNodes, + getCachedProviderNodes, getSettings, } from "@/lib/localDb"; @@ -159,7 +159,7 @@ export async function buildConfigSyncBundle(): Promise { await Promise.all([ getSettings(), getProviderConnections(), - getProviderNodes(), + getCachedProviderNodes(), getModelAliases(), getCombos(), getApiKeys(), diff --git a/src/lib/tokenHealthCheck.ts b/src/lib/tokenHealthCheck.ts index b21b91005c..58ad8e64af 100644 --- a/src/lib/tokenHealthCheck.ts +++ b/src/lib/tokenHealthCheck.ts @@ -13,7 +13,7 @@ import { getProviderConnections, - getProviderConnectionById, + getCachedProviderConnectionById, updateProviderConnection, getSettings, resolveProxyForConnection, @@ -355,7 +355,7 @@ async function sweep() { export async function checkConnection(conn) { if (!conn?.id) return; - const latestConnection = (await getProviderConnectionById(conn.id)) || conn; + const latestConnection = (await getCachedProviderConnectionById(conn.id)) || conn; conn = latestConnection; // Per-provider opt-out of proactive refresh (e.g. Codex/OpenAI cascade @@ -644,7 +644,7 @@ export async function checkConnection(conn) { // Once used, the old token is permanently invalidated. // Retrying will never succeed → deactivate and stop the loop. if (isUnrecoverableRefreshError(result)) { - const currentConnection = await getProviderConnectionById(conn.id); + const currentConnection = await getCachedProviderConnectionById(conn.id); const credentialsChangedSinceSweep = !!currentConnection && (currentConnection.refreshToken !== attemptedRefreshToken || @@ -759,7 +759,7 @@ export async function checkConnection(conn) { // providerSpecificData.copilotTokenExpiresAt (Unix seconds). if (String(conn.provider || "").toLowerCase() === "github") { // Re-read the latest connection after the OAuth refresh (onPersist may have updated it). - const latestConn = (await getProviderConnectionById(conn.id).catch(() => null)) || conn; + const latestConn = (await getCachedProviderConnectionById(conn.id).catch(() => null)) || conn; const accessTokenForCopilot = result.accessToken || latestConn.accessToken; if (accessTokenForCopilot) { diff --git a/src/sse/services/auth.ts b/src/sse/services/auth.ts index 1d3b3f31a4..a37e007cdc 100644 --- a/src/sse/services/auth.ts +++ b/src/sse/services/auth.ts @@ -1,7 +1,7 @@ import { randomUUID, createHash } from "crypto"; import { getProviderConnections, - getProviderNodes, + getCachedProviderNodes, validateApiKey, updateProviderConnection, clearConnectionErrorIfUnchanged, @@ -984,7 +984,7 @@ async function getProviderSearchPool(provider: string): Promise { // (for example "78code/gpt-5.4"), but live credentials are stored under // internal provider ids like openai-compatible-responses-. try { - const providerNodes = await getProviderNodes(); + const providerNodes = await getCachedProviderNodes(); for (const node of Array.isArray(providerNodes) ? providerNodes : []) { const nodeRecord = asRecord(node); const nodePrefix = typeof nodeRecord.prefix === "string" ? nodeRecord.prefix.trim() : ""; diff --git a/src/sse/services/model.ts b/src/sse/services/model.ts index 9c8bbfe134..8186323b73 100644 --- a/src/sse/services/model.ts +++ b/src/sse/services/model.ts @@ -4,7 +4,7 @@ import { getComboByName, getComboById, getComboByNameInsensitive, - getProviderNodes, + getCachedProviderNodes, getCustomModels, } from "@/lib/localDb"; import { getCachedSettings } from "@/lib/localDb"; @@ -144,7 +144,7 @@ export async function getModelInfo(modelStr) { // Match by node.prefix (user-defined alias) OR node.id (internal UUID id stored by // combo steps), so that combo targets using the internal node id still resolve // correctly (#2778). - const openaiNodes = await getProviderNodes({ type: "openai-compatible" }); + const openaiNodes = await getCachedProviderNodes({ type: "openai-compatible" }); const matchedOpenAI = openaiNodes.find( (node) => node.prefix === prefixToCheck || node.id === prefixToCheck ); @@ -167,7 +167,7 @@ export async function getModelInfo(modelStr) { } // Check Anthropic Compatible nodes - const anthropicNodes = await getProviderNodes({ type: "anthropic-compatible" }); + const anthropicNodes = await getCachedProviderNodes({ type: "anthropic-compatible" }); const matchedAnthropic = anthropicNodes.find( (node) => node.prefix === prefixToCheck || node.id === prefixToCheck );