@juspay/neurolink 9.41.0 → 9.42.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (212) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +7 -1
  3. package/dist/auth/anthropicOAuth.d.ts +18 -3
  4. package/dist/auth/anthropicOAuth.js +149 -4
  5. package/dist/auth/providers/firebase.js +5 -1
  6. package/dist/auth/providers/jwt.js +5 -1
  7. package/dist/auth/providers/workos.js +5 -1
  8. package/dist/auth/sessionManager.d.ts +1 -1
  9. package/dist/auth/sessionManager.js +58 -27
  10. package/dist/browser/neurolink.min.js +354 -334
  11. package/dist/cli/commands/mcp.d.ts +6 -0
  12. package/dist/cli/commands/mcp.js +188 -181
  13. package/dist/cli/commands/proxy.d.ts +2 -1
  14. package/dist/cli/commands/proxy.js +713 -431
  15. package/dist/cli/commands/task.js +3 -0
  16. package/dist/cli/factories/commandFactory.d.ts +2 -0
  17. package/dist/cli/factories/commandFactory.js +38 -0
  18. package/dist/cli/parser.js +4 -3
  19. package/dist/client/aiSdkAdapter.js +3 -0
  20. package/dist/client/streamingClient.js +30 -10
  21. package/dist/core/baseProvider.d.ts +6 -1
  22. package/dist/core/baseProvider.js +208 -230
  23. package/dist/core/factory.d.ts +3 -0
  24. package/dist/core/factory.js +138 -188
  25. package/dist/core/modules/GenerationHandler.js +3 -2
  26. package/dist/core/redisConversationMemoryManager.js +7 -3
  27. package/dist/evaluation/BatchEvaluator.js +4 -1
  28. package/dist/evaluation/hooks/observabilityHooks.js +5 -3
  29. package/dist/evaluation/pipeline/evaluationPipeline.d.ts +3 -2
  30. package/dist/evaluation/pipeline/evaluationPipeline.js +24 -9
  31. package/dist/evaluation/pipeline/strategies/batchStrategy.js +6 -3
  32. package/dist/evaluation/pipeline/strategies/samplingStrategy.js +18 -10
  33. package/dist/evaluation/scorers/scorerRegistry.d.ts +3 -0
  34. package/dist/evaluation/scorers/scorerRegistry.js +353 -282
  35. package/dist/lib/auth/anthropicOAuth.d.ts +18 -3
  36. package/dist/lib/auth/anthropicOAuth.js +149 -4
  37. package/dist/lib/auth/providers/firebase.js +5 -1
  38. package/dist/lib/auth/providers/jwt.js +5 -1
  39. package/dist/lib/auth/providers/workos.js +5 -1
  40. package/dist/lib/auth/sessionManager.d.ts +1 -1
  41. package/dist/lib/auth/sessionManager.js +58 -27
  42. package/dist/lib/client/aiSdkAdapter.js +3 -0
  43. package/dist/lib/client/streamingClient.js +30 -10
  44. package/dist/lib/core/baseProvider.d.ts +6 -1
  45. package/dist/lib/core/baseProvider.js +208 -230
  46. package/dist/lib/core/factory.d.ts +3 -0
  47. package/dist/lib/core/factory.js +138 -188
  48. package/dist/lib/core/modules/GenerationHandler.js +3 -2
  49. package/dist/lib/core/redisConversationMemoryManager.js +7 -3
  50. package/dist/lib/evaluation/BatchEvaluator.js +4 -1
  51. package/dist/lib/evaluation/hooks/observabilityHooks.js +5 -3
  52. package/dist/lib/evaluation/pipeline/evaluationPipeline.d.ts +3 -2
  53. package/dist/lib/evaluation/pipeline/evaluationPipeline.js +24 -9
  54. package/dist/lib/evaluation/pipeline/strategies/batchStrategy.js +6 -3
  55. package/dist/lib/evaluation/pipeline/strategies/samplingStrategy.js +18 -10
  56. package/dist/lib/evaluation/scorers/scorerRegistry.d.ts +3 -0
  57. package/dist/lib/evaluation/scorers/scorerRegistry.js +353 -282
  58. package/dist/lib/mcp/toolRegistry.d.ts +2 -0
  59. package/dist/lib/mcp/toolRegistry.js +32 -31
  60. package/dist/lib/neurolink.d.ts +41 -2
  61. package/dist/lib/neurolink.js +1616 -1681
  62. package/dist/lib/observability/otelBridge.d.ts +2 -2
  63. package/dist/lib/observability/otelBridge.js +12 -3
  64. package/dist/lib/providers/amazonBedrock.js +2 -4
  65. package/dist/lib/providers/anthropic.d.ts +9 -5
  66. package/dist/lib/providers/anthropic.js +19 -14
  67. package/dist/lib/providers/anthropicBaseProvider.d.ts +3 -3
  68. package/dist/lib/providers/anthropicBaseProvider.js +5 -4
  69. package/dist/lib/providers/azureOpenai.d.ts +1 -1
  70. package/dist/lib/providers/azureOpenai.js +5 -4
  71. package/dist/lib/providers/googleAiStudio.js +30 -6
  72. package/dist/lib/providers/googleVertex.d.ts +10 -0
  73. package/dist/lib/providers/googleVertex.js +437 -423
  74. package/dist/lib/providers/huggingFace.d.ts +3 -3
  75. package/dist/lib/providers/huggingFace.js +6 -8
  76. package/dist/lib/providers/litellm.d.ts +1 -0
  77. package/dist/lib/providers/litellm.js +76 -55
  78. package/dist/lib/providers/mistral.js +2 -1
  79. package/dist/lib/providers/ollama.js +93 -23
  80. package/dist/lib/providers/openAI.d.ts +2 -0
  81. package/dist/lib/providers/openAI.js +141 -141
  82. package/dist/lib/providers/openRouter.js +2 -1
  83. package/dist/lib/providers/openaiCompatible.d.ts +4 -4
  84. package/dist/lib/providers/openaiCompatible.js +4 -4
  85. package/dist/lib/proxy/claudeFormat.d.ts +3 -2
  86. package/dist/lib/proxy/claudeFormat.js +27 -14
  87. package/dist/lib/proxy/cloaking/plugins/sessionIdentity.d.ts +2 -6
  88. package/dist/lib/proxy/cloaking/plugins/sessionIdentity.js +9 -33
  89. package/dist/lib/proxy/modelRouter.js +3 -0
  90. package/dist/lib/proxy/oauthFetch.d.ts +1 -1
  91. package/dist/lib/proxy/oauthFetch.js +289 -316
  92. package/dist/lib/proxy/proxyConfig.js +46 -24
  93. package/dist/lib/proxy/proxyEnv.d.ts +19 -0
  94. package/dist/lib/proxy/proxyEnv.js +73 -0
  95. package/dist/lib/proxy/proxyFetch.js +291 -217
  96. package/dist/lib/proxy/proxyTracer.d.ts +133 -0
  97. package/dist/lib/proxy/proxyTracer.js +645 -0
  98. package/dist/lib/proxy/rawStreamCapture.d.ts +10 -0
  99. package/dist/lib/proxy/rawStreamCapture.js +83 -0
  100. package/dist/lib/proxy/requestLogger.d.ts +32 -5
  101. package/dist/lib/proxy/requestLogger.js +503 -47
  102. package/dist/lib/proxy/sseInterceptor.d.ts +97 -0
  103. package/dist/lib/proxy/sseInterceptor.js +427 -0
  104. package/dist/lib/proxy/usageStats.d.ts +4 -3
  105. package/dist/lib/proxy/usageStats.js +25 -12
  106. package/dist/lib/rag/chunkers/MarkdownChunker.js +13 -5
  107. package/dist/lib/rag/chunking/markdownChunker.js +15 -6
  108. package/dist/lib/server/routes/claudeProxyRoutes.d.ts +17 -3
  109. package/dist/lib/server/routes/claudeProxyRoutes.js +3032 -1349
  110. package/dist/lib/services/server/ai/observability/instrumentation.d.ts +7 -1
  111. package/dist/lib/services/server/ai/observability/instrumentation.js +337 -161
  112. package/dist/lib/tasks/backends/bullmqBackend.d.ts +1 -0
  113. package/dist/lib/tasks/backends/bullmqBackend.js +35 -22
  114. package/dist/lib/tasks/store/redisTaskStore.d.ts +1 -0
  115. package/dist/lib/tasks/store/redisTaskStore.js +54 -39
  116. package/dist/lib/tasks/taskManager.d.ts +5 -0
  117. package/dist/lib/tasks/taskManager.js +158 -30
  118. package/dist/lib/telemetry/index.d.ts +2 -1
  119. package/dist/lib/telemetry/index.js +2 -1
  120. package/dist/lib/telemetry/telemetryService.d.ts +3 -0
  121. package/dist/lib/telemetry/telemetryService.js +69 -5
  122. package/dist/lib/types/cli.d.ts +10 -0
  123. package/dist/lib/types/proxyTypes.d.ts +160 -5
  124. package/dist/lib/types/streamTypes.d.ts +25 -3
  125. package/dist/lib/utils/messageBuilder.js +3 -2
  126. package/dist/lib/utils/providerHealth.d.ts +19 -0
  127. package/dist/lib/utils/providerHealth.js +279 -33
  128. package/dist/lib/utils/providerUtils.js +17 -22
  129. package/dist/lib/utils/toolChoice.d.ts +4 -0
  130. package/dist/lib/utils/toolChoice.js +7 -0
  131. package/dist/mcp/toolRegistry.d.ts +2 -0
  132. package/dist/mcp/toolRegistry.js +32 -31
  133. package/dist/neurolink.d.ts +41 -2
  134. package/dist/neurolink.js +1616 -1681
  135. package/dist/observability/otelBridge.d.ts +2 -2
  136. package/dist/observability/otelBridge.js +12 -3
  137. package/dist/providers/amazonBedrock.js +2 -4
  138. package/dist/providers/anthropic.d.ts +9 -5
  139. package/dist/providers/anthropic.js +19 -14
  140. package/dist/providers/anthropicBaseProvider.d.ts +3 -3
  141. package/dist/providers/anthropicBaseProvider.js +5 -4
  142. package/dist/providers/azureOpenai.d.ts +1 -1
  143. package/dist/providers/azureOpenai.js +5 -4
  144. package/dist/providers/googleAiStudio.js +30 -6
  145. package/dist/providers/googleVertex.d.ts +10 -0
  146. package/dist/providers/googleVertex.js +437 -423
  147. package/dist/providers/huggingFace.d.ts +3 -3
  148. package/dist/providers/huggingFace.js +6 -7
  149. package/dist/providers/litellm.d.ts +1 -0
  150. package/dist/providers/litellm.js +76 -55
  151. package/dist/providers/mistral.js +2 -1
  152. package/dist/providers/ollama.js +93 -23
  153. package/dist/providers/openAI.d.ts +2 -0
  154. package/dist/providers/openAI.js +141 -141
  155. package/dist/providers/openRouter.js +2 -1
  156. package/dist/providers/openaiCompatible.d.ts +4 -4
  157. package/dist/providers/openaiCompatible.js +4 -3
  158. package/dist/proxy/claudeFormat.d.ts +3 -2
  159. package/dist/proxy/claudeFormat.js +27 -14
  160. package/dist/proxy/cloaking/plugins/sessionIdentity.d.ts +2 -6
  161. package/dist/proxy/cloaking/plugins/sessionIdentity.js +9 -33
  162. package/dist/proxy/modelRouter.js +3 -0
  163. package/dist/proxy/oauthFetch.d.ts +1 -1
  164. package/dist/proxy/oauthFetch.js +289 -316
  165. package/dist/proxy/proxyConfig.js +46 -24
  166. package/dist/proxy/proxyEnv.d.ts +19 -0
  167. package/dist/proxy/proxyEnv.js +72 -0
  168. package/dist/proxy/proxyFetch.js +291 -217
  169. package/dist/proxy/proxyTracer.d.ts +133 -0
  170. package/dist/proxy/proxyTracer.js +644 -0
  171. package/dist/proxy/rawStreamCapture.d.ts +10 -0
  172. package/dist/proxy/rawStreamCapture.js +82 -0
  173. package/dist/proxy/requestLogger.d.ts +32 -5
  174. package/dist/proxy/requestLogger.js +503 -47
  175. package/dist/proxy/sseInterceptor.d.ts +97 -0
  176. package/dist/proxy/sseInterceptor.js +426 -0
  177. package/dist/proxy/usageStats.d.ts +4 -3
  178. package/dist/proxy/usageStats.js +25 -12
  179. package/dist/rag/chunkers/MarkdownChunker.js +13 -5
  180. package/dist/rag/chunking/markdownChunker.js +15 -6
  181. package/dist/server/routes/claudeProxyRoutes.d.ts +17 -3
  182. package/dist/server/routes/claudeProxyRoutes.js +3032 -1349
  183. package/dist/services/server/ai/observability/instrumentation.d.ts +7 -1
  184. package/dist/services/server/ai/observability/instrumentation.js +337 -161
  185. package/dist/tasks/backends/bullmqBackend.d.ts +1 -0
  186. package/dist/tasks/backends/bullmqBackend.js +35 -22
  187. package/dist/tasks/store/redisTaskStore.d.ts +1 -0
  188. package/dist/tasks/store/redisTaskStore.js +54 -39
  189. package/dist/tasks/taskManager.d.ts +5 -0
  190. package/dist/tasks/taskManager.js +158 -30
  191. package/dist/telemetry/index.d.ts +2 -1
  192. package/dist/telemetry/index.js +2 -1
  193. package/dist/telemetry/telemetryService.d.ts +3 -0
  194. package/dist/telemetry/telemetryService.js +69 -5
  195. package/dist/types/cli.d.ts +10 -0
  196. package/dist/types/proxyTypes.d.ts +160 -5
  197. package/dist/types/streamTypes.d.ts +25 -3
  198. package/dist/utils/messageBuilder.js +3 -2
  199. package/dist/utils/providerHealth.d.ts +19 -0
  200. package/dist/utils/providerHealth.js +279 -33
  201. package/dist/utils/providerUtils.js +18 -22
  202. package/dist/utils/toolChoice.d.ts +4 -0
  203. package/dist/utils/toolChoice.js +6 -0
  204. package/docs/assets/dashboards/neurolink-proxy-observability-dashboard.json +6609 -0
  205. package/docs/changelog.md +252 -0
  206. package/package.json +19 -2
  207. package/scripts/observability/check-proxy-telemetry.mjs +235 -0
  208. package/scripts/observability/docker-compose.proxy-observability.yaml +55 -0
  209. package/scripts/observability/import-openobserve-dashboard.mjs +240 -0
  210. package/scripts/observability/manage-local-openobserve.sh +215 -0
  211. package/scripts/observability/otel-collector.proxy-observability.yaml +78 -0
  212. package/scripts/observability/proxy-observability.env.example +23 -0
@@ -4,9 +4,62 @@
4
4
  * Lightweight implementation extracted from research of major proxy packages
5
5
  */
6
6
  import { logger } from "../utils/logger.js";
7
- import { SpanStatusCode } from "@opentelemetry/api";
7
+ import { SpanStatusCode, propagation, context } from "@opentelemetry/api";
8
8
  import { tracers } from "../telemetry/tracers.js";
9
9
  import { shouldBypassProxy } from "./utils/noProxyUtils.js";
10
+ import { createHash } from "node:crypto";
11
+ async function getLangfuseContext() {
12
+ try {
13
+ // Dynamic import to avoid hard dependency — getLangfuseContext is only
14
+ // available when the observability module is loaded.
15
+ const mod = await import("../services/server/ai/observability/instrumentation.js");
16
+ return mod.getLangfuseContext?.();
17
+ }
18
+ catch {
19
+ return undefined;
20
+ }
21
+ }
22
+ /**
23
+ * Inject OTel trace context (traceparent/tracestate) and NeuroLink session context
24
+ * into outgoing request headers. This enables:
25
+ * - The NeuroLink proxy to link proxy spans as children of the calling SDK's trace
26
+ * - Conversation-level session/user attribution on proxy spans
27
+ */
28
+ function mergeTraceHeaders(input, init) {
29
+ const existingHeaders = new Headers(input instanceof Request ? input.headers : undefined);
30
+ if (init?.headers) {
31
+ const initHeaders = new Headers(init.headers);
32
+ for (const [key, value] of initHeaders.entries()) {
33
+ existingHeaders.set(key, value);
34
+ }
35
+ }
36
+ return existingHeaders;
37
+ }
38
+ async function injectTraceContext(input, init) {
39
+ const carrier = {};
40
+ propagation.inject(context.active(), carrier);
41
+ // Also inject NeuroLink session context from Langfuse AsyncLocalStorage
42
+ const langfuseContext = await getLangfuseContext();
43
+ if (langfuseContext?.sessionId) {
44
+ carrier["x-neurolink-session-id"] = langfuseContext.sessionId;
45
+ }
46
+ if (langfuseContext?.userId) {
47
+ carrier["x-neurolink-user-id"] = langfuseContext.userId;
48
+ }
49
+ if (langfuseContext?.conversationId) {
50
+ carrier["x-neurolink-conversation-id"] = langfuseContext.conversationId;
51
+ }
52
+ if (Object.keys(carrier).length === 0) {
53
+ return init ?? {};
54
+ }
55
+ const existingHeaders = mergeTraceHeaders(input, init);
56
+ for (const [key, value] of Object.entries(carrier)) {
57
+ if (!existingHeaders.has(key)) {
58
+ existingHeaders.set(key, value);
59
+ }
60
+ }
61
+ return { ...init, headers: existingHeaders };
62
+ }
10
63
  const fetchTracer = tracers.http;
