UNPKG

@juspay/neurolink

Version:

The pipe layer of an AI nervous system: one interface connecting provider neurons to your application, across three inference types — generate, stream and decide. `decide` returns typed, calibrated judgements from a non-generative model (~400ms, ~$0.00002

483 lines (482 loc) • 19.7 kB
import { createHash } from "node:crypto"; import { watchFile, unwatchFile } from "node:fs"; import { readFile } from "node:fs/promises"; import { createAccountAllowlist, ENV_ANTHROPIC_ACCOUNT_KEY, isAccountAllowed, LEGACY_ANTHROPIC_ACCOUNT_KEY, normalizeAnthropicAccountKey, } from "./accountSelection.js"; import { ModelRouter } from "./modelRouter.js"; import { loadProxyConfig } from "./proxyConfig.js"; import { logger } from "../utils/logger.js"; const DEFAULT_WATCH_INTERVAL_MS = 1_000; const DEFAULT_WATCH_DEBOUNCE_MS = 100; const DEFAULT_SESSION_SOFT_LIMIT = 0.97; const DEFAULT_SESSION_RESET_TOLERANCE_MS = 15 * 60 * 1_000; function errorCode(error) { let current = error; for (let depth = 0; depth < 4; depth += 1) { if (!current || typeof current !== "object") { return undefined; } const code = current.code; if (typeof code === "string") { return code; } current = current.cause; } return undefined; } function isMissingFileError(error) { return errorCode(error) === "ENOENT"; } function normalizeStrategy(configured, override) { if (override) { return override; } return configured === "round-robin" ? "round-robin" : "fill-first"; } function resolveQuotaRoutingEnabled(envValue, configured) { if (envValue === undefined) { return configured ?? true; } const normalized = envValue.trim().toLowerCase(); return normalized !== "off" && normalized !== "false" && normalized !== "0"; } function resolveCodexOutboundFallbackEnabled(envValue, configured) { if (envValue === undefined || envValue.trim() === "") { // Unlike resolveQuotaRoutingEnabled's `configured ?? true`, this defaults // false — required so today's behavior is byte-identical when unconfigured. // An empty/whitespace-only value (e.g. `FOO=` in a .env file) is treated // the same as unset, not as an explicit opt-in. return configured ?? false; } const normalized = envValue.trim().toLowerCase(); return normalized !== "off" && normalized !== "false" && normalized !== "0"; } function resolveSessionSoftLimit(envValue, configured, rejectInvalid) { if (envValue !== undefined) { const parsed = Number(envValue); if (Number.isFinite(parsed) && parsed > 0 && parsed <= 1) { return parsed; } const message = `Invalid NEUROLINK_PROXY_SESSION_SOFT_LIMIT=${envValue}; ` + "expected number in (0, 1]"; if (rejectInvalid) { throw new Error(message); } logger.warn(`[proxy-config] ${message}; using configured/default value`); } return configured ?? DEFAULT_SESSION_SOFT_LIMIT; } function resolveSessionResetToleranceMs(envValue, configured, rejectInvalid) { if (envValue !== undefined) { const parsed = Number(envValue); if (Number.isInteger(parsed) && parsed > 0) { return parsed; } const message = `Invalid NEUROLINK_PROXY_SESSION_RESET_TOLERANCE_MS=${envValue}; ` + "expected positive integer"; if (rejectInvalid) { throw new Error(message); } logger.warn(`[proxy-config] ${message}; using configured/default value`); } return configured ?? DEFAULT_SESSION_RESET_TOLERANCE_MS; } async function resolvePrimaryAccountKey(primaryEmail) { const trimmed = primaryEmail?.trim(); if (!trimmed) { return undefined; } const key = normalizeAnthropicAccountKey(trimmed); try { const { tokenStore } = await import("../auth/tokenStore.js"); const known = await tokenStore.listByPrefix("anthropic:"); if (!known.some((knownKey) => normalizeAnthropicAccountKey(knownKey) === key)) { logger.warn(`[proxy] WARN: configured routing.primaryAccount=${trimmed} not found in token store; it will activate after authentication`); } } catch (error) { logger.debug(`[proxy] could not validate primary account against token store: ${error instanceof Error ? error.message : String(error)}`); } return key; } async function resolveAccountAllowlist(configuredAccounts) { const allowlist = createAccountAllowlist(configuredAccounts); if (allowlist === undefined) { return undefined; } if (allowlist.size === 0) { logger.warn("[proxy] routing.accountAllowlist is empty; all stored Anthropic credentials are denied"); return allowlist; } try { const { tokenStore } = await import("../auth/tokenStore.js"); const known = new Set((await tokenStore.listByPrefix("anthropic:")).map(normalizeAnthropicAccountKey)); known.add(LEGACY_ANTHROPIC_ACCOUNT_KEY); known.add(ENV_ANTHROPIC_ACCOUNT_KEY); for (const key of allowlist) { if (!known.has(key)) { logger.warn(`[proxy] WARN: routing.accountAllowlist entry ${key} is allowed but not currently authenticated`); } } } catch (error) { logger.debug(`[proxy] could not validate account allowlist against token store: ${error instanceof Error ? error.message : String(error)}`); } return allowlist; } function cloneLoadedConfig(loaded) { if (!loaded) { return null; } const routing = loaded.routing; if (!routing) { return Object.freeze({}); } return Object.freeze({ routing: Object.freeze({ ...routing, ...(routing.modelMappings ? { modelMappings: Object.freeze(routing.modelMappings.map((entry) => Object.freeze({ ...entry }))), } : {}), ...(routing.fallbackChain ? { fallbackChain: Object.freeze(routing.fallbackChain.map((entry) => Object.freeze({ ...entry }))), } : {}), ...(routing.passthroughModels ? { passthroughModels: Object.freeze([...routing.passthroughModels]) } : {}), ...(routing.accountAllowlist ? { accountAllowlist: Object.freeze([...routing.accountAllowlist]) } : {}), }), }); } function assertResolvedRoutingValues(value, path = "routing") { if (typeof value === "string") { if (value.includes("${")) { throw new Error(`Unresolved environment placeholder at ${path}`); } return; } if (Array.isArray(value)) { value.forEach((entry, index) => assertResolvedRoutingValues(entry, `${path}[${index}]`)); return; } if (value && typeof value === "object") { for (const [key, entry] of Object.entries(value)) { assertResolvedRoutingValues(entry, `${path}.${key}`); } } } async function buildCandidate(options, generation, allowMissingConfig, allowMissingEnvFile, rejectInvalidHotEnv) { const effectiveEnv = { ...options.baseEnv }; let envFilePresent = false; let envFileValues = {}; if (options.envFilePath) { try { const envFileContent = await readFile(options.envFilePath, "utf8"); const { parse } = await import("dotenv"); envFileValues = parse(envFileContent); Object.assign(effectiveEnv, envFileValues); envFilePresent = true; } catch (error) { if (!isMissingFileError(error) || options.envFileRequired || !allowMissingEnvFile) { throw new Error(`Failed to load proxy env file ${options.envFilePath}: ${error instanceof Error ? error.message : String(error)}`, { cause: error }); } } } let loadedConfig = null; let configFilePresent = false; try { loadedConfig = (await loadProxyConfig(options.configPath, { env: effectiveEnv, })); configFilePresent = true; } catch (error) { if (!isMissingFileError(error) || options.configRequired || !allowMissingConfig) { throw error; } } const proxyConfig = cloneLoadedConfig(loadedConfig); const routing = proxyConfig?.routing; assertResolvedRoutingValues(routing); const strategy = normalizeStrategy(routing?.strategy, options.strategyOverride); const primaryAccountKey = await resolvePrimaryAccountKey(routing?.primaryAccount); const accountAllowlist = await resolveAccountAllowlist(routing?.accountAllowlist); if (primaryAccountKey && !isAccountAllowed(primaryAccountKey, accountAllowlist)) { throw new Error(`Configured routing.primaryAccount=${routing?.primaryAccount} is excluded by routing.accountAllowlist`); } const quotaRoutingEnabled = resolveQuotaRoutingEnabled(effectiveEnv.NEUROLINK_PROXY_QUOTA_ROUTING, routing?.quotaRouting); const sessionSoftLimit = resolveSessionSoftLimit(effectiveEnv.NEUROLINK_PROXY_SESSION_SOFT_LIMIT, routing?.sessionSoftLimit, rejectInvalidHotEnv); const sessionResetToleranceMs = resolveSessionResetToleranceMs(effectiveEnv.NEUROLINK_PROXY_SESSION_RESET_TOLERANCE_MS, routing?.sessionResetToleranceMs, rejectInvalidHotEnv); const modelRouter = routing ? new ModelRouter({ strategy, modelMappings: routing.modelMappings ?? [], fallbackChain: routing.fallbackChain ?? [], autoFallback: routing.autoFallback, maxInflightPerAccount: routing.maxInflightPerAccount, passthroughModels: routing.passthroughModels, quotaRouting: routing.quotaRouting, sessionSoftLimit: routing.sessionSoftLimit, sessionResetToleranceMs: routing.sessionResetToleranceMs, primaryAccount: routing.primaryAccount, accountAllowlist: routing.accountAllowlist, }) : undefined; const envFileHash = createHash("sha256") .update(envFilePresent ? JSON.stringify(Object.entries(envFileValues).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0)) : "[missing]") .digest("hex") .slice(0, 16); const useOverage = routing?.useOverage ?? "auto"; const accountRanking = routing?.accountRanking ?? "expiry-first"; const preferPrimary = routing?.preferPrimary ?? false; const sessionAffinity = routing?.sessionAffinity ?? false; const sessionAffinityIdleTtlMs = routing?.sessionAffinityIdleTtlMs ?? 3_600_000; const spillInflight = routing?.spillInflight ?? 0; const codexOutboundFallbackEnabled = resolveCodexOutboundFallbackEnabled(effectiveEnv.NEUROLINK_PROXY_CODEX_OUTBOUND_FALLBACK, routing?.codexOutboundFallbackEnabled); const codexOutboundFallbackTargets = routing?.codexOutboundFallbackTargets ?? []; const codexOutboundFallbackModelMappings = routing?.codexOutboundFallbackModelMappings ?? []; const fingerprintSource = JSON.stringify({ strategy, passthrough: options.passthrough, routing: routing ?? null, primaryAccountKey, accountAllowlist: accountAllowlist ? [...accountAllowlist].sort() : null, quotaRoutingEnabled, sessionSoftLimit, sessionResetToleranceMs, useOverage, accountRanking, preferPrimary, sessionAffinity, sessionAffinityIdleTtlMs, spillInflight, codexOutboundFallbackEnabled, codexOutboundFallbackTargets, codexOutboundFallbackModelMappings, }); const configHash = createHash("sha256") .update(fingerprintSource) .digest("hex") .slice(0, 16); return { snapshot: Object.freeze({ generation, loadedAt: new Date().toISOString(), configHash, proxyConfig, strategy, modelRouter, passthrough: options.passthrough, primaryAccountKey, accountAllowlist, quotaRoutingEnabled, sessionSoftLimit, sessionResetToleranceMs, useOverage, accountRanking, preferPrimary, sessionAffinity, sessionAffinityIdleTtlMs, spillInflight, codexOutboundFallbackEnabled, codexOutboundFallbackTargets, codexOutboundFallbackModelMappings, }), configFilePresent, envFilePresent, envFileHash, }; } /** Atomic last-known-good runtime configuration with file-triggered reloads. */ export class ProxyRuntimeConfigStore { options; currentSnapshot; status; listeners = new Set(); reloadListeners = new Set(); reloadQueue = Promise.resolve(); reloadTimer; configFileObserved; envFileObserved; currentEnvFileHash; watchListener = (current, previous) => { if (current.mtimeMs === previous.mtimeMs && current.size === previous.size && current.nlink === previous.nlink) { return; } this.scheduleWatchReload(); }; constructor(options, snapshot, configFileObserved, envFileObserved, envFileHash) { this.options = { ...options, baseEnv: { ...options.baseEnv } }; this.currentSnapshot = snapshot; this.configFileObserved = configFileObserved; this.envFileObserved = envFileObserved; this.currentEnvFileHash = envFileHash; this.status = { configPath: options.configPath, ...(options.envFilePath ? { envFilePath: options.envFilePath } : {}), generation: snapshot.generation, loadedAt: snapshot.loadedAt, configHash: snapshot.configHash, watching: false, consecutiveFailures: 0, }; } static async create(options) { const candidate = await buildCandidate(options, 1, true, true, false); return new ProxyRuntimeConfigStore(options, candidate.snapshot, candidate.configFilePresent, candidate.envFilePresent, candidate.envFileHash); } getSnapshot() { return this.currentSnapshot; } getStatus() { return { ...this.status }; } subscribe(listener) { this.listeners.add(listener); return () => this.listeners.delete(listener); } subscribeReload(listener) { this.reloadListeners.add(listener); return () => this.reloadListeners.delete(listener); } reload(source = "manual") { const run = this.reloadQueue.then(() => this.performReload(source), () => this.performReload(source)); this.reloadQueue = run.then(() => undefined, () => undefined); return run; } startWatching() { if (this.status.watching) { return; } const interval = Math.max(50, this.options.watchIntervalMs ?? DEFAULT_WATCH_INTERVAL_MS); watchFile(this.options.configPath, { interval, persistent: false }, this.watchListener); if (this.options.envFilePath) { watchFile(this.options.envFilePath, { interval, persistent: false }, this.watchListener); } this.status = { ...this.status, watching: true }; } stopWatching() { if (!this.status.watching) { return; } unwatchFile(this.options.configPath, this.watchListener); if (this.options.envFilePath) { unwatchFile(this.options.envFilePath, this.watchListener); } if (this.reloadTimer) { clearTimeout(this.reloadTimer); this.reloadTimer = undefined; } this.status = { ...this.status, watching: false }; } scheduleWatchReload() { if (this.reloadTimer) { clearTimeout(this.reloadTimer); } this.reloadTimer = setTimeout(() => { this.reloadTimer = undefined; void this.reload("watch"); }, Math.max(0, this.options.watchDebounceMs ?? DEFAULT_WATCH_DEBOUNCE_MS)); this.reloadTimer.unref?.(); } async performReload(source) { const attemptedAt = new Date().toISOString(); this.status = { ...this.status, lastReloadAttemptAt: attemptedAt, lastReloadSource: source, }; try { const candidate = await buildCandidate(this.options, this.currentSnapshot.generation + 1, !this.configFileObserved, !this.envFileObserved, true); this.configFileObserved ||= candidate.configFilePresent; this.envFileObserved ||= candidate.envFilePresent; if (candidate.snapshot.configHash === this.currentSnapshot.configHash && candidate.envFileHash === this.currentEnvFileHash) { this.status = { ...this.status, lastReloadAt: attemptedAt, lastReloadError: undefined, consecutiveFailures: 0, }; const result = { applied: true, changed: false, generation: this.currentSnapshot.generation, }; this.notifyReloadListeners(result); return result; } const environmentChanged = candidate.envFileHash !== this.currentEnvFileHash; this.currentSnapshot = candidate.snapshot; this.currentEnvFileHash = candidate.envFileHash; this.status = { ...this.status, generation: candidate.snapshot.generation, loadedAt: candidate.snapshot.loadedAt, configHash: candidate.snapshot.configHash, lastReloadAt: attemptedAt, lastReloadError: undefined, consecutiveFailures: 0, }; logger.always(`[proxy] configuration generation ${candidate.snapshot.generation} applied via ${source}`); for (const listener of this.listeners) { try { listener(candidate.snapshot); } catch (error) { logger.warn(`[proxy] runtime config listener failed: ${error instanceof Error ? error.message : String(error)}`); } } const result = { applied: true, changed: true, generation: candidate.snapshot.generation, ...(environmentChanged ? { environmentChanged: true } : {}), }; this.notifyReloadListeners(result); return result; } catch (error) { const message = error instanceof Error ? error.message : String(error); this.status = { ...this.status, lastReloadError: message, consecutiveFailures: this.status.consecutiveFailures + 1, }; logger.warn(`[proxy] configuration reload rejected; keeping generation ${this.currentSnapshot.generation}: ${message}`); const result = { applied: false, changed: false, generation: this.currentSnapshot.generation, error: message, }; this.notifyReloadListeners(result); return result; } } notifyReloadListeners(result) { for (const listener of this.reloadListeners) { try { listener(result, this.getStatus()); } catch (error) { logger.warn(`[proxy] runtime config reload listener failed: ${error instanceof Error ? error.message : String(error)}`); } } } }