@juspay/neurolink 9.42.0 → 9.43.0

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 (116) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/auth/anthropicOAuth.js +12 -0
  3. package/dist/browser/neurolink.min.js +335 -334
  4. package/dist/cli/commands/mcp.d.ts +6 -0
  5. package/dist/cli/commands/mcp.js +200 -184
  6. package/dist/cli/commands/proxy.js +560 -518
  7. package/dist/core/baseProvider.d.ts +6 -1
  8. package/dist/core/baseProvider.js +219 -232
  9. package/dist/core/factory.d.ts +3 -0
  10. package/dist/core/factory.js +140 -190
  11. package/dist/core/modules/ToolsManager.d.ts +1 -0
  12. package/dist/core/modules/ToolsManager.js +40 -42
  13. package/dist/core/toolEvents.d.ts +3 -0
  14. package/dist/core/toolEvents.js +7 -0
  15. package/dist/evaluation/pipeline/evaluationPipeline.js +5 -2
  16. package/dist/evaluation/scorers/scorerRegistry.d.ts +3 -0
  17. package/dist/evaluation/scorers/scorerRegistry.js +356 -284
  18. package/dist/lib/auth/anthropicOAuth.js +12 -0
  19. package/dist/lib/core/baseProvider.d.ts +6 -1
  20. package/dist/lib/core/baseProvider.js +219 -232
  21. package/dist/lib/core/factory.d.ts +3 -0
  22. package/dist/lib/core/factory.js +140 -190
  23. package/dist/lib/core/modules/ToolsManager.d.ts +1 -0
  24. package/dist/lib/core/modules/ToolsManager.js +40 -42
  25. package/dist/lib/core/toolEvents.d.ts +3 -0
  26. package/dist/lib/core/toolEvents.js +8 -0
  27. package/dist/lib/evaluation/pipeline/evaluationPipeline.js +5 -2
  28. package/dist/lib/evaluation/scorers/scorerRegistry.d.ts +3 -0
  29. package/dist/lib/evaluation/scorers/scorerRegistry.js +356 -284
  30. package/dist/lib/mcp/toolRegistry.d.ts +2 -0
  31. package/dist/lib/mcp/toolRegistry.js +32 -31
  32. package/dist/lib/neurolink.d.ts +38 -0
  33. package/dist/lib/neurolink.js +1890 -1707
  34. package/dist/lib/providers/googleAiStudio.js +0 -5
  35. package/dist/lib/providers/googleNativeGemini3.d.ts +4 -0
  36. package/dist/lib/providers/googleNativeGemini3.js +39 -1
  37. package/dist/lib/providers/googleVertex.d.ts +10 -0
  38. package/dist/lib/providers/googleVertex.js +445 -445
  39. package/dist/lib/providers/litellm.d.ts +1 -0
  40. package/dist/lib/providers/litellm.js +73 -64
  41. package/dist/lib/providers/ollama.js +17 -4
  42. package/dist/lib/providers/openAI.d.ts +2 -0
  43. package/dist/lib/providers/openAI.js +139 -140
  44. package/dist/lib/proxy/claudeFormat.js +14 -5
  45. package/dist/lib/proxy/oauthFetch.js +298 -318
  46. package/dist/lib/proxy/proxyConfig.js +3 -1
  47. package/dist/lib/proxy/proxyFetch.js +250 -222
  48. package/dist/lib/proxy/proxyHealth.d.ts +17 -0
  49. package/dist/lib/proxy/proxyHealth.js +55 -0
  50. package/dist/lib/proxy/requestLogger.js +140 -48
  51. package/dist/lib/proxy/routingPolicy.d.ts +33 -0
  52. package/dist/lib/proxy/routingPolicy.js +255 -0
  53. package/dist/lib/proxy/snapshotPersistence.d.ts +2 -0
  54. package/dist/lib/proxy/snapshotPersistence.js +41 -0
  55. package/dist/lib/proxy/sseInterceptor.js +36 -11
  56. package/dist/lib/server/routes/claudeProxyRoutes.d.ts +2 -1
  57. package/dist/lib/server/routes/claudeProxyRoutes.js +2916 -2377
  58. package/dist/lib/services/server/ai/observability/instrumentation.js +194 -218
  59. package/dist/lib/tasks/backends/bullmqBackend.js +24 -18
  60. package/dist/lib/tasks/store/redisTaskStore.js +42 -17
  61. package/dist/lib/tasks/taskManager.d.ts +2 -0
  62. package/dist/lib/tasks/taskManager.js +100 -5
  63. package/dist/lib/telemetry/telemetryService.js +9 -5
  64. package/dist/lib/types/cli.d.ts +4 -0
  65. package/dist/lib/types/proxyTypes.d.ts +211 -1
  66. package/dist/lib/types/tools.d.ts +18 -0
  67. package/dist/lib/utils/providerHealth.d.ts +1 -0
  68. package/dist/lib/utils/providerHealth.js +46 -31
  69. package/dist/lib/utils/providerUtils.js +11 -22
  70. package/dist/lib/utils/schemaConversion.d.ts +1 -0
  71. package/dist/lib/utils/schemaConversion.js +3 -0
  72. package/dist/mcp/toolRegistry.d.ts +2 -0
  73. package/dist/mcp/toolRegistry.js +32 -31
  74. package/dist/neurolink.d.ts +38 -0
  75. package/dist/neurolink.js +1890 -1707
  76. package/dist/providers/googleAiStudio.js +0 -5
  77. package/dist/providers/googleNativeGemini3.d.ts +4 -0
  78. package/dist/providers/googleNativeGemini3.js +39 -1
  79. package/dist/providers/googleVertex.d.ts +10 -0
  80. package/dist/providers/googleVertex.js +445 -445
  81. package/dist/providers/litellm.d.ts +1 -0
  82. package/dist/providers/litellm.js +73 -64
  83. package/dist/providers/ollama.js +17 -4
  84. package/dist/providers/openAI.d.ts +2 -0
  85. package/dist/providers/openAI.js +139 -140
  86. package/dist/proxy/claudeFormat.js +14 -5
  87. package/dist/proxy/oauthFetch.js +298 -318
  88. package/dist/proxy/proxyConfig.js +3 -1
  89. package/dist/proxy/proxyFetch.js +250 -222
  90. package/dist/proxy/proxyHealth.d.ts +17 -0
  91. package/dist/proxy/proxyHealth.js +54 -0
  92. package/dist/proxy/requestLogger.js +140 -48
  93. package/dist/proxy/routingPolicy.d.ts +33 -0
  94. package/dist/proxy/routingPolicy.js +254 -0
  95. package/dist/proxy/snapshotPersistence.d.ts +2 -0
  96. package/dist/proxy/snapshotPersistence.js +40 -0
  97. package/dist/proxy/sseInterceptor.js +36 -11
  98. package/dist/server/routes/claudeProxyRoutes.d.ts +2 -1
  99. package/dist/server/routes/claudeProxyRoutes.js +2916 -2377
  100. package/dist/services/server/ai/observability/instrumentation.js +194 -218
  101. package/dist/tasks/backends/bullmqBackend.js +24 -18
  102. package/dist/tasks/store/redisTaskStore.js +42 -17
  103. package/dist/tasks/taskManager.d.ts +2 -0
  104. package/dist/tasks/taskManager.js +100 -5
  105. package/dist/telemetry/telemetryService.js +9 -5
  106. package/dist/types/cli.d.ts +4 -0
  107. package/dist/types/proxyTypes.d.ts +211 -1
  108. package/dist/types/tools.d.ts +18 -0
  109. package/dist/utils/providerHealth.d.ts +1 -0
  110. package/dist/utils/providerHealth.js +46 -31
  111. package/dist/utils/providerUtils.js +12 -22
  112. package/dist/utils/schemaConversion.d.ts +1 -0
  113. package/dist/utils/schemaConversion.js +3 -0
  114. package/package.json +3 -2
  115. package/scripts/observability/check-proxy-telemetry.mjs +1 -1
  116. package/scripts/observability/manage-local-openobserve.sh +36 -5