11
64
  /**
12
65
  * Extract hostname from a URL string for safe logging (no auth tokens or paths).
@@ -279,6 +332,234 @@ async function createProxyAgent(proxyUrl) {
279
332
  throw new Error(`Unsupported proxy protocol: ${parsed.protocol}`);
280
333
  }
281
334
  }
335
+ function sanitizeProxyUrl(url) {
336
+ return maskProxyUrl(url) ?? "NOT_SET";
337
+ }
338
+ function getTargetUrl(input) {
339
+ return typeof input === "string"
340
+ ? input
341
+ : input instanceof URL
342
+ ? input.href
343
+ : input.url;
344
+ }
345
+ function createDirectFetchHandler() {
346
+ return async (input, init) => {
347
+ const enrichedInit = await injectTraceContext(input, init);
348
+ const reqId = `req-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
349
+ const startTs = Date.now();
350
+ const url = getTargetUrl(input);
351
+ if (logger.shouldLog("debug")) {
352
+ const { size: bodySize, type: bodyType } = parseBody(enrichedInit?.body);
353
+ logger.debug("[Observability] HTTP request to LLM provider", {
354
+ requestId: reqId,
355
+ url,
356
+ method: enrichedInit?.method || "POST",
357
+ bodySize,
358
+ bodyType,
359
+ });
360
+ }
361
+ try {
362
+ const response = await fetchWithRetry(input, enrichedInit);
363
+ if (logger.shouldLog("debug")) {
364
+ const { parsed: responseBody, size: responseSize, type: responseType, headers: responseHeaders, } = await readResponseBody(response);
365
+ logger.debug("[Observability] HTTP response from LLM provider", {
366
+ requestId: reqId,
367
+ url,
368
+ status: response.status,
369
+ statusText: response.statusText,
370
+ durationMs: Date.now() - startTs,
371
+ contentLength: responseSize,
372
+ hasContent: !!responseBody,
373
+ bodyType: responseType,
374
+ responseHeaders,
375
+ });
376
+ }
377
+ return response;
378
+ }
379
+ catch (error) {
380
+ logger.debug("[Observability] HTTP request failed", {
381
+ requestId: reqId,
382
+ url,
383
+ error: error instanceof Error ? error.message : String(error),
384
+ durationMs: Date.now() - startTs,
385
+ });
386
+ throw error;
387
+ }
388
+ };
389
+ }
390
+ async function executeProxiedFetch(input, init, proxyEnv) {
391
+ const { httpsProxy, httpProxy, allProxy, socksProxy, noProxy } = proxyEnv;
392
+ init = await injectTraceContext(input, init);
393
+ const requestId = `req-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
394
+ const requestStartTime = Date.now();
395
+ const targetUrl = getTargetUrl(input);
396
+ if (logger.shouldLog("debug")) {
397
+ const { size: bodySize, type: bodyType } = parseBody(init?.body);
398
+ logger.debug("[Observability] HTTP request to LLM provider", {
399
+ requestId,
400
+ url: targetUrl,
401
+ method: init?.method || "POST",
402
+ bodySize,
403
+ bodyType,
404
+ });
405
+ }
406
+ logger.debug(`[Proxy Fetch] ENHANCED REQUEST START`, {
407
+ requestId,
408
+ targetUrl,
409
+ timestamp: new Date().toISOString(),
410
+ httpProxy: sanitizeProxyUrl(httpProxy),
411
+ httpsProxy: sanitizeProxyUrl(httpsProxy),
412
+ allProxy: sanitizeProxyUrl(allProxy),
413
+ socksProxy: sanitizeProxyUrl(socksProxy),
414
+ noProxy: noProxy || "NOT_SET",
415
+ initMethod: init?.method || "GET",
416
+ });
417
+ // Clone the request before any proxy attempt so that if the proxy path
418
+ // consumes the body stream and then fails, the fallback still has an intact
419
+ // body to send.
420
+ const requestClone = input instanceof Request ? input.clone() : null;
421
+ try {
422
+ const proxyUrl = selectProxyUrl(targetUrl);
423
+ if (proxyUrl) {
424
+ const url = new URL(targetUrl);
425
+ logger.debug(`[Proxy Fetch] 🔗 ENHANCED URL ANALYSIS`, {
426
+ requestId,
427
+ targetUrl,
428
+ urlHostname: url.hostname,
429
+ urlProtocol: url.protocol,
430
+ urlPort: url.port,
431
+ selectedProxyUrl: sanitizeProxyUrl(proxyUrl),
432
+ timestamp: new Date().toISOString(),
433
+ });
434
+ logger.debug(`[Proxy Fetch] 🎯 ENHANCED PROXY AGENT CREATION`, {
435
+ requestId,
436
+ proxyUrl: sanitizeProxyUrl(proxyUrl),
437
+ targetHostname: url.hostname,
438
+ targetProtocol: url.protocol,
439
+ aboutToCreateProxyAgent: true,
440
+ timestamp: new Date().toISOString(),
441
+ });
442
+ const globalWithCache = globalThis;
443
+ if (!globalWithCache.__NL_PROXY_AGENT_CACHE__) {
444
+ globalWithCache.__NL_PROXY_AGENT_CACHE__ = new Map();
445
+ }
446
+ const agentCache = globalWithCache.__NL_PROXY_AGENT_CACHE__;
447
+ const cacheKey = createHash("sha256")
448
+ .update(maskProxyUrl(proxyUrl) ?? proxyUrl)
449
+ .digest("hex");
450
+ const dispatcher = agentCache.get(cacheKey) || (await createProxyAgent(proxyUrl));
451
+ agentCache.set(cacheKey, dispatcher);
452
+ logger.debug(`[Proxy Fetch] ✅ ENHANCED PROXY AGENT CREATED`, {
453
+ requestId,
454
+ hasDispatcher: !!dispatcher,
455
+ dispatcherType: typeof dispatcher,
456
+ dispatcherConstructor: dispatcher?.constructor?.name || "unknown",
457
+ timestamp: new Date().toISOString(),
458
+ });
459
+ let fetchInput;
460
+ let fetchInit = { ...init };
461
+ if (input instanceof Request) {
462
+ fetchInput = input.url;
463
+ fetchInit = {
464
+ method: input.method,
465
+ headers: input.headers,
466
+ body: input.body,
467
+ ...init,
468
+ };
469
+ }
470
+ else {
471
+ fetchInput = input;
472
+ }
473
+ const undici = await import("undici");
474
+ const response = await undici.fetch(fetchInput, {
475
+ ...fetchInit,
476
+ dispatcher,
477
+ });
478
+ if (logger.shouldLog("debug")) {
479
+ const { parsed: responseBody, size: responseSize, type: responseType, headers: responseHeaders, } = await readResponseBody(response);
480
+ logger.debug("[Observability] HTTP response from LLM provider", {
481
+ requestId,
482
+ url: targetUrl,
483
+ status: response?.status,
484
+ statusText: response?.statusText,
485
+ durationMs: Date.now() - requestStartTime,
486
+ contentLength: responseSize,
487
+ hasContent: !!responseBody,
488
+ bodyType: responseType,
489
+ proxied: true,
490
+ responseHeaders,
491
+ });
492
+ }
493
+ logger.debug(`[Proxy Fetch] ENHANCED PROXY SUCCESS`, {
494
+ requestId,
495
+ responseStatus: response?.status,
496
+ responseOk: response?.ok,
497
+ proxyUsed: true,
498
+ timestamp: new Date().toISOString(),
499
+ });
500
+ return response;
501
+ }
502
+ }
503
+ catch (error) {
504
+ const errorMessage = error instanceof Error ? error.message : String(error);
505
+ logger.debug("[Observability] HTTP request failed", {
506
+ requestId,
507
+ url: targetUrl,
508
+ error: errorMessage,
509
+ durationMs: Date.now() - requestStartTime,
510
+ });
511
+ logger.debug(`[Proxy Fetch] ENHANCED ERROR ANALYSIS`, {
512
+ requestId,
513
+ error: errorMessage,
514
+ errorType: error instanceof Error ? error.constructor.name : typeof error,
515
+ willFallback: true,
516
+ timestamp: new Date().toISOString(),
517
+ });
518
+ logger.warn(`[Proxy Fetch] Enhanced proxy failed (${errorMessage}), falling back to direct connection`);
519
+ }
520
+ logger.debug(`[Proxy Fetch] ENHANCED FALLBACK TO STANDARD FETCH`, {
521
+ requestId,
522
+ fallbackReason: "No proxy configured or proxy failed",
523
+ timestamp: new Date().toISOString(),
524
+ });
525
+ // Use the cloned request for the fallback so that the body stream is not
526
+ // already consumed from the proxy attempt above.
527
+ const fallbackInput = (input instanceof Request ? (requestClone ?? input) : input);
528
+ try {
529
+ const response = await fetchWithRetry(fallbackInput, init);
530
+ if (logger.shouldLog("debug")) {
531
+ const { parsed: responseBody, size: responseSize, type: responseType, headers: responseHeaders, } = await readResponseBody(response);
532
+ logger.debug("[Observability] HTTP response from LLM provider", {
533
+ requestId,
534
+ url: targetUrl,
535
+ status: response.status,
536
+ statusText: response.statusText,
537
+ durationMs: Date.now() - requestStartTime,
538
+ contentLength: responseSize,
539
+ hasContent: !!responseBody,
540
+ bodyType: responseType,
541
+ proxied: false,
542
+ responseHeaders,
543
+ });
544
+ }
545
+ return response;
546
+ }
547
+ catch (fallbackError) {
548
+ const fallbackMessage = fallbackError instanceof Error
549
+ ? fallbackError.message
550
+ : String(fallbackError);
551
+ logger.debug("[Observability] HTTP request failed", {
552
+ requestId,
553
+ url: targetUrl,
554
+ error: fallbackMessage,
555
+ durationMs: Date.now() - requestStartTime,
556
+ });
557
+ throw fallbackError;
558
+ }
559
+ }
560
+ function createProxiedFetchHandler(proxyEnv) {
561
+ return async (input, init) => executeProxiedFetch(input, init, proxyEnv);
562
+ }
282
563
  // ==================== ENHANCED PROXY FETCH FUNCTION ====================
283
564
  /**
284
565
  * Create a proxy-aware fetch function with enhanced capabilities
@@ -291,9 +572,14 @@ export function createProxyFetch() {
291
572
  const allProxy = process.env.ALL_PROXY || process.env.all_proxy;
292
573
  const socksProxy = process.env.SOCKS_PROXY || process.env.socks_proxy;
293
574
  const noProxy = process.env.NO_PROXY || process.env.no_proxy;
575
+ const proxyEnv = {
576
+ httpsProxy,
577
+ httpProxy,
578
+ allProxy,
579
+ socksProxy,
580
+ noProxy,
581
+ };
294
582
  // ENHANCED LOGGING: Capture ALL proxy-related environment variables — credentials redacted
295
- // Reuse module-level maskProxyUrl, defaulting to "NOT_SET" for undefined values
296
- const sanitizeProxyUrl = (url) => maskProxyUrl(url) ?? "NOT_SET";
297
583
  if (logger.shouldLog("debug")) {
298
584
  const allProxyRelatedEnvVars = Object.keys(process.env)
299
585
  .filter((key) => key.toLowerCase().includes("proxy"))
@@ -316,52 +602,7 @@ export function createProxyFetch() {
316
602
  // If no proxy configured, return instrumented standard fetch
317
603
  if (!httpsProxy && !httpProxy && !allProxy && !socksProxy) {
318
604
  logger.debug("[Proxy Fetch] No proxy environment variables found - using standard fetch");
319
- return async (input, init) => {
320
- const reqId = `req-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
321
- const startTs = Date.now();
322
- const url = typeof input === "string"
323
- ? input
324
- : input instanceof URL
325
- ? input.href
326
- : input.url;
327
- if (logger.shouldLog("debug")) {
328
- const { size: bodySize, type: bodyType } = parseBody(init?.body);
329
- logger.debug("[Observability] HTTP request to LLM provider", {
330
- requestId: reqId,
331
- url,
332
- method: init?.method || "POST",
333
- bodySize,
334
- bodyType,
335
- });
336
- }
337
- try {
338
- const response = await fetchWithRetry(input, init);
339
- if (logger.shouldLog("debug")) {
340
- const { parsed: responseBody, size: responseSize, type: responseType, headers: responseHeaders, } = await readResponseBody(response);
341
- logger.debug("[Observability] HTTP response from LLM provider", {
342
- requestId: reqId,
343
- url,
344
- status: response.status,
345
- statusText: response.statusText,
346
- durationMs: Date.now() - startTs,
347
- contentLength: responseSize,
348
- hasContent: !!responseBody,
349
- bodyType: responseType,
350
- responseHeaders,
351
- });
352
- }
353
- return response;
354
- }
355
- catch (error) {
356
- logger.debug("[Observability] HTTP request failed", {
357
- requestId: reqId,
358
- url,
359
- error: error instanceof Error ? error.message : String(error),
360
- durationMs: Date.now() - startTs,
361
- });
362
- throw error;
363
- }
364
- };
605
+ return createDirectFetchHandler();
365
606
  }
366
607
  logger.debug(`[Proxy Fetch] Configuring enhanced proxy with multiple protocol support`);
367
608
  logger.debug(`[Proxy Fetch] HTTP_PROXY: ${sanitizeProxyUrl(httpProxy)}`);
@@ -369,174 +610,7 @@ export function createProxyFetch() {
369
610
  logger.debug(`[Proxy Fetch] ALL_PROXY: ${sanitizeProxyUrl(allProxy)}`);
370
611
  logger.debug(`[Proxy Fetch] SOCKS_PROXY: ${sanitizeProxyUrl(socksProxy)}`);
371
612
  logger.debug(`[Proxy Fetch] NO_PROXY: ${noProxy || "not set"}`);
372
- // Return enhanced proxy-aware fetch function
373
- return async (input, init) => {
374
- const requestId = `req-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
375
- const requestStartTime = Date.now();
376
- // Determine target URL
377
- const targetUrl = typeof input === "string"
378
- ? input
379
- : input instanceof URL
380
- ? input.href
381
- : input.url;
382
- // Request logging with sensitive header redaction — gated behind debug check
383
- if (logger.shouldLog("debug")) {
384
- const { size: bodySize, type: bodyType } = parseBody(init?.body);
385
- logger.debug("[Observability] HTTP request to LLM provider", {
386
- requestId,
387
- url: targetUrl,
388
- method: init?.method || "POST",
389
- bodySize,
390
- bodyType,
391
- });
392
- }
393
- logger.debug(`[Proxy Fetch] ENHANCED REQUEST START`, {
394
- requestId,
395
- targetUrl,
396
- timestamp: new Date().toISOString(),
397
- httpProxy: sanitizeProxyUrl(httpProxy),
398
- httpsProxy: sanitizeProxyUrl(httpsProxy),
399
- allProxy: sanitizeProxyUrl(allProxy),
400
- socksProxy: sanitizeProxyUrl(socksProxy),
401
- initMethod: init?.method || "GET",
402
- });
403
- try {
404
- // Enhanced proxy selection with NO_PROXY bypass and multiple protocols
405
- const proxyUrl = selectProxyUrl(targetUrl);
406
- if (proxyUrl) {
407
- const url = new URL(targetUrl);
408
- const sanitizedProxy = sanitizeProxyUrl(proxyUrl);
409
- logger.debug(`[Proxy Fetch] 🔗 ENHANCED URL ANALYSIS`, {
410
- requestId,
411
- targetUrl,
412
- urlHostname: url.hostname,
413
- urlProtocol: url.protocol,
414
- urlPort: url.port,
415
- selectedProxyUrl: sanitizedProxy,
416
- timestamp: new Date().toISOString(),
417
- });
418
- logger.debug(`[Proxy Fetch] 🎯 ENHANCED PROXY AGENT CREATION`, {
419
- requestId,
420
- proxyUrl: sanitizedProxy,
421
- targetHostname: url.hostname,
422
- targetProtocol: url.protocol,
423
- aboutToCreateProxyAgent: true,
424
- timestamp: new Date().toISOString(),
425
- });
426
- // Create/reuse proxy agent (HTTP/HTTPS/SOCKS)
427
- const agentCache = globalThis.__NL_PROXY_AGENT_CACHE__ ??
428
- (globalThis.__NL_PROXY_AGENT_CACHE__ = new Map());
429
- const cacheKey = maskProxyUrl(proxyUrl) ?? proxyUrl; // mask credentials in cache key
430
- const dispatcher = agentCache.get(cacheKey) || (await createProxyAgent(proxyUrl));
431
- agentCache.set(cacheKey, dispatcher);
432
- logger.debug(`[Proxy Fetch] ✅ ENHANCED PROXY AGENT CREATED`, {
433
- requestId,
434
- hasDispatcher: !!dispatcher,
435
- dispatcherType: typeof dispatcher,
436
- dispatcherConstructor: dispatcher?.constructor?.name || "unknown",
437
- timestamp: new Date().toISOString(),
438
- });
439
- // Handle Request objects by extracting URL and merging properties
440
- let fetchInput;
441
- let fetchInit = { ...init };
442
- if (input instanceof Request) {
443
- fetchInput = input.url;
444
- fetchInit = {
445
- method: input.method,
446
- headers: input.headers,
447
- body: input.body,
448
- ...init, // Allow init to override Request properties
449
- };
450
- }
451
- else {
452
- fetchInput = input;
453
- }
454
- // Use undici fetch with enhanced dispatcher (supports HTTP/HTTPS/SOCKS)
455
- const undici = await import("undici");
456
- const response = await undici.fetch(fetchInput, {
457
- ...fetchInit,
458
- dispatcher: dispatcher,
459
- });
460
- if (logger.shouldLog("debug")) {
461
- const { parsed: responseBody, size: responseSize, type: responseType, headers: responseHeaders, } = await readResponseBody(response);
462
- logger.debug("[Observability] HTTP response from LLM provider", {
463
- requestId,
464
- url: targetUrl,
465
- status: response?.status,
466
- statusText: response?.statusText,
467
- durationMs: Date.now() - requestStartTime,
468
- contentLength: responseSize,
469
- hasContent: !!responseBody,
470
- bodyType: responseType,
471
- proxied: true,
472
- responseHeaders,
473
- });
474
- }
475
- logger.debug(`[Proxy Fetch] ENHANCED PROXY SUCCESS`, {
476
- requestId,
477
- responseStatus: response?.status,
478
- responseOk: response?.ok,
479
- proxyUsed: true,
480
- timestamp: new Date().toISOString(),
481
- });
482
- return response;
483
- }
484
- }
485
- catch (error) {
486
- const errorMessage = error instanceof Error ? error.message : String(error);
487
- logger.debug("[Observability] HTTP request failed", {
488
- requestId,
489
- url: targetUrl,
490
- error: errorMessage,
491
- durationMs: Date.now() - requestStartTime,
492
- });
493
- logger.debug(`[Proxy Fetch] ENHANCED ERROR ANALYSIS`, {
494
- requestId,
495
- error: errorMessage,
496
- errorType: error instanceof Error ? error.constructor.name : typeof error,
497
- willFallback: true,
498
- timestamp: new Date().toISOString(),
499
- });
500
- logger.warn(`[Proxy Fetch] Enhanced proxy failed (${errorMessage}), falling back to direct connection`);
501
- }
502
- // Fallback to standard fetch
503
- logger.debug(`[Proxy Fetch] ENHANCED FALLBACK TO STANDARD FETCH`, {
504
- requestId,
505
- fallbackReason: "No proxy configured or proxy failed",
506
- timestamp: new Date().toISOString(),
507
- });
508
- try {
509
- const response = await fetchWithRetry(input, init);
510
- if (logger.shouldLog("debug")) {
511
- const { parsed: responseBody, size: responseSize, type: responseType, headers: responseHeaders, } = await readResponseBody(response);
512
- logger.debug("[Observability] HTTP response from LLM provider", {
513
- requestId,
514
- url: targetUrl,
515
- status: response.status,
516
- statusText: response.statusText,
517
- durationMs: Date.now() - requestStartTime,
518
- contentLength: responseSize,
519
- hasContent: !!responseBody,
520
- bodyType: responseType,
521
- proxied: false,
522
- responseHeaders,
523
- });
524
- }
525
- return response;
526
- }
527
- catch (fallbackError) {
528
- const fallbackMessage = fallbackError instanceof Error
529
- ? fallbackError.message
530
- : String(fallbackError);
531
- logger.debug("[Observability] HTTP request failed", {
532
- requestId,
533
- url: targetUrl,
534
- error: fallbackMessage,
535
- durationMs: Date.now() - requestStartTime,
536
- });
537
- throw fallbackError;
538
- }
539
- };
613
+ return createProxiedFetchHandler(proxyEnv);
540
614
  }
541
615
  /**
542
616
  * Mask credentials in a proxy URL for safe logging/reporting.
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Proxy Request Tracer
3
+ *
4
+ * Creates and manages OTel spans for the proxy request lifecycle.
5
+ * Provides a clean API for claudeProxyRoutes to trace each phase:
6
+ * receive -> account_selection -> upstream (per retry) -> stream -> end
7
+ *
8
+ * Uses the existing instrumentation infrastructure:
9
+ * - getTracer() from instrumentation.ts for span creation
10
+ * - setLangfuseContext() for Langfuse enrichment
11
+ * - OtelBridge for context propagation to/from upstream
12
+ * - SpanAttributes from spanTypes.ts for attribute naming
13
+ * - calculateCost() from pricing.ts for cost tracking
14
+ * - TelemetryService for metrics recording
15
+ */
16
+ import { type Span } from "@opentelemetry/api";
17
+ type ProxyRequestContext = {
18
+ requestId: string;
19
+ method: string;
20
+ path: string;
21
+ model: string;
22
+ stream: boolean;
23
+ toolCount: number;
24
+ sessionId?: string;
25
+ userAgent?: string;
26
+ clientApp?: string;
27
+ };
28
+ type AccountSelectionContext = {
29
+ strategy: string;
30
+ accountsTotal: number;
31
+ accountsHealthy: number;
32
+ selectedAccount: string;
33
+ accountType: string;
34
+ rateLimitBefore5h?: number;
35
+ rateLimitBefore7d?: number;
36
+ };
37
+ type UpstreamAttemptContext = {
38
+ attempt: number;
39
+ account: string;
40
+ polyfillHeaders: boolean;
41
+ polyfillBody: boolean;
42
+ upstreamUrl: string;
43
+ };
44
+ type UsageContext = {
45
+ inputTokens: number;
46
+ outputTokens: number;
47
+ cacheCreationTokens: number;
48
+ cacheReadTokens: number;
49
+ reasoningTokens?: number;
50
+ rateLimitAfter5h?: number;
51
+ rateLimitAfter7d?: number;
52
+ };
53
+ declare class ProxyTracer {
54
+ private readonly rootSpan;
55
+ private readonly proxyTracer;
56
+ private readonly bridge;
57
+ private readonly requestId;
58
+ private readonly model;
59
+ private readonly startTime;
60
+ private readonly isStream;
61
+ private accountEmail?;
62
+ private usage?;
63
+ private mode;
64
+ private constructor();
65
+ /**
66
+ * Create a root span for a proxy request and set Langfuse context.
67
+ *
68
+ * If the incoming request carries a `traceparent` header, the root span
69
+ * will be linked to the caller's trace via OtelBridge.extractContext().
70
+ */
71
+ static startRequest(ctx: ProxyRequestContext, incomingHeaders?: Record<string, string>): ProxyTracer;
72
+ /** Span covering the initial request receive and parse phase. */
73
+ startReceive(): Span;
74
+ /** Span covering account selection logic (fill-first / round-robin). */
75
+ startAccountSelection(): Span;
76
+ /** Span covering a single upstream attempt. One per retry. */
77
+ startUpstreamAttempt(ctx: UpstreamAttemptContext): Span;
78
+ /** Span covering the SSE stream relay phase. */
79
+ startStream(): Span;
80
+ /** Record account selection outcome on the root span. */
81
+ setAccountSelection(ctx: AccountSelectionContext): void;
82
+ /** Record token usage and cost on the root span. */
83
+ setUsage(ctx: UsageContext): void;
84
+ /** Record an error on the root span. */
85
+ setError(errorType: string, errorMessage: string): void;
86
+ /** Record whether the request was handled in full or passthrough mode. */
87
+ setMode(mode: "full" | "passthrough" | "passthrough-cli"): void;
88
+ /**
89
+ * Record that the proxy substituted a different model than was requested.
90
+ * Sets span attributes and increments the substitution metric counter.
91
+ */
92
+ setModelSubstitution(requestedModel: string, actualModel: string): void;
93
+ /** Log the incoming client request body (redacted). */
94
+ logRequestBody(body: string): void;
95
+ /** Log the incoming client request headers (redacted). */
96
+ logRequestHeaders(headers: Record<string, string>): void;
97
+ /** Log the upstream request body (redacted, as sent to Anthropic). */
98
+ logUpstreamRequestBody(body: string): void;
99
+ /** Log the upstream request headers (redacted). */
100
+ logUpstreamRequestHeaders(headers: Record<string, string>): void;
101
+ /** Log the upstream response headers (redacted). */
102
+ logUpstreamResponseHeaders(headers: Record<string, string>): void;
103
+ /** Log the upstream response body (redacted). */
104
+ logUpstreamResponseBody(body: string): void;
105
+ /** Log SSE stream events (each event has type, timestamp, data). */
106
+ logStreamEvents(events: Array<{
107
+ type: string;
108
+ timestamp: number;
109
+ data: string;
110
+ }>): void;
111
+ /** Record an upstream retry attempt. */
112
+ recordRetry(account: string, reason: string): void;
113
+ /** Record request and/or response body sizes for bandwidth tracking. */
114
+ recordBodySizes(requestBytes?: number, responseBytes?: number): void;
115
+ /** Return the OTel trace/span IDs for this request (for log correlation). */
116
+ getTraceContext(): {
117
+ traceId: string;
118
+ spanId: string;
119
+ };
120
+ /** Return the captured usage (set by setUsage). */
121
+ getUsage(): UsageContext | undefined;
122
+ /** End the root span with final HTTP status and duration, and emit OTEL metrics. */
123
+ end(responseStatus: number, durationMs: number): void;
124
+ /** Record metrics via TelemetryService (call after setUsage). */
125
+ recordMetrics(): void;
126
+ /**
127
+ * Get trace context headers for propagation to the upstream Anthropic request.
128
+ * Injects the current trace's `traceparent` / `tracestate` into a new header map.
129
+ */
130
+ getTraceHeaders(): Record<string, string>;
131
+ }
132
+ export { ProxyTracer };
133
+ export type { ProxyRequestContext, AccountSelectionContext, UpstreamAttemptContext, UsageContext, };