UNPKG

@juspay/neurolink

Version:

Universal AI Development Platform with working MCP integration, multi-provider support, voice (TTS/STT/realtime), and professional CLI. 58+ external MCP servers discoverable, multimodal file processing, RAG pipelines. Build, test, and deploy AI applicatio

666 lines 25.7 kB
/** * Enhanced proxy-aware fetch implementation for AI SDK providers * Supports HTTP/HTTPS, SOCKS4/5, authentication, and NO_PROXY bypass * Lightweight implementation extracted from research of major proxy packages */ import { logger } from "../utils/logger.js"; import { SpanStatusCode, propagation, context } from "@opentelemetry/api"; import { tracers } from "../telemetry/tracers.js"; import { shouldBypassProxy } from "./utils/noProxyUtils.js"; import { createHash } from "node:crypto"; async function getLangfuseContext() { try { // Dynamic import to avoid hard dependency — getLangfuseContext is only // available when the observability module is loaded. const mod = await import("../services/server/ai/observability/instrumentation.js"); return mod.getLangfuseContext?.(); } catch { return undefined; } } /** * Inject OTel trace context (traceparent/tracestate) and NeuroLink session context * into outgoing request headers. This enables: * - The NeuroLink proxy to link proxy spans as children of the calling SDK's trace * - Conversation-level session/user attribution on proxy spans */ function mergeTraceHeaders(input, init) { const existingHeaders = new Headers(input instanceof Request ? input.headers : undefined); if (init?.headers) { const initHeaders = new Headers(init.headers); for (const [key, value] of initHeaders.entries()) { existingHeaders.set(key, value); } } return existingHeaders; } async function injectTraceContext(input, init) { const carrier = {}; propagation.inject(context.active(), carrier); // Also inject NeuroLink session context from Langfuse AsyncLocalStorage const langfuseContext = await getLangfuseContext(); if (langfuseContext?.sessionId) { carrier["x-neurolink-session-id"] = langfuseContext.sessionId; } if (langfuseContext?.userId) { carrier["x-neurolink-user-id"] = langfuseContext.userId; } if (langfuseContext?.conversationId) { carrier["x-neurolink-conversation-id"] = langfuseContext.conversationId; } if (Object.keys(carrier).length === 0) { return init ?? {}; } const existingHeaders = mergeTraceHeaders(input, init); for (const [key, value] of Object.entries(carrier)) { if (!existingHeaders.has(key)) { existingHeaders.set(key, value); } } return { ...init, headers: existingHeaders }; } const fetchTracer = tracers.http; /** * Extract hostname from a URL string for safe logging (no auth tokens or paths). * Returns "[unknown]" if parsing fails. */ function extractHostname(url) { try { const urlStr = typeof url === "string" ? url : url instanceof URL ? url.href : url.url; const parsed = new URL(urlStr); return parsed.hostname; } catch { return "[unknown]"; } } /** * Retry-aware fetch wrapper for transient network errors (ECONNRESET, ETIMEDOUT, socket hang up). * Protects all LLM API calls and token refreshes that go through createProxyFetch(). * Instrumented with OpenTelemetry spans for retry visibility. */ async function fetchWithRetry(url, init, maxRetries = 3, baseDelay = 500) { const hostname = extractHostname(url); return fetchTracer.startActiveSpan("neurolink.http.fetchWithRetry", async (span) => { span.setAttribute("http.request.max_retries", maxRetries); span.setAttribute("http.request.hostname", hostname); span.setAttribute("http.request.method", init?.method || "GET"); // eslint-disable-next-line no-useless-assignment let totalAttempts = 0; try { for (let attempt = 0; attempt <= maxRetries; attempt++) { totalAttempts = attempt + 1; try { const response = await fetch(url, init); // Record success attributes span.setAttribute("http.request.total_attempts", totalAttempts); span.setAttribute("http.response.status_code", response.status); span.setStatus({ code: SpanStatusCode.OK }); return response; } catch (error) { const err = error; const isRetryable = err?.code === "ECONNRESET" || err?.code === "ETIMEDOUT" || err?.message?.includes("socket hang up") || err?.message?.includes("network socket disconnected"); if (!isRetryable || attempt === maxRetries) { // Final failure — record on span and rethrow span.setAttribute("http.request.total_attempts", totalAttempts); span.setStatus({ code: SpanStatusCode.ERROR, message: err?.message || err?.code || "fetchWithRetry final failure", }); span.recordException(error instanceof Error ? error : new Error(String(error))); throw error; } // Transient error — add retry event and continue loop const delay = baseDelay * Math.pow(2, attempt); span.addEvent("http.request.retry", { "retry.attempt": attempt + 1, "retry.delay_ms": delay, "retry.error": (err?.code || err?.message || String(error)).slice(0, 256), }); logger.debug(`[fetchWithRetry] Transient error (${err?.code || err?.message}), retrying in ${delay}ms (attempt ${attempt + 1}/${maxRetries})`); await new Promise((r) => setTimeout(r, delay)); } } throw new Error("fetchWithRetry exhausted"); // unreachable } finally { span.end(); } }); } /** * Parse request body to readable format for debug logging */ function parseBody(body) { if (!body) { return { parsed: null, size: 0, type: "empty" }; } if (typeof body === "string") { try { return { parsed: JSON.parse(body), size: body.length, type: "json" }; } catch { return { parsed: body, size: body.length, type: "text" }; } } if (body instanceof ArrayBuffer) { return { parsed: "[ArrayBuffer]", size: body.byteLength, type: "arraybuffer", }; } if (body instanceof Uint8Array) { return { parsed: "[Uint8Array]", size: body.length, type: "uint8array" }; } return { parsed: "[Stream]", size: -1, type: "stream" }; } /** * Sensitive header names whose values should be redacted in logs */ const SENSITIVE_HEADERS = new Set([ "authorization", "x-api-key", "api-key", "x-goog-api-key", "proxy-authorization", "cookie", "set-cookie", ]); /** * Clone response and read body + headers for debug logging */ async function readResponseBody(response) { const headers = {}; response.headers.forEach((value, key) => { headers[key] = SENSITIVE_HEADERS.has(key.toLowerCase()) ? `${value.substring(0, 4)}***` : value; }); try { const cloned = response.clone(); const text = await cloned.text(); try { return { parsed: JSON.parse(text), size: text.length, type: "json", headers, }; } catch { return { parsed: text, size: text.length, type: "text", headers }; } } catch { return { parsed: "[unable to read body]", size: -1, type: "error", headers, }; } } // ==================== LIGHTWEIGHT PROXY IMPLEMENTATIONS ==================== // ParsedProxyConfig interface moved to ../types/utilities.js /** * Parse proxy URL with authentication support */ function parseProxyUrl(proxyUrl) { try { const url = new URL(proxyUrl); const config = { protocol: url.protocol, hostname: url.hostname, port: parseInt(url.port) || getDefaultPort(url.protocol), cleanUrl: `${url.protocol}//${url.hostname}:${url.port || getDefaultPort(url.protocol)}`, }; // Extract authentication if present if (url.username && url.password) { config.auth = { username: decodeURIComponent(url.username), password: decodeURIComponent(url.password), }; } return config; } catch (error) { // Sanitize proxy URL to avoid leaking credentials in logs/errors let safeUrl; try { const u = new URL(proxyUrl); u.username = ""; u.password = ""; safeUrl = u.toString(); } catch { safeUrl = "[invalid-url]"; } logger.error("[Proxy] Failed to parse proxy URL", { proxyUrl: safeUrl, error, }); throw new Error(`Invalid proxy URL: ${safeUrl}`, { cause: error }); } } /** * Get default port for protocol */ function getDefaultPort(protocol) { switch (protocol) { case "http:": return 8080; case "https:": return 8080; case "socks4:": return 1080; case "socks5:": return 1080; default: return 8080; } } /** * Select appropriate proxy URL based on target and environment */ function selectProxyUrl(targetUrl) { // Check NO_PROXY bypass first if (shouldBypassProxy(targetUrl)) { logger.debug("[Proxy] Bypassing proxy due to NO_PROXY", { targetUrl }); return null; } try { const url = new URL(targetUrl); const httpsProxy = process.env.HTTPS_PROXY || process.env.https_proxy; const httpProxy = process.env.HTTP_PROXY || process.env.http_proxy; const allProxy = process.env.ALL_PROXY || process.env.all_proxy; const socksProxy = process.env.SOCKS_PROXY || process.env.socks_proxy; // Priority: Protocol-specific > ALL_PROXY > SOCKS_PROXY if (url.protocol === "https:" && httpsProxy) { return httpsProxy; } if (url.protocol === "http:" && httpProxy) { return httpProxy; } if (allProxy) { return allProxy; } if (socksProxy) { return socksProxy; } return null; } catch (error) { logger.warn("[Proxy] Error selecting proxy URL", { targetUrl, error }); return null; } } /** * Create appropriate proxy agent based on protocol */ async function createProxyAgent(proxyUrl) { const parsed = parseProxyUrl(proxyUrl); logger.debug("[Proxy] Creating proxy agent", { protocol: parsed.protocol, hostname: parsed.hostname, port: parsed.port, hasAuth: !!parsed.auth, }); switch (parsed.protocol) { case "http:": case "https:": { // Use existing undici ProxyAgent for HTTP/HTTPS const { ProxyAgent } = await import("undici"); return new ProxyAgent(proxyUrl); } case "socks4:": case "socks5:": { // SOCKS proxy support is not included in the build to avoid optional dependencies throw new Error(`SOCKS proxy support requires 'proxy-agent' package. ` + `Install it with: npm install proxy-agent`); } default: throw new Error(`Unsupported proxy protocol: ${parsed.protocol}`); } } function sanitizeProxyUrl(url) { return maskProxyUrl(url) ?? "NOT_SET"; } function getTargetUrl(input) { return typeof input === "string" ? input : input instanceof URL ? input.href : input.url; } function createDirectFetchHandler() { return async (input, init) => { const enrichedInit = await injectTraceContext(input, init); const reqId = `req-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`; const startTs = Date.now(); const url = getTargetUrl(input); if (logger.shouldLog("debug")) { const { size: bodySize, type: bodyType } = parseBody(enrichedInit?.body); logger.debug("[Observability] HTTP request to LLM provider", { requestId: reqId, url, method: enrichedInit?.method || "POST", bodySize, bodyType, }); } try { const response = await fetchWithRetry(input, enrichedInit); if (logger.shouldLog("debug")) { const { parsed: responseBody, size: responseSize, type: responseType, headers: responseHeaders, } = await readResponseBody(response); logger.debug("[Observability] HTTP response from LLM provider", { requestId: reqId, url, status: response.status, statusText: response.statusText, durationMs: Date.now() - startTs, contentLength: responseSize, hasContent: !!responseBody, bodyType: responseType, responseHeaders, }); } return response; } catch (error) { logger.debug("[Observability] HTTP request failed", { requestId: reqId, url, error: error instanceof Error ? error.message : String(error), durationMs: Date.now() - startTs, }); throw error; } }; } async function executeProxiedFetch(input, init, proxyEnv) { const { httpsProxy, httpProxy, allProxy, socksProxy, noProxy } = proxyEnv; init = await injectTraceContext(input, init); const requestId = `req-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`; const requestStartTime = Date.now(); const targetUrl = getTargetUrl(input); if (logger.shouldLog("debug")) { const { size: bodySize, type: bodyType } = parseBody(init?.body); logger.debug("[Observability] HTTP request to LLM provider", { requestId, url: targetUrl, method: init?.method || "POST", bodySize, bodyType, }); } logger.debug(`[Proxy Fetch] ENHANCED REQUEST START`, { requestId, targetUrl, timestamp: new Date().toISOString(), httpProxy: sanitizeProxyUrl(httpProxy), httpsProxy: sanitizeProxyUrl(httpsProxy), allProxy: sanitizeProxyUrl(allProxy), socksProxy: sanitizeProxyUrl(socksProxy), noProxy: noProxy || "NOT_SET", initMethod: init?.method || "GET", }); // Clone the request before any proxy attempt so that if the proxy path // consumes the body stream and then fails, the fallback still has an intact // body to send. const requestClone = input instanceof Request ? input.clone() : null; try { const proxyUrl = selectProxyUrl(targetUrl); if (proxyUrl) { const url = new URL(targetUrl); logger.debug(`[Proxy Fetch] 🔗 ENHANCED URL ANALYSIS`, { requestId, targetUrl, urlHostname: url.hostname, urlProtocol: url.protocol, urlPort: url.port, selectedProxyUrl: sanitizeProxyUrl(proxyUrl), timestamp: new Date().toISOString(), }); logger.debug(`[Proxy Fetch] 🎯 ENHANCED PROXY AGENT CREATION`, { requestId, proxyUrl: sanitizeProxyUrl(proxyUrl), targetHostname: url.hostname, targetProtocol: url.protocol, aboutToCreateProxyAgent: true, timestamp: new Date().toISOString(), }); const globalWithCache = globalThis; if (!globalWithCache.__NL_PROXY_AGENT_CACHE__) { globalWithCache.__NL_PROXY_AGENT_CACHE__ = new Map(); } const agentCache = globalWithCache.__NL_PROXY_AGENT_CACHE__; const cacheKey = createHash("sha256") .update(maskProxyUrl(proxyUrl) ?? proxyUrl) .digest("hex"); const dispatcher = agentCache.get(cacheKey) || (await createProxyAgent(proxyUrl)); agentCache.set(cacheKey, dispatcher); logger.debug(`[Proxy Fetch] ✅ ENHANCED PROXY AGENT CREATED`, { requestId, hasDispatcher: !!dispatcher, dispatcherType: typeof dispatcher, dispatcherConstructor: dispatcher?.constructor?.name || "unknown", timestamp: new Date().toISOString(), }); let fetchInput; let fetchInit = { ...init }; if (input instanceof Request) { fetchInput = input.url; fetchInit = { method: input.method, headers: input.headers, body: input.body, ...init, }; } else { fetchInput = input; } const undici = await import("undici"); const response = await undici.fetch(fetchInput, { ...fetchInit, dispatcher, }); if (logger.shouldLog("debug")) { const { parsed: responseBody, size: responseSize, type: responseType, headers: responseHeaders, } = await readResponseBody(response); logger.debug("[Observability] HTTP response from LLM provider", { requestId, url: targetUrl, status: response?.status, statusText: response?.statusText, durationMs: Date.now() - requestStartTime, contentLength: responseSize, hasContent: !!responseBody, bodyType: responseType, proxied: true, responseHeaders, }); } logger.debug(`[Proxy Fetch] ENHANCED PROXY SUCCESS`, { requestId, responseStatus: response?.status, responseOk: response?.ok, proxyUsed: true, timestamp: new Date().toISOString(), }); return response; } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); logger.debug("[Observability] HTTP request failed", { requestId, url: targetUrl, error: errorMessage, durationMs: Date.now() - requestStartTime, }); logger.debug(`[Proxy Fetch] ENHANCED ERROR ANALYSIS`, { requestId, error: errorMessage, errorType: error instanceof Error ? error.constructor.name : typeof error, willFallback: true, timestamp: new Date().toISOString(), }); logger.warn(`[Proxy Fetch] Enhanced proxy failed (${errorMessage}), falling back to direct connection`); } logger.debug(`[Proxy Fetch] ENHANCED FALLBACK TO STANDARD FETCH`, { requestId, fallbackReason: "No proxy configured or proxy failed", timestamp: new Date().toISOString(), }); // Use the cloned request for the fallback so that the body stream is not // already consumed from the proxy attempt above. const fallbackInput = (input instanceof Request ? (requestClone ?? input) : input); try { const response = await fetchWithRetry(fallbackInput, init); if (logger.shouldLog("debug")) { const { parsed: responseBody, size: responseSize, type: responseType, headers: responseHeaders, } = await readResponseBody(response); logger.debug("[Observability] HTTP response from LLM provider", { requestId, url: targetUrl, status: response.status, statusText: response.statusText, durationMs: Date.now() - requestStartTime, contentLength: responseSize, hasContent: !!responseBody, bodyType: responseType, proxied: false, responseHeaders, }); } return response; } catch (fallbackError) { const fallbackMessage = fallbackError instanceof Error ? fallbackError.message : String(fallbackError); logger.debug("[Observability] HTTP request failed", { requestId, url: targetUrl, error: fallbackMessage, durationMs: Date.now() - requestStartTime, }); throw fallbackError; } } function createProxiedFetchHandler(proxyEnv) { return async (input, init) => executeProxiedFetch(input, init, proxyEnv); } // ==================== ENHANCED PROXY FETCH FUNCTION ==================== /** * Create a proxy-aware fetch function with enhanced capabilities * Supports HTTP/HTTPS, SOCKS4/5, authentication, and NO_PROXY bypass */ export function createProxyFetch() { // Detect ALL proxy-related environment variables const httpsProxy = process.env.HTTPS_PROXY || process.env.https_proxy; const httpProxy = process.env.HTTP_PROXY || process.env.http_proxy; const allProxy = process.env.ALL_PROXY || process.env.all_proxy; const socksProxy = process.env.SOCKS_PROXY || process.env.socks_proxy; const noProxy = process.env.NO_PROXY || process.env.no_proxy; const proxyEnv = { httpsProxy, httpProxy, allProxy, socksProxy, noProxy, }; // ENHANCED LOGGING: Capture ALL proxy-related environment variables — credentials redacted if (logger.shouldLog("debug")) { const allProxyRelatedEnvVars = Object.keys(process.env) .filter((key) => key.toLowerCase().includes("proxy")) .reduce((acc, key) => { const val = process.env[key] || "NOT_SET"; acc[key] = key.toLowerCase() === "no_proxy" ? val : sanitizeProxyUrl(val); return acc; }, {}); logger.debug("[Proxy Fetch] ENHANCED_PROXY_ENV_DETECTION", { httpProxy: sanitizeProxyUrl(httpProxy), httpsProxy: sanitizeProxyUrl(httpsProxy), allProxy: sanitizeProxyUrl(allProxy), socksProxy: sanitizeProxyUrl(socksProxy), noProxy: noProxy || "NOT_SET", allProxyRelatedEnvVars, message: "Enhanced proxy environment detection — credentials redacted", }); } // If no proxy configured, return instrumented standard fetch if (!httpsProxy && !httpProxy && !allProxy && !socksProxy) { logger.debug("[Proxy Fetch] No proxy environment variables found - using standard fetch"); return createDirectFetchHandler(); } logger.debug(`[Proxy Fetch] Configuring enhanced proxy with multiple protocol support`); logger.debug(`[Proxy Fetch] HTTP_PROXY: ${sanitizeProxyUrl(httpProxy)}`); logger.debug(`[Proxy Fetch] HTTPS_PROXY: ${sanitizeProxyUrl(httpsProxy)}`); logger.debug(`[Proxy Fetch] ALL_PROXY: ${sanitizeProxyUrl(allProxy)}`); logger.debug(`[Proxy Fetch] SOCKS_PROXY: ${sanitizeProxyUrl(socksProxy)}`); logger.debug(`[Proxy Fetch] NO_PROXY: ${noProxy || "not set"}`); return createProxiedFetchHandler(proxyEnv); } /** * Mask credentials in a proxy URL for safe logging/reporting. * * Exported so provider-side fetch loggers (lmStudio, llamaCpp, deepseek, * nvidiaNim) can sanitize upstream URLs before emitting warnings — reverse- * proxied deployments can embed credentials or signed query params in the * base URL, and those should never reach application logs verbatim. */ export function maskProxyUrl(url) { if (!url) { return null; } try { const u = new URL(url); if (u.username || u.password) { u.username = "***"; u.password = "***"; } return u.toString(); } catch { return "[invalid-url]"; } } /** * Get enhanced proxy status information */ export function getProxyStatus() { const httpsProxy = process.env.HTTPS_PROXY || process.env.https_proxy; const httpProxy = process.env.HTTP_PROXY || process.env.http_proxy; const allProxy = process.env.ALL_PROXY || process.env.all_proxy; const socksProxy = process.env.SOCKS_PROXY || process.env.socks_proxy; const noProxy = process.env.NO_PROXY || process.env.no_proxy; return { enabled: !!(httpsProxy || httpProxy || allProxy || socksProxy), httpProxy: maskProxyUrl(httpProxy), httpsProxy: maskProxyUrl(httpsProxy), allProxy: maskProxyUrl(allProxy), socksProxy: maskProxyUrl(socksProxy), noProxy: noProxy || null, method: "enhanced-proxy-agent", capabilities: [ "HTTP/HTTPS Proxy", "SOCKS4/SOCKS5 Proxy", "Proxy Authentication", "NO_PROXY Bypass", "CIDR Range Matching", "Wildcard Domain Matching", ], }; } //# sourceMappingURL=proxyFetch.js.map