@@ -21,6 +21,72 @@ import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION, } from "@opentelemetry/semanti
21
21
  import { AsyncLocalStorage } from "async_hooks";
22
22
  import { logger } from "../../../../utils/logger.js";
23
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
+ }
24
90
  const contextStorage = new AsyncLocalStorage();
25
91
  let tracerProvider = null;
26
92
  let meterProvider = null;
@@ -337,48 +403,20 @@ class ContextEnricher {
337
403
  return Promise.resolve();
338
404
  }
339
405
  }
340
- /**
341
- * Initialize OpenTelemetry with Langfuse span processor
342
- *
343
- * This connects Vercel AI SDK's experimental_telemetry to Langfuse by:
344
- * 1. Creating LangfuseSpanProcessor with Langfuse credentials
345
- * 2. Creating a NodeTracerProvider with service metadata and span processor
346
- * 3. Registering the provider globally for AI SDK to use
347
- *
348
- * NEW: If useExternalTracerProvider is true or autoDetectExternalProvider detects
349
- * an existing provider, steps 2 and 3 are skipped. The span processors are still
350
- * created and can be retrieved via getSpanProcessors().
351
- *
352
- * @param config - Langfuse configuration passed from parent application
353
- */
354
- export function initializeOpenTelemetry(config) {
355
- // Guard against multiple initializations — but always update config
356
- // so that later NeuroLink instances can change traceNameFormat,
357
- // autoDetectOperationName, and other configuration preferences
358
- // without re-initializing the OTEL infrastructure.
359
- if (isInitialized) {
360
- currentConfig = config;
361
- logger.debug(`${LOG_PREFIX} Already initialized, config updated`, {
362
- usingExternalProvider,
363
- hasLangfuseProcessor: !!langfuseProcessor,
364
- hasTraceNameFormat: typeof config.traceNameFormat === "function",
365
- });
366
- return;
367
- }
368
- // FIRST: Check for external provider mode - bypasses enabled check
369
- // NOTE: When autoDetectExternalProvider is true, we trust the flag directly rather than
370
- // calling hasExternalTracerProvider(). This is because Neurolink may bundle its own copy
371
- // of @opentelemetry/api, which has a separate global state from the host application.
372
- // The hasExternalTracerProvider() check would query Neurolink's bundled @opentelemetry/api
373
- // global state (which has no provider registered), not the host's global state.
374
- // By trusting autoDetectExternalProvider=true, we let the host application signal that
375
- // it has already registered a TracerProvider.
376
- const shouldUseExternal = config?.useExternalTracerProvider === true ||
377
- config?.autoDetectExternalProvider === true;
378
- if (shouldUseExternal) {
379
- // Validate credentials even in external mode
380
- if (!config?.publicKey || !config?.secretKey) {
381
- 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`, {
382
420
  hasPublicKey: !!config?.publicKey,
383
421
  hasSecretKey: !!config?.secretKey,
384
422
  });
@@ -386,86 +424,71 @@ export function initializeOpenTelemetry(config) {
386
424
  isCredentialsValid = false;
387
425
  return;
388
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);
389
443
  try {
390
- currentConfig = config;
391
- isCredentialsValid = true;
392
- // Create span processor for external provider mode
393
- // shouldExportSpan: export all spans (v5 default filters to gen_ai spans only)
394
- langfuseProcessor = new LangfuseSpanProcessor({
395
- publicKey: config.publicKey,
396
- secretKey: config.secretKey,
397
- baseUrl: config.baseUrl || "https://cloud.langfuse.com",
398
- environment: config.environment || "dev",
399
- release: config.release || "v1.0.0",
400
- shouldExportSpan: () => true,
401
- });
402
- usingExternalProvider = true;
403
- isInitialized = true;
404
- // Auto-register ContextEnricher with the global TracerProvider
405
- // This ensures trace names are set even when host doesn't call getSpanProcessors()
406
- try {
407
- const globalProvider = trace.getTracerProvider();
408
- // Check if it's a real provider with addSpanProcessor method (not the no-op default)
409
- if (globalProvider &&
410
- typeof globalProvider
411
- .addSpanProcessor === "function") {
412
- const provider = globalProvider;
413
- // Add ContextEnricher for trace name enrichment
414
- provider.addSpanProcessor(new ContextEnricher());
415
- // Only add LangfuseSpanProcessor if the host has not already registered one.
416
- // When skipLangfuseSpanProcessor is true, the host (e.g. Curator) already
417
- // registers its own LangfuseSpanProcessor via a DeferredSpanProcessor, so
418
- // adding another one here would cause duplicate trace exports to Langfuse.
419
- const skipLangfuse = config.skipLangfuseSpanProcessor === true;
420
- if (!skipLangfuse) {
421
- provider.addSpanProcessor(langfuseProcessor);
422
- }
423
- logger.info(`${LOG_PREFIX} Auto-registered processors with global TracerProvider`, {
424
- processors: skipLangfuse
425
- ? ["ContextEnricher"]
426
- : ["ContextEnricher", "LangfuseSpanProcessor"],
427
- reason: "External provider mode with auto-registration",
428
- skippedLangfuseSpanProcessor: skipLangfuse,
429
- });
430
- }
431
- else {
432
- // No real provider found - host will need to add processors manually
433
- logger.info(`${LOG_PREFIX} Using external TracerProvider mode`, {
434
- reason: config.useExternalTracerProvider
435
- ? "useExternalTracerProvider=true"
436
- : "autoDetectExternalProvider=true (trusting host signal)",
437
- instructions: "Add span processors to your TracerProvider using getSpanProcessors()",
438
- });
439
- logger.info(`${LOG_PREFIX} Span processors ready for external use`, {
440
- processors: ["ContextEnricher", "LangfuseSpanProcessor"],
441
- usage: "import { getSpanProcessors } from '@juspay/neurolink'",
442
- });
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);
443
451
  }
444
- }
445
- catch (autoRegisterError) {
446
- // Auto-registration failed - fall back to manual registration
447
- logger.warn(`${LOG_PREFIX} Auto-registration failed, manual registration required`, {
448
- error: autoRegisterError instanceof Error
449
- ? autoRegisterError.message
450
- : String(autoRegisterError),
451
- 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,
452
458
  });
459
+ return;
453
460
  }
454
- 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
+ });
455
473
  }
456
- catch (error) {
457
- logger.error(`${LOG_PREFIX} Failed to create span processor for external mode`, {
458
- error: error instanceof Error ? error.message : String(error),
459
- 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()",
460
480
  });
461
- isInitialized = true;
462
- return;
463
481
  }
464
482
  }
465
- const otlpEndpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
466
- const langfuseRequested = config?.enabled === true;
467
- const hasLangfuseCreds = !!config.publicKey && !!config.secretKey;
468
- // THEN: Check whether we have any standalone observability backend at all.
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) {
469
492
  if ((!langfuseRequested || !hasLangfuseCreds) && !otlpEndpoint) {
470
493
  if (langfuseRequested && !hasLangfuseCreds) {
471
494
  logger.warn(`${LOG_PREFIX} Langfuse requested but credentials are missing, and no OTLP endpoint is configured; skipping initialization`, {
@@ -489,55 +512,31 @@ export function initializeOpenTelemetry(config) {
489
512
  try {
490
513
  currentConfig = config;
491
514
  isCredentialsValid = hasLangfuseCreds;
492
- // Step 1: Create LangfuseSpanProcessor only when Langfuse is explicitly enabled
493
- // with real credentials. OTLP-only mode is valid and should not construct one.
494
- if (langfuseRequested && hasLangfuseCreds) {
495
- // shouldExportSpan: export all spans (v5 default filters to gen_ai spans only)
496
- langfuseProcessor = new LangfuseSpanProcessor({
497
- publicKey: config.publicKey,
498
- secretKey: config.secretKey,
499
- baseUrl: config.baseUrl || "https://cloud.langfuse.com",
500
- environment: config.environment || "dev",
501
- release: config.release || "v1.0.0",
502
- shouldExportSpan: () => true,
503
- });
504
- }
505
- else {
506
- langfuseProcessor = null;
507
- }
515
+ langfuseProcessor =
516
+ langfuseRequested && hasLangfuseCreds
517
+ ? createLangfuseProcessor(config)
518
+ : null;
508
519
  logger.debug(`${LOG_PREFIX} Standalone observability mode`, {
509
520
  langfuseEnabled: !!langfuseProcessor,
510
521
  otlpEnabled: !!otlpEndpoint,
511
522
  baseUrl: config.baseUrl || "https://cloud.langfuse.com",
512
523
  environment: config.environment || "dev",
513
524
  });
514
- // Step 2: Create our own TracerProvider (standalone behavior)
515
- // Use OTEL_SERVICE_NAME env var if available, otherwise "neurolink"
516
- const serviceName = process.env.OTEL_SERVICE_NAME || "neurolink";
517
- const resource = resourceFromAttributes({
518
- [ATTR_SERVICE_NAME]: serviceName,
519
- [ATTR_SERVICE_VERSION]: config.release || "v1.0.0",
520
- "deployment.environment": config.environment || "dev",
521
- });
522
- // Build span processor list
523
525
  const spanProcessors = [new ContextEnricher()];
524
526
  if (langfuseProcessor) {
525
527
  spanProcessors.push(langfuseProcessor);
526
528
  }
527
- // Step 2b: If OTEL_EXPORTER_OTLP_ENDPOINT is set, also export via OTLP HTTP
528
- // This allows sending traces to an OpenTelemetry Collector (e.g. for OpenObserve)
529
529
  if (otlpEndpoint) {
530
530
  try {
531
531
  const otlpExporter = new OTLPTraceExporter({
532
532
  url: `${otlpEndpoint}/v1/traces`,
533
533
  });
534
- const otlpBatchProcessor = new BatchSpanProcessor(otlpExporter, {
534
+ spanProcessors.push(new BatchSpanProcessor(otlpExporter, {
535
535
  maxQueueSize: 2048,
536
536
  maxExportBatchSize: 512,
537
537
  scheduledDelayMillis: 1000,
538
538
  exportTimeoutMillis: 30000,
539
- });
540
- spanProcessors.push(otlpBatchProcessor);
539
+ }));
541
540
  logger.info(`${LOG_PREFIX} OTLP trace exporter added`, {
542
541
  endpoint: `${otlpEndpoint}/v1/traces`,
543
542
  serviceName,
@@ -552,81 +551,13 @@ export function initializeOpenTelemetry(config) {
552
551
  });
553
552
  }
554
553
  }
555
- tracerProvider = new NodeTracerProvider({
556
- resource,
557
- spanProcessors,
558
- });
559
- // Step 4: Register globally with explicit W3C propagator
560
- // This ensures traceparent headers from calling SDKs are extracted correctly,
561
- // even if another library registers a no-op propagator before us.
554
+ tracerProvider = new NodeTracerProvider({ resource, spanProcessors });
562
555
  tracerProvider.register({
563
556
  propagator: new W3CTraceContextPropagator(),
564
557
  });
565
558
  usingExternalProvider = false;
566
559
  isInitialized = true;
567
- // Step 5: If OTLP endpoint is set, also set up MeterProvider for metrics export
568
- // This enables TelemetryService's metrics.getMeter() instruments to export via OTLP
569
- if (otlpEndpoint) {
570
- try {
571
- const metricExporter = new OTLPMetricExporter({
572
- url: `${otlpEndpoint}/v1/metrics`,
573
- });
574
- const metricReader = new PeriodicExportingMetricReader({
575
- exporter: metricExporter,
576
- exportIntervalMillis: 15000, // Export every 15 seconds
577
- exportTimeoutMillis: 10000,
578
- });
579
- meterProvider = new MeterProvider({
580
- resource,
581
- readers: [metricReader],
582
- });
583
- // Register globally so TelemetryService's metrics.getMeter() picks it up
584
- metrics.setGlobalMeterProvider(meterProvider);
585
- logger.info(`${LOG_PREFIX} OTLP metric exporter added — MeterProvider registered globally`, {
586
- endpoint: `${otlpEndpoint}/v1/metrics`,
587
- exportIntervalMs: 15000,
588
- serviceName,
589
- meterProviderType: meterProvider.constructor.name,
590
- });
591
- }
592
- catch (metricsError) {
593
- logger.warn(`${LOG_PREFIX} Failed to create OTLP metric exporter (non-fatal)`, {
594
- error: metricsError instanceof Error
595
- ? metricsError.message
596
- : String(metricsError),
597
- endpoint: otlpEndpoint,
598
- });
599
- }
600
- // Step 6: Set up LoggerProvider for OTLP log export
601
- // This enables logRequest() to emit structured log records via OTLP
602
- try {
603
- const logExporter = new OTLPLogExporter({
604
- url: `${otlpEndpoint}/v1/logs`,
605
- });
606
- const logProcessor = new BatchLogRecordProcessor(logExporter, {
607
- maxQueueSize: 2048,
608
- maxExportBatchSize: 512,
609
- scheduledDelayMillis: 2000,
610
- exportTimeoutMillis: 30000,
611
- });
612
- loggerProvider = new LoggerProvider({
613
- resource,
614
- processors: [logProcessor],
615
- });
616
- logger.info(`${LOG_PREFIX} OTLP log exporter added — LoggerProvider created`, {
617
- endpoint: `${otlpEndpoint}/v1/logs`,
618
- serviceName,
619
- });
620
- }
621
- catch (logsError) {
622
- logger.warn(`${LOG_PREFIX} Failed to create OTLP log exporter (non-fatal)`, {
623
- error: logsError instanceof Error
624
- ? logsError.message
625
- : String(logsError),
626
- endpoint: otlpEndpoint,
627
- });
628
- }
629
- }
560
+ initializeOtlpMetricsAndLogs(resource, otlpEndpoint, serviceName);
630
561
  logger.info(`${LOG_PREFIX} Observability initialized`, {
631
562
  baseUrl: config.baseUrl || "https://cloud.langfuse.com",
632
563
  environment: config.environment || "dev",
@@ -638,23 +569,19 @@ export function initializeOpenTelemetry(config) {
638
569
  });
639
570
  }
640
571
  catch (error) {
641
- // Check if this is a duplicate registration error
642
572
  const errorMessage = error instanceof Error ? error.message : String(error);
643
573
  const isDuplicateError = errorMessage.includes("duplicate registration") ||
644
574
  errorMessage.includes("already registered") ||
645
575
  errorMessage.includes("already set");
646
576
  if (isDuplicateError) {
647
- // Graceful handling: switch to external mode
648
577
  logger.warn(`${LOG_PREFIX} TracerProvider already registered, switching to external mode`, {
649
578
  error: errorMessage,
650
579
  recommendation: "Set useExternalTracerProvider=true or autoDetectExternalProvider=true in config",
651
580
  });
652
581
  usingExternalProvider = true;
653
582
  isInitialized = true;
654
- // Don't throw - processors are still usable
655
583
  return;
656
584
  }
657
- // Other errors: log and re-throw
658
585
  logger.error(`${LOG_PREFIX} Initialization failed`, {
659
586
  error: errorMessage,
660
587
  stack: error instanceof Error ? error.stack : undefined,
@@ -662,6 +589,55 @@ export function initializeOpenTelemetry(config) {
662
589
  throw error;
663
590
  }
664
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
+ }
665
641
  /**
666
642
  * Flush all pending spans to Langfuse
667
643
  */
@@ -64,28 +64,34 @@ export class BullMQBackend {
64
64
  async schedule(task, executor) {
65
65
  const queue = this.getQueue();
66
66
  this.executors.set(task.id, executor);
67
- const jobData = { taskId: task.id, task };
68
- const schedule = task.schedule;
69
- if (schedule.type === "cron") {
70
- await queue.upsertJobScheduler(task.id, {
71
- pattern: schedule.expression,
72
- ...(schedule.timezone ? { tz: schedule.timezone } : {}),
73
- }, { name: task.name, data: jobData });
74
- }
75
- else if (schedule.type === "interval") {
76
- await queue.upsertJobScheduler(task.id, { every: schedule.every }, { name: task.name, data: jobData });
67
+ try {
68
+ const jobData = { taskId: task.id, task };
69
+ const schedule = task.schedule;
70
+ if (schedule.type === "cron") {
71
+ await queue.upsertJobScheduler(task.id, {
72
+ pattern: schedule.expression,
73
+ ...(schedule.timezone ? { tz: schedule.timezone } : {}),
74
+ }, { name: task.name, data: jobData });
75
+ }
76
+ else if (schedule.type === "interval") {
77
+ await queue.upsertJobScheduler(task.id, { every: schedule.every }, { name: task.name, data: jobData });
78
+ }
79
+ else if (schedule.type === "once") {
80
+ const at = typeof schedule.at === "string" ? new Date(schedule.at) : schedule.at;
81
+ const delay = Math.max(0, at.getTime() - Date.now());
82
+ await queue.add(task.name, jobData, {
83
+ jobId: task.id,
84
+ delay,
85
+ });
86
+ }
77
87
  }
78
- else if (schedule.type === "once") {
79
- const at = typeof schedule.at === "string" ? new Date(schedule.at) : schedule.at;
80
- const delay = Math.max(0, at.getTime() - Date.now());
81
- await queue.add(task.name, jobData, {
82
- jobId: task.id,
83
- delay,
84
- });
88
+ catch (error) {
89
+ this.executors.delete(task.id);
90
+ throw error;
85
91
  }
86
92
  logger.info("[BullMQ] Task scheduled", {
87
93
  taskId: task.id,
88
- type: schedule.type,
94
+ type: task.schedule.type,
89
95
  });
90
96
  }
91
97
  async cancel(taskId) {
@@ -63,7 +63,7 @@ export class RedisTaskStore {
63
63
  async save(task) {
64
64
  const client = this.getClient();
65
65
  await client.hSet(TASKS_HASH, task.id, JSON.stringify(task));
66
- this.applyRetentionTTL(task);
66
+ this.applyRetentionTTL(task, client);
67
67
  }
68
68
  async get(taskId) {
69
69
  const client = this.getClient();
@@ -95,7 +95,7 @@ export class RedisTaskStore {
95
95
  updatedAt: new Date().toISOString(),
96
96
  };
97
97
  await client.hSet(TASKS_HASH, taskId, JSON.stringify(updated));
98
- this.applyRetentionTTL(updated);
98
+ this.applyRetentionTTL(updated, client);
99
99
  return updated;
100
100
  }
101
101
  async delete(taskId) {
@@ -167,7 +167,7 @@ export class RedisTaskStore {
167
167
  * Set Redis TTL on terminal-state tasks so they auto-expire.
168
168
  * Active and paused tasks never expire.
169
169
  */
170
- applyRetentionTTL(task) {
170
+ applyRetentionTTL(task, client) {
171
171
  // We don't set EXPIRE on the hash field directly (Redis doesn't support per-field TTL).
172
172
  // Instead, run logs and history keys get TTL. The task hash field itself must be
173
173
  // cleaned up via manual deletion or BullMQ's built-in job cleanup.
@@ -177,21 +177,46 @@ export class RedisTaskStore {
177
177
  cancelled: this.retentionConfig.cancelledTTL,
178
178
  };
179
179
  const ttlMs = ttlMap[task.status];
180
- if (ttlMs) {
181
- const client = this.getClient();
182
- const ttlSeconds = Math.ceil(ttlMs / 1000);
183
- // Set TTL on associated keys
184
- client.expire(taskRunsKey(task.id), ttlSeconds).catch((err) => {
185
- logger.debug("[TaskStore:Redis] Failed to set TTL", {
186
- error: String(err),
187
- });
188
- });
189
- client.expire(taskHistoryKey(task.id), ttlSeconds).catch((err) => {
190
- logger.debug("[TaskStore:Redis] Failed to set TTL", {
191
- error: String(err),
192
- });
193
- });
180
+ if (!ttlMs || !client.isOpen) {
181
+ return;
194
182
  }
183
+ const ttlSeconds = Math.ceil(ttlMs / 1000);
184
+ // Set TTL on associated keys best-effort. A successful task write should not
185
+ // be surfaced as a failure just because the retention metadata could not be updated.
186
+ void (async () => {
187
+ const runsKey = taskRunsKey(task.id);
188
+ for (let attempt = 1; attempt <= 3; attempt++) {
189
+ try {
190
+ await client.expire(runsKey, ttlSeconds);
191
+ break;
192
+ }
193
+ catch (err) {
194
+ if (attempt === 3) {
195
+ logger.warn("[TaskStore:Redis] expire failed after 3 attempts on task runs key — task data may outlive retention window", { taskId: task.id, key: runsKey, ttlSeconds, err: String(err) });
196
+ }
197
+ else {
198
+ await new Promise((r) => setTimeout(r, 100 * attempt));
199
+ }
200
+ }
201
+ }
202
+ })();
203
+ void (async () => {
204
+ const histKey = taskHistoryKey(task.id);
205
+ for (let attempt = 1; attempt <= 3; attempt++) {
206
+ try {
207
+ await client.expire(histKey, ttlSeconds);
208
+ break;
209
+ }
210
+ catch (err) {
211
+ if (attempt === 3) {
212
+ logger.warn("[TaskStore:Redis] expire failed after 3 attempts on task history key — task data may outlive retention window", { taskId: task.id, key: histKey, ttlSeconds, err: String(err) });
213
+ }
214
+ else {
215
+ await new Promise((r) => setTimeout(r, 100 * attempt));
216
+ }
217
+ }
218
+ }
219
+ })();
195
220
  }
196
221
  }
197
222
  //# sourceMappingURL=redisTaskStore.js.map
@@ -49,6 +49,8 @@ export declare class TaskManager {
49
49
  shutdown(): Promise<void>;
50
50
  /** Check if the backend is healthy */
51
51
  isHealthy(): Promise<boolean>;
52
+ private restoreScheduledTask;
53
+ private rollbackTaskUpdate;
52
54
  /**
53
55
  * Called by the backend on each scheduled tick.
54
56
  * Executes the task, updates state, fires callbacks/events.