mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-02 13:22:11 +03:00
feat: add getCachedProviderConnectionById and getCachedProviderNodes with 38-file callers conversion
PR #2 of memory-pressure series — cache the two remaining hot database access patterns that were uncached: Core changes: - readCache.ts: add connectionByIdCache, nodesCache, getCachedProviderConnectionById, getCachedProviderNodes, extend invalidateDbCache for "nodes" scope - nodes.ts: invalidate nodes cache on create/update/delete - localDb.ts: export new cache functions, remove dead code exports (isConnectionRateLimited, getRateLimitedConnections) Callers converted (38 files): - open-sse hot paths: chatCore.ts, codexFailover.ts, concurrencyCaps.ts, quotaExhaustionCutoff.ts, tokenRefresh.ts - src hot paths: quotaCache.ts, connectionProvider.ts, quotaCombos.ts, quotaKey.ts, saturationSignals.ts, tokenHealthCheck.ts, providerHealthAutopilot.ts, claudeAuthFile.ts, codexAuthFile.ts - Admin routes: providers/[id]/{route,login,models,refresh,sync-models,test}.ts - Nodes/export/sync: provider-nodes/route.ts, export-json/route.ts, sync/bundle.ts, localHealthCheck.ts - v1 audio/speech/route.ts, transcriptions/route.ts, translations/route.ts - v1 models/catalog.ts, rerank/route.ts - embeddings/service.ts, imageRouteModel.ts, memory/embedding/index.ts - sse/services/auth.ts, model.ts All cached wrappers use 5s TTL (matching existing pattern). Cache invalidated on all related writes. Both core and open-sse typechecks pass (zero errors).
This commit is contained in:
@@ -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?.(
|
||||
|
||||
@@ -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<void> {
|
||||
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);
|
||||
|
||||
@@ -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<number | null> {
|
||||
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 {
|
||||
|
||||
@@ -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<string, unknown> | undefined;
|
||||
try {
|
||||
connection = (await getProviderConnectionById(connectionId)) as
|
||||
connection = (await getCachedProviderConnectionById(connectionId)) as
|
||||
| Record<string, unknown>
|
||||
| undefined;
|
||||
} catch {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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<string, any> | null;
|
||||
const existing = (await getCachedProviderConnectionById(id)) as Record<string, any> | 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<string, any> | null;
|
||||
const connection = (await getCachedProviderConnectionById(id)) as Record<string, any> | null;
|
||||
if (!connection) {
|
||||
return NextResponse.json({ error: "Connection not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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<object>} 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 };
|
||||
|
||||
@@ -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<string, unknown> | 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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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<typeof buildDynamicAudioProvider>[] = [];
|
||||
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;
|
||||
|
||||
@@ -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<typeof buildDynamicAudioProvider>[] = [];
|
||||
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;
|
||||
|
||||
@@ -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<typeof buildDynamicAudioProvider>[] = [];
|
||||
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;
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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<typeof buildDynamicRerankProvider>[] = [];
|
||||
try {
|
||||
const nodes = await getProviderNodes();
|
||||
const nodes = await getCachedProviderNodes();
|
||||
localProviders = (Array.isArray(nodes) ? nodes : [])
|
||||
.filter((n: any) => {
|
||||
try {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<TRow = unknown> {
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -107,6 +107,43 @@ export async function getCachedProviderConnections(
|
||||
connectionsCache.set("all", value);
|
||||
return value;
|
||||
}
|
||||
const connectionByIdCache = new TTLCache<Record<string, unknown> | null>(CONNECTIONS_TTL_MS);
|
||||
const nodesCache = new TTLCache<unknown[]>(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<Record<string, unknown> | 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<string, unknown>
|
||||
): Promise<unknown[]> {
|
||||
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,
|
||||
|
||||
@@ -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<typeof buildDynamicEmbeddingProvider>[] = [];
|
||||
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 &&
|
||||
|
||||
@@ -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 `<nodeId>/<model>` form.
|
||||
@@ -36,7 +36,7 @@ export async function resolveImageModelPrefix(modelStr: string): Promise<string>
|
||||
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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<void> {
|
||||
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<string, unknown>) =>
|
||||
typeof n.baseUrl === "string" && isLocalhostUrl(n.baseUrl as string)
|
||||
|
||||
@@ -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<EmbeddingProviderListing
|
||||
// Get dynamic local providers
|
||||
let dynamicProviders: ReturnType<typeof buildDynamicEmbeddingProvider>[] = [];
|
||||
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"];
|
||||
|
||||
@@ -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<string, unknown>;
|
||||
@@ -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" } };
|
||||
}
|
||||
|
||||
@@ -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<ClaudeConnectionLike> {
|
||||
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");
|
||||
}
|
||||
|
||||
@@ -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<CodexConnectionLike> {
|
||||
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");
|
||||
}
|
||||
|
||||
@@ -18,9 +18,9 @@
|
||||
export async function resolveConnectionProvider(connectionId: string): Promise<string> {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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<void> {
|
||||
for (const connId of pool.connectionIds) {
|
||||
let connection: Record<string, unknown> | null = null;
|
||||
try {
|
||||
connection = (await getProviderConnectionById(connId)) as Record<string, unknown> | null;
|
||||
connection = (await getCachedProviderConnectionById(connId)) as Record<string, unknown> | null;
|
||||
} catch {
|
||||
// Connection lookup failure — skip this connection.
|
||||
continue;
|
||||
@@ -361,7 +361,7 @@ export async function removeQuotaCombosForPool(poolId: string): Promise<void> {
|
||||
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;
|
||||
|
||||
@@ -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<string, unknown>).provider;
|
||||
|
||||
@@ -368,13 +368,13 @@ export function __setAnthropicSaturationDepsForTests(
|
||||
}
|
||||
|
||||
async function defaultAnthropicDeps(): Promise<AnthropicSaturationDeps> {
|
||||
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<Record<
|
||||
localDbMod.getCachedProviderConnectionById(connectionId) as Promise<Record<
|
||||
string,
|
||||
unknown
|
||||
> | null>,
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
getCombos,
|
||||
getModelAliases,
|
||||
getProviderConnections,
|
||||
getProviderNodes,
|
||||
getCachedProviderNodes,
|
||||
getSettings,
|
||||
} from "@/lib/localDb";
|
||||
|
||||
@@ -159,7 +159,7 @@ export async function buildConfigSyncBundle(): Promise<ConfigSyncBundle> {
|
||||
await Promise.all([
|
||||
getSettings(),
|
||||
getProviderConnections(),
|
||||
getProviderNodes(),
|
||||
getCachedProviderNodes(),
|
||||
getModelAliases(),
|
||||
getCombos(),
|
||||
getApiKeys(),
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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<string[]> {
|
||||
// (for example "78code/gpt-5.4"), but live credentials are stored under
|
||||
// internal provider ids like openai-compatible-responses-<uuid>.
|
||||
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() : "";
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user