@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
@@ -8,7 +8,8 @@
8
8
  */
9
9
  import { LangfuseSpanProcessor } from "@langfuse/otel";
10
10
  import { trace } from "@opentelemetry/api";
11
- import type { SpanProcessor } from "@opentelemetry/sdk-trace-base";
11
+ import { LoggerProvider } from "@opentelemetry/sdk-logs";
12
+ import { type SpanProcessor } from "@opentelemetry/sdk-trace-base";
12
13
  import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
13
14
  import type { LangfuseConfig } from "../../../../types/observability.js";
14
15
  /**
@@ -104,6 +105,11 @@ export declare function getLangfuseSpanProcessor(): LangfuseSpanProcessor | null
104
105
  * Get the tracer provider
105
106
  */
106
107
  export declare function getTracerProvider(): NodeTracerProvider | null;
108
+ /**
109
+ * Get the logger provider for emitting OTLP log records.
110
+ * Returns null if OTLP is not configured or LoggerProvider was not created.
111
+ */
112
+ export declare function getLoggerProvider(): LoggerProvider | null;
107
113
  /**
108
114
  * Check if OpenTelemetry is initialized
109
115
  */
@@ -7,15 +7,90 @@
7
7
  * Flow: Vercel AI SDK → OpenTelemetry Spans → LangfuseSpanProcessor → Langfuse Platform
8
8
  */
9
9
  import { LangfuseSpanProcessor } from "@langfuse/otel";
10
- import { trace } from "@opentelemetry/api";
10
+ import { metrics, trace } from "@opentelemetry/api";
11
+ import { W3CTraceContextPropagator } from "@opentelemetry/core";
12
+ import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-http";
13
+ import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http";
14
+ import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
11
15
  import { resourceFromAttributes } from "@opentelemetry/resources";
16
+ import { MeterProvider, PeriodicExportingMetricReader, } from "@opentelemetry/sdk-metrics";
17
+ import { BatchLogRecordProcessor, LoggerProvider, } from "@opentelemetry/sdk-logs";
18
+ import { BatchSpanProcessor, } from "@opentelemetry/sdk-trace-base";
12
19
  import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
13
20
  import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION, } from "@opentelemetry/semantic-conventions";
14
21
  import { AsyncLocalStorage } from "async_hooks";
15
22
  import { logger } from "../../../../utils/logger.js";
16
23
  const LOG_PREFIX = "[OpenTelemetry]";
24
+ function createOtelResource(config, serviceName) {
25
+ return resourceFromAttributes({
26
+ [ATTR_SERVICE_NAME]: serviceName,
27
+ [ATTR_SERVICE_VERSION]: config.release || "v1.0.0",
28
+ "deployment.environment": config.environment || "dev",
29
+ });
30
+ }
31
+ function initializeOtlpMetricsAndLogs(resource, otlpEndpoint, serviceName) {
32
+ if (!otlpEndpoint) {
33
+ return;
34
+ }
35
+ try {
36
+ const metricExporter = new OTLPMetricExporter({
37
+ url: `${otlpEndpoint}/v1/metrics`,
38
+ });
39
+ const metricReader = new PeriodicExportingMetricReader({
40
+ exporter: metricExporter,
41
+ exportIntervalMillis: 15000,
42
+ exportTimeoutMillis: 10000,
43
+ });
44
+ meterProvider = new MeterProvider({
45
+ resource,
46
+ readers: [metricReader],
47
+ });
48
+ metrics.setGlobalMeterProvider(meterProvider);
49
+ logger.info(`${LOG_PREFIX} OTLP metric exporter added — MeterProvider registered globally`, {
50
+ endpoint: `${otlpEndpoint}/v1/metrics`,
51
+ exportIntervalMs: 15000,
52
+ serviceName,
53
+ meterProviderType: meterProvider.constructor.name,
54
+ });
55
+ }
56
+ catch (metricsError) {
57
+ logger.warn(`${LOG_PREFIX} Failed to create OTLP metric exporter (non-fatal)`, {
58
+ error: metricsError instanceof Error
59
+ ? metricsError.message
60
+ : String(metricsError),
61
+ endpoint: otlpEndpoint,
62
+ });
63
+ }
64
+ try {
65
+ const logExporter = new OTLPLogExporter({
66
+ url: `${otlpEndpoint}/v1/logs`,
67
+ });
68
+ const logProcessor = new BatchLogRecordProcessor(logExporter, {
69
+ maxQueueSize: 2048,
70
+ maxExportBatchSize: 512,
71
+ scheduledDelayMillis: 2000,
72
+ exportTimeoutMillis: 30000,
73
+ });
74
+ loggerProvider = new LoggerProvider({
75
+ resource,
76
+ processors: [logProcessor],
77
+ });
78
+ logger.info(`${LOG_PREFIX} OTLP log exporter added — LoggerProvider created`, {
79
+ endpoint: `${otlpEndpoint}/v1/logs`,
80
+ serviceName,
81
+ });
82
+ }
83
+ catch (logsError) {
84
+ logger.warn(`${LOG_PREFIX} Failed to create OTLP log exporter (non-fatal)`, {
85
+ error: logsError instanceof Error ? logsError.message : String(logsError),
86
+ endpoint: otlpEndpoint,
87
+ });
88
+ }
89
+ }
17
90
  const contextStorage = new AsyncLocalStorage();
18
91
  let tracerProvider = null;
92
+ let meterProvider = null;
93
+ let loggerProvider = null;
19
94
  let langfuseProcessor = null;
20
95
  let isInitialized = false;
21
96
  let isCredentialsValid = false;
@@ -328,48 +403,20 @@ class ContextEnricher {
328
403
  return Promise.resolve();
329
404
  }
330
405
  }
331
- /**
332
- * Initialize OpenTelemetry with Langfuse span processor
333
- *
334
- * This connects Vercel AI SDK's experimental_telemetry to Langfuse by:
335
- * 1. Creating LangfuseSpanProcessor with Langfuse credentials
336
- * 2. Creating a NodeTracerProvider with service metadata and span processor
337
- * 3. Registering the provider globally for AI SDK to use
338
- *
339
- * NEW: If useExternalTracerProvider is true or autoDetectExternalProvider detects
340
- * an existing provider, steps 2 and 3 are skipped. The span processors are still
341
- * created and can be retrieved via getSpanProcessors().
342
- *
343
- * @param config - Langfuse configuration passed from parent application
344
- */
345
- export function initializeOpenTelemetry(config) {
346
- // Guard against multiple initializations — but always update config
347
- // so that later NeuroLink instances can change traceNameFormat,
348
- // autoDetectOperationName, and other configuration preferences
349
- // without re-initializing the OTEL infrastructure.
350
- if (isInitialized) {
351
- currentConfig = config;
352
- logger.debug(`${LOG_PREFIX} Already initialized, config updated`, {
353
- usingExternalProvider,
354
- hasLangfuseProcessor: !!langfuseProcessor,
355
- hasTraceNameFormat: typeof config.traceNameFormat === "function",
356
- });
357
- return;
358
- }
359
- // FIRST: Check for external provider mode - bypasses enabled check
360
- // NOTE: When autoDetectExternalProvider is true, we trust the flag directly rather than
361
- // calling hasExternalTracerProvider(). This is because Neurolink may bundle its own copy
362
- // of @opentelemetry/api, which has a separate global state from the host application.
363
- // The hasExternalTracerProvider() check would query Neurolink's bundled @opentelemetry/api
364
- // global state (which has no provider registered), not the host's global state.
365
- // By trusting autoDetectExternalProvider=true, we let the host application signal that
366
- // it has already registered a TracerProvider.
367
- const shouldUseExternal = config?.useExternalTracerProvider === true ||
368
- config?.autoDetectExternalProvider === true;
369
- if (shouldUseExternal) {
370
- // Validate credentials even in external mode
371
- if (!config?.publicKey || !config?.secretKey) {
372
- logger.warn(`${LOG_PREFIX} External provider mode but missing credentials, skipping initialization`, {
406
+ function createLangfuseProcessor(config) {
407
+ return new LangfuseSpanProcessor({
408
+ publicKey: config.publicKey,
409
+ secretKey: config.secretKey,
410
+ baseUrl: config.baseUrl || "https://cloud.langfuse.com",
411
+ environment: config.environment || "dev",
412
+ release: config.release || "v1.0.0",
413
+ shouldExportSpan: () => true,
414
+ });
415
+ }
416
+ function initializeExternalOpenTelemetryMode(config, resource, otlpEndpoint, serviceName, langfuseRequested, hasLangfuseCreds) {
417
+ if (langfuseRequested && !hasLangfuseCreds) {
418
+ if (!otlpEndpoint) {
419
+ logger.warn(`${LOG_PREFIX} External provider mode requested Langfuse but credentials are missing, and no OTLP endpoint is configured; skipping initialization`, {
373
420
  hasPublicKey: !!config?.publicKey,
374
421
  hasSecretKey: !!config?.secretKey,
375
422
  });
@@ -377,154 +424,164 @@ export function initializeOpenTelemetry(config) {
377
424
  isCredentialsValid = false;
378
425
  return;
379
426
  }
427
+ logger.warn(`${LOG_PREFIX} External provider mode missing Langfuse credentials; continuing with OTLP-only metrics/logs`, {
428
+ hasPublicKey: !!config?.publicKey,
429
+ hasSecretKey: !!config?.secretKey,
430
+ otlpEnabled: true,
431
+ });
432
+ }
433
+ try {
434
+ currentConfig = config;
435
+ isCredentialsValid = hasLangfuseCreds;
436
+ langfuseProcessor =
437
+ langfuseRequested && hasLangfuseCreds
438
+ ? createLangfuseProcessor(config)
439
+ : null;
440
+ usingExternalProvider = true;
441
+ isInitialized = true;
442
+ initializeOtlpMetricsAndLogs(resource, otlpEndpoint, serviceName);
380
443
  try {
381
- currentConfig = config;
382
- isCredentialsValid = true;
383
- // Create span processor for external provider mode
384
- // shouldExportSpan: export all spans (v5 default filters to gen_ai spans only)
385
- langfuseProcessor = new LangfuseSpanProcessor({
386
- publicKey: config.publicKey,
387
- secretKey: config.secretKey,
388
- baseUrl: config.baseUrl || "https://cloud.langfuse.com",
389
- environment: config.environment || "dev",
390
- release: config.release || "v1.0.0",
391
- shouldExportSpan: () => true,
392
- });
393
- usingExternalProvider = true;
394
- isInitialized = true;
395
- // Auto-register ContextEnricher with the global TracerProvider
396
- // This ensures trace names are set even when host doesn't call getSpanProcessors()
397
- try {
398
- const globalProvider = trace.getTracerProvider();
399
- // Check if it's a real provider with addSpanProcessor method (not the no-op default)
400
- if (globalProvider &&
401
- typeof globalProvider
402
- .addSpanProcessor === "function") {
403
- const provider = globalProvider;
404
- // Add ContextEnricher for trace name enrichment
405
- provider.addSpanProcessor(new ContextEnricher());
406
- // Only add LangfuseSpanProcessor if the host has not already registered one.
407
- // When skipLangfuseSpanProcessor is true, the host (e.g. Curator) already
408
- // registers its own LangfuseSpanProcessor via a DeferredSpanProcessor, so
409
- // adding another one here would cause duplicate trace exports to Langfuse.
410
- const skipLangfuse = config.skipLangfuseSpanProcessor === true;
411
- if (!skipLangfuse) {
412
- provider.addSpanProcessor(langfuseProcessor);
413
- }
414
- logger.info(`${LOG_PREFIX} Auto-registered processors with global TracerProvider`, {
415
- processors: skipLangfuse
416
- ? ["ContextEnricher"]
417
- : ["ContextEnricher", "LangfuseSpanProcessor"],
418
- reason: "External provider mode with auto-registration",
419
- skippedLangfuseSpanProcessor: skipLangfuse,
420
- });
421
- }
422
- else {
423
- // No real provider found - host will need to add processors manually
424
- logger.info(`${LOG_PREFIX} Using external TracerProvider mode`, {
425
- reason: config.useExternalTracerProvider
426
- ? "useExternalTracerProvider=true"
427
- : "autoDetectExternalProvider=true (trusting host signal)",
428
- instructions: "Add span processors to your TracerProvider using getSpanProcessors()",
429
- });
430
- logger.info(`${LOG_PREFIX} Span processors ready for external use`, {
431
- processors: ["ContextEnricher", "LangfuseSpanProcessor"],
432
- usage: "import { getSpanProcessors } from '@juspay/neurolink'",
433
- });
444
+ const globalProvider = trace.getTracerProvider();
445
+ const provider = globalProvider;
446
+ if (globalProvider && typeof provider.addSpanProcessor === "function") {
447
+ provider.addSpanProcessor(new ContextEnricher());
448
+ const skipLangfuse = config.skipLangfuseSpanProcessor === true || !langfuseProcessor;
449
+ if (!skipLangfuse && langfuseProcessor) {
450
+ provider.addSpanProcessor(langfuseProcessor);
434
451
  }
435
- }
436
- catch (autoRegisterError) {
437
- // Auto-registration failed - fall back to manual registration
438
- logger.warn(`${LOG_PREFIX} Auto-registration failed, manual registration required`, {
439
- error: autoRegisterError instanceof Error
440
- ? autoRegisterError.message
441
- : String(autoRegisterError),
442
- instructions: "Add span processors to your TracerProvider using getSpanProcessors()",
452
+ logger.info(`${LOG_PREFIX} Auto-registered processors with global TracerProvider`, {
453
+ processors: skipLangfuse
454
+ ? ["ContextEnricher"]
455
+ : ["ContextEnricher", "LangfuseSpanProcessor"],
456
+ reason: "External provider mode with auto-registration",
457
+ skippedLangfuseSpanProcessor: skipLangfuse,
443
458
  });
459
+ return;
444
460
  }
445
- return;
461
+ logger.info(`${LOG_PREFIX} Using external TracerProvider mode`, {
462
+ reason: config.useExternalTracerProvider
463
+ ? "useExternalTracerProvider=true"
464
+ : "autoDetectExternalProvider=true (trusting host signal)",
465
+ instructions: "Add span processors to your TracerProvider using getSpanProcessors()",
466
+ });
467
+ logger.info(`${LOG_PREFIX} Span processors ready for external use`, {
468
+ processors: langfuseProcessor
469
+ ? ["ContextEnricher", "LangfuseSpanProcessor"]
470
+ : ["ContextEnricher"],
471
+ usage: "import { getSpanProcessors } from '@juspay/neurolink'",
472
+ });
446
473
  }
447
- catch (error) {
448
- logger.error(`${LOG_PREFIX} Failed to create span processor for external mode`, {
449
- error: error instanceof Error ? error.message : String(error),
450
- stack: error instanceof Error ? error.stack : undefined,
474
+ catch (autoRegisterError) {
475
+ logger.warn(`${LOG_PREFIX} Auto-registration failed, manual registration required`, {
476
+ error: autoRegisterError instanceof Error
477
+ ? autoRegisterError.message
478
+ : String(autoRegisterError),
479
+ instructions: "Add span processors to your TracerProvider using getSpanProcessors()",
451
480
  });
452
- isInitialized = true;
453
- return;
454
481
  }
455
482
  }
456
- // THEN: Check enabled for standalone mode
457
- if (!config?.enabled) {
458
- logger.debug(`${LOG_PREFIX} Langfuse disabled and no external provider, skipping initialization`);
483
+ catch (error) {
484
+ logger.error(`${LOG_PREFIX} Failed to create span processor for external mode`, {
485
+ error: error instanceof Error ? error.message : String(error),
486
+ stack: error instanceof Error ? error.stack : undefined,
487
+ });
488
+ isInitialized = true;
489
+ }
490
+ }
491
+ function initializeStandaloneOpenTelemetryMode(config, resource, otlpEndpoint, serviceName, langfuseRequested, hasLangfuseCreds) {
492
+ if ((!langfuseRequested || !hasLangfuseCreds) && !otlpEndpoint) {
493
+ if (langfuseRequested && !hasLangfuseCreds) {
494
+ logger.warn(`${LOG_PREFIX} Langfuse requested but credentials are missing, and no OTLP endpoint is configured; skipping initialization`, {
495
+ hasPublicKey: !!config.publicKey,
496
+ hasSecretKey: !!config.secretKey,
497
+ });
498
+ }
499
+ else {
500
+ logger.debug(`${LOG_PREFIX} Langfuse disabled and OTLP endpoint missing, skipping initialization`);
501
+ }
459
502
  isInitialized = true;
460
503
  return;
461
504
  }
462
- // Validate credentials for standalone mode
463
- if (!config.publicKey || !config.secretKey) {
464
- logger.warn(`${LOG_PREFIX} Langfuse enabled but missing credentials, skipping initialization`, {
505
+ if (langfuseRequested && !hasLangfuseCreds) {
506
+ logger.warn(`${LOG_PREFIX} Langfuse requested but credentials are missing; continuing with OTLP-only telemetry`, {
465
507
  hasPublicKey: !!config.publicKey,
466
508
  hasSecretKey: !!config.secretKey,
509
+ otlpEnabled: !!otlpEndpoint,
467
510
  });
468
- isInitialized = true;
469
- isCredentialsValid = false;
470
- return;
471
511
  }
472
512
  try {
473
513
  currentConfig = config;
474
- isCredentialsValid = true;
475
- // Step 1: Create LangfuseSpanProcessor for standalone mode
476
- // shouldExportSpan: export all spans (v5 default filters to gen_ai spans only)
477
- langfuseProcessor = new LangfuseSpanProcessor({
478
- publicKey: config.publicKey,
479
- secretKey: config.secretKey,
480
- baseUrl: config.baseUrl || "https://cloud.langfuse.com",
481
- environment: config.environment || "dev",
482
- release: config.release || "v1.0.0",
483
- shouldExportSpan: () => true,
484
- });
485
- logger.debug(`${LOG_PREFIX} Created LangfuseSpanProcessor`, {
514
+ isCredentialsValid = hasLangfuseCreds;
515
+ langfuseProcessor =
516
+ langfuseRequested && hasLangfuseCreds
517
+ ? createLangfuseProcessor(config)
518
+ : null;
519
+ logger.debug(`${LOG_PREFIX} Standalone observability mode`, {
520
+ langfuseEnabled: !!langfuseProcessor,
521
+ otlpEnabled: !!otlpEndpoint,
486
522
  baseUrl: config.baseUrl || "https://cloud.langfuse.com",
487
523
  environment: config.environment || "dev",
488
524
  });
489
- // Step 2: Create our own TracerProvider (standalone behavior)
490
- const resource = resourceFromAttributes({
491
- [ATTR_SERVICE_NAME]: "neurolink",
492
- [ATTR_SERVICE_VERSION]: config.release || "v1.0.0",
493
- "deployment.environment": config.environment || "dev",
494
- });
495
- tracerProvider = new NodeTracerProvider({
496
- resource,
497
- spanProcessors: [new ContextEnricher(), langfuseProcessor],
525
+ const spanProcessors = [new ContextEnricher()];
526
+ if (langfuseProcessor) {
527
+ spanProcessors.push(langfuseProcessor);
528
+ }
529
+ if (otlpEndpoint) {
530
+ try {
531
+ const otlpExporter = new OTLPTraceExporter({
532
+ url: `${otlpEndpoint}/v1/traces`,
533
+ });
534
+ spanProcessors.push(new BatchSpanProcessor(otlpExporter, {
535
+ maxQueueSize: 2048,
536
+ maxExportBatchSize: 512,
537
+ scheduledDelayMillis: 1000,
538
+ exportTimeoutMillis: 30000,
539
+ }));
540
+ logger.info(`${LOG_PREFIX} OTLP trace exporter added`, {
541
+ endpoint: `${otlpEndpoint}/v1/traces`,
542
+ serviceName,
543
+ });
544
+ }
545
+ catch (otlpError) {
546
+ logger.warn(`${LOG_PREFIX} Failed to create OTLP exporter (non-fatal)`, {
547
+ error: otlpError instanceof Error
548
+ ? otlpError.message
549
+ : String(otlpError),
550
+ endpoint: otlpEndpoint,
551
+ });
552
+ }
553
+ }
554
+ tracerProvider = new NodeTracerProvider({ resource, spanProcessors });
555
+ tracerProvider.register({
556
+ propagator: new W3CTraceContextPropagator(),
498
557
  });
499
- // Step 4: Register globally
500
- tracerProvider.register();
501
558
  usingExternalProvider = false;
502
559
  isInitialized = true;
503
- logger.info(`${LOG_PREFIX} Initialized with Langfuse span processor`, {
560
+ initializeOtlpMetricsAndLogs(resource, otlpEndpoint, serviceName);
561
+ logger.info(`${LOG_PREFIX} Observability initialized`, {
504
562
  baseUrl: config.baseUrl || "https://cloud.langfuse.com",
505
563
  environment: config.environment || "dev",
506
564
  release: config.release || "v1.0.0",
507
565
  mode: "standalone",
566
+ langfuseEnabled: !!langfuseProcessor,
567
+ otlpEnabled: !!otlpEndpoint,
568
+ serviceName,
508
569
  });
509
570
  }
510
571
  catch (error) {
511
- // Check if this is a duplicate registration error
512
572
  const errorMessage = error instanceof Error ? error.message : String(error);
513
573
  const isDuplicateError = errorMessage.includes("duplicate registration") ||
514
574
  errorMessage.includes("already registered") ||
515
575
  errorMessage.includes("already set");
516
576
  if (isDuplicateError) {
517
- // Graceful handling: switch to external mode
518
577
  logger.warn(`${LOG_PREFIX} TracerProvider already registered, switching to external mode`, {
519
578
  error: errorMessage,
520
579
  recommendation: "Set useExternalTracerProvider=true or autoDetectExternalProvider=true in config",
521
580
  });
522
581
  usingExternalProvider = true;
523
582
  isInitialized = true;
524
- // Don't throw - processors are still usable
525
583
  return;
526
584
  }
527
- // Other errors: log and re-throw
528
585
  logger.error(`${LOG_PREFIX} Initialization failed`, {
529
586
  error: errorMessage,
530
587
  stack: error instanceof Error ? error.stack : undefined,
@@ -532,6 +589,55 @@ export function initializeOpenTelemetry(config) {
532
589
  throw error;
533
590
  }
534
591
  }
592
+ /**
593
+ * Initialize OpenTelemetry with Langfuse span processor
594
+ *
595
+ * This connects Vercel AI SDK's experimental_telemetry to Langfuse by:
596
+ * 1. Creating LangfuseSpanProcessor with Langfuse credentials
597
+ * 2. Creating a NodeTracerProvider with service metadata and span processor
598
+ * 3. Registering the provider globally for AI SDK to use
599
+ *
600
+ * NEW: If useExternalTracerProvider is true or autoDetectExternalProvider detects
601
+ * an existing provider, steps 2 and 3 are skipped. The span processors are still
602
+ * created and can be retrieved via getSpanProcessors().
603
+ *
604
+ * @param config - Langfuse configuration passed from parent application
605
+ */
606
+ export function initializeOpenTelemetry(config) {
607
+ // Guard against multiple initializations — but always update config
608
+ // so that later NeuroLink instances can change traceNameFormat,
609
+ // autoDetectOperationName, and other configuration preferences
610
+ // without re-initializing the OTEL infrastructure.
611
+ if (isInitialized) {
612
+ currentConfig = config;
613
+ logger.debug(`${LOG_PREFIX} Already initialized, config updated`, {
614
+ usingExternalProvider,
615
+ hasLangfuseProcessor: !!langfuseProcessor,
616
+ hasTraceNameFormat: typeof config.traceNameFormat === "function",
617
+ });
618
+ return;
619
+ }
620
+ // FIRST: Check for external provider mode - bypasses enabled check
621
+ // NOTE: When autoDetectExternalProvider is true, we trust the flag directly rather than
622
+ // calling hasExternalTracerProvider(). This is because Neurolink may bundle its own copy
623
+ // of @opentelemetry/api, which has a separate global state from the host application.
624
+ // The hasExternalTracerProvider() check would query Neurolink's bundled @opentelemetry/api
625
+ // global state (which has no provider registered), not the host's global state.
626
+ // By trusting autoDetectExternalProvider=true, we let the host application signal that
627
+ // it has already registered a TracerProvider.
628
+ const shouldUseExternal = config?.useExternalTracerProvider === true ||
629
+ config?.autoDetectExternalProvider === true;
630
+ const otlpEndpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
631
+ const langfuseRequested = config?.enabled === true;
632
+ const hasLangfuseCreds = !!config.publicKey && !!config.secretKey;
633
+ const serviceName = process.env.OTEL_SERVICE_NAME || "neurolink";
634
+ const resource = createOtelResource(config, serviceName);
635
+ if (shouldUseExternal) {
636
+ initializeExternalOpenTelemetryMode(config, resource, otlpEndpoint, serviceName, langfuseRequested, hasLangfuseCreds);
637
+ return;
638
+ }
639
+ initializeStandaloneOpenTelemetryMode(config, resource, otlpEndpoint, serviceName, langfuseRequested, hasLangfuseCreds);
640
+ }
535
641
  /**
536
642
  * Flush all pending spans to Langfuse
537
643
  */
@@ -540,22 +646,75 @@ export async function flushOpenTelemetry() {
540
646
  logger.debug(`${LOG_PREFIX} Not initialized, skipping flush`);
541
647
  return;
542
648
  }
543
- if (!langfuseProcessor) {
544
- logger.debug(`${LOG_PREFIX} No processor to flush (Langfuse disabled)`);
545
- return;
649
+ const failures = [];
650
+ if (langfuseProcessor) {
651
+ try {
652
+ logger.info(`${LOG_PREFIX} Flushing Langfuse spans...`);
653
+ await langfuseProcessor.forceFlush();
654
+ }
655
+ catch (error) {
656
+ failures.push({ signal: "langfuse", error });
657
+ logger.error(`${LOG_PREFIX} Langfuse flush failed`, {
658
+ error: error instanceof Error ? error.message : String(error),
659
+ stack: error instanceof Error ? error.stack : undefined,
660
+ });
661
+ }
546
662
  }
547
- try {
548
- logger.info(`${LOG_PREFIX} Flushing pending spans to Langfuse...`);
549
- await langfuseProcessor.forceFlush();
550
- logger.info(`${LOG_PREFIX} Successfully flushed spans to Langfuse`);
663
+ else {
664
+ logger.debug(`${LOG_PREFIX} Langfuse disabled, skipping Langfuse flush`);
551
665
  }
552
- catch (error) {
553
- logger.error(`${LOG_PREFIX} Flush failed`, {
554
- error: error instanceof Error ? error.message : String(error),
555
- stack: error instanceof Error ? error.stack : undefined,
556
- });
557
- throw error;
666
+ if (tracerProvider && !usingExternalProvider) {
667
+ try {
668
+ logger.info(`${LOG_PREFIX} Flushing OTLP traces...`);
669
+ await tracerProvider.forceFlush();
670
+ }
671
+ catch (error) {
672
+ failures.push({ signal: "traces", error });
673
+ logger.error(`${LOG_PREFIX} Trace flush failed`, {
674
+ error: error instanceof Error ? error.message : String(error),
675
+ stack: error instanceof Error ? error.stack : undefined,
676
+ });
677
+ }
558
678
  }
679
+ else {
680
+ logger.debug(`${LOG_PREFIX} No TracerProvider to flush`);
681
+ }
682
+ if (meterProvider) {
683
+ try {
684
+ logger.info(`${LOG_PREFIX} Flushing OTLP metrics...`);
685
+ await meterProvider.forceFlush();
686
+ }
687
+ catch (error) {
688
+ failures.push({ signal: "metrics", error });
689
+ logger.error(`${LOG_PREFIX} Metric flush failed`, {
690
+ error: error instanceof Error ? error.message : String(error),
691
+ stack: error instanceof Error ? error.stack : undefined,
692
+ });
693
+ }
694
+ }
695
+ else {
696
+ logger.debug(`${LOG_PREFIX} No MeterProvider to flush`);
697
+ }
698
+ if (loggerProvider) {
699
+ try {
700
+ logger.info(`${LOG_PREFIX} Flushing OTLP logs...`);
701
+ await loggerProvider.forceFlush();
702
+ }
703
+ catch (error) {
704
+ failures.push({ signal: "logs", error });
705
+ logger.error(`${LOG_PREFIX} Log flush failed`, {
706
+ error: error instanceof Error ? error.message : String(error),
707
+ stack: error instanceof Error ? error.stack : undefined,
708
+ });
709
+ }
710
+ }
711
+ else {
712
+ logger.debug(`${LOG_PREFIX} No LoggerProvider to flush`);
713
+ }
714
+ if (failures.length > 0) {
715
+ throw new Error(`${LOG_PREFIX} Flush failed for: ${failures.map((f) => f.signal).join(", ")}`);
716
+ }
717
+ logger.info(`${LOG_PREFIX} Flush complete`);
559
718
  }
560
719
  /**
561
720
  * Shutdown OpenTelemetry and Langfuse span processor
@@ -577,7 +736,17 @@ export async function shutdownOpenTelemetry() {
577
736
  if (cachedContextEnricher) {
578
737
  await cachedContextEnricher.shutdown();
579
738
  }
739
+ // Shutdown MeterProvider if we created it
740
+ if (meterProvider) {
741
+ await meterProvider.shutdown();
742
+ }
743
+ // Shutdown LoggerProvider if we created it
744
+ if (loggerProvider) {
745
+ await loggerProvider.shutdown();
746
+ }
580
747
  tracerProvider = null;
748
+ meterProvider = null;
749
+ loggerProvider = null;
581
750
  langfuseProcessor = null;
582
751
  cachedContextEnricher = null;
583
752
  isInitialized = false;
@@ -603,6 +772,13 @@ export function getLangfuseSpanProcessor() {
603
772
  export function getTracerProvider() {
604
773
  return tracerProvider;
605
774
  }
775
+ /**
776
+ * Get the logger provider for emitting OTLP log records.
777
+ * Returns null if OTLP is not configured or LoggerProvider was not created.
778
+ */
779
+ export function getLoggerProvider() {
780
+ return loggerProvider;
781
+ }
606
782
  /**
607
783
  * Check if OpenTelemetry is initialized
608
784
  */
@@ -29,4 +29,5 @@ export declare class BullMQBackend implements TaskBackend {
29
29
  */
30
30
  private getConnectionConfig;
31
31
  private ensureInitialized;
32
+ private getQueue;
32
33
  }