@juspay/neurolink 13.5.0 → 13.5.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 (118) hide show
  1. package/CHANGELOG.md +3 -3
  2. package/dist/adapters/video/videoAnalyzer.d.ts +1 -0
  3. package/dist/adapters/video/videoAnalyzer.js +4 -0
  4. package/dist/autoresearch/index.d.ts +1 -0
  5. package/dist/autoresearch/index.js +4 -0
  6. package/dist/autoresearch/worker.js +4 -0
  7. package/dist/browser/neurolink.min.js +467 -467
  8. package/dist/cli/commands/observability.js +11 -4
  9. package/dist/cli/commands/telemetry.js +36 -6
  10. package/dist/cli/utils/formatters.d.ts +17 -0
  11. package/dist/cli/utils/formatters.js +39 -0
  12. package/dist/constants/langfuse.d.ts +17 -1
  13. package/dist/constants/langfuse.js +23 -1
  14. package/dist/constants/telemetry.d.ts +6 -0
  15. package/dist/constants/telemetry.js +6 -0
  16. package/dist/core/baseProvider.js +2 -0
  17. package/dist/core/geminiLoopAdapter.js +19 -0
  18. package/dist/core/loopEngine.js +10 -7
  19. package/dist/core/modules/StreamHandler.js +5 -4
  20. package/dist/core/modules/TelemetryHandler.d.ts +5 -1
  21. package/dist/core/modules/TelemetryHandler.js +16 -0
  22. package/dist/core/modules/ToolsManager.js +3 -0
  23. package/dist/core/nativeGenerateLoop.js +1 -1
  24. package/dist/evaluation/index.d.ts +1 -0
  25. package/dist/evaluation/index.js +4 -0
  26. package/dist/evaluation/ragasEvaluator.js +1 -0
  27. package/dist/mcp/externalServerManager.js +6 -2
  28. package/dist/mcp/toolRegistry.js +3 -0
  29. package/dist/neurolink.d.ts +16 -8
  30. package/dist/neurolink.js +115 -61
  31. package/dist/observability/FEATURE-STATUS.md +0 -4
  32. package/dist/observability/exporterRegistry.d.ts +6 -1
  33. package/dist/observability/exporterRegistry.js +18 -0
  34. package/dist/observability/exporterSpanProcessor.d.ts +20 -6
  35. package/dist/observability/exporterSpanProcessor.js +166 -34
  36. package/dist/observability/exporters/baseExporter.d.ts +22 -3
  37. package/dist/observability/exporters/baseExporter.js +87 -5
  38. package/dist/observability/exporters/langfuseExporter.d.ts +29 -3
  39. package/dist/observability/exporters/langfuseExporter.js +251 -41
  40. package/dist/observability/langfuseMasking.d.ts +1 -1
  41. package/dist/observability/langfuseMasking.js +1 -1
  42. package/dist/observability/otlpEncoding.d.ts +18 -5
  43. package/dist/observability/otlpEncoding.js +69 -12
  44. package/dist/observability/utils/spanSerializer.d.ts +3 -2
  45. package/dist/observability/utils/spanSerializer.js +5 -3
  46. package/dist/processors/base/BaseFileProcessor.d.ts +1 -0
  47. package/dist/processors/base/BaseFileProcessor.js +4 -0
  48. package/dist/processors/index.d.ts +1 -0
  49. package/dist/processors/index.js +4 -0
  50. package/dist/processors/registry/ProcessorRegistry.d.ts +1 -0
  51. package/dist/processors/registry/ProcessorRegistry.js +5 -0
  52. package/dist/providers/amazonBedrock/client.js +7 -5
  53. package/dist/providers/amazonSagemaker.js +32 -12
  54. package/dist/providers/anthropic/client.js +1 -0
  55. package/dist/providers/anthropic/loopAdapter.js +7 -3
  56. package/dist/providers/googleAiStudio/client.js +2 -0
  57. package/dist/providers/googleVertex/client.d.ts +6 -5
  58. package/dist/providers/googleVertex/client.js +46 -19
  59. package/dist/providers/openaiChatCompletionsBase.js +4 -4
  60. package/dist/proxy/codexUsage.js +59 -2
  61. package/dist/proxy/proxyTracer.d.ts +25 -1
  62. package/dist/proxy/proxyTracer.js +146 -22
  63. package/dist/proxy/proxyTranslationEngine.js +4 -2
  64. package/dist/proxy/sseInterceptor.js +6 -4
  65. package/dist/rag/chunkers/BaseChunker.js +1 -0
  66. package/dist/rag/index.d.ts +1 -0
  67. package/dist/rag/index.js +4 -0
  68. package/dist/rag/ragIntegration.js +1 -0
  69. package/dist/rag/reranker/reranker.js +2 -0
  70. package/dist/rag/retrieval/vectorQueryTool.js +1 -0
  71. package/dist/server/routes/codexProxyRoutes.js +3 -1
  72. package/dist/server/utils/validation.d.ts +3 -3
  73. package/dist/services/server/ai/observability/instrumentation.d.ts +8 -7
  74. package/dist/services/server/ai/observability/instrumentation.js +217 -47
  75. package/dist/services/server/ai/observability/langfuseSetup.d.ts +19 -1
  76. package/dist/services/server/ai/observability/langfuseSetup.js +62 -4
  77. package/dist/services/server/ai/observability/otlpSetup.d.ts +12 -2
  78. package/dist/services/server/ai/observability/otlpSetup.js +55 -4
  79. package/dist/telemetry/aiMetrics.d.ts +56 -0
  80. package/dist/telemetry/aiMetrics.js +229 -0
  81. package/dist/telemetry/attributes.d.ts +24 -1
  82. package/dist/telemetry/attributes.js +94 -9
  83. package/dist/telemetry/chatMessages.d.ts +6 -0
  84. package/dist/telemetry/chatMessages.js +37 -0
  85. package/dist/telemetry/index.d.ts +1 -1
  86. package/dist/telemetry/index.js +1 -1
  87. package/dist/telemetry/observation.d.ts +5 -5
  88. package/dist/telemetry/observation.js +128 -39
  89. package/dist/telemetry/spanSanitizer.d.ts +2 -2
  90. package/dist/telemetry/spanSanitizer.js +76 -8
  91. package/dist/telemetry/telemetryStarter.d.ts +16 -0
  92. package/dist/telemetry/telemetryStarter.js +28 -0
  93. package/dist/telemetry/traceContext.d.ts +3 -3
  94. package/dist/telemetry/traceContext.js +3 -3
  95. package/dist/telemetry/tracers.d.ts +13 -1
  96. package/dist/telemetry/tracers.js +49 -10
  97. package/dist/telemetry/withSpan.d.ts +19 -3
  98. package/dist/telemetry/withSpan.js +149 -20
  99. package/dist/types/exporter.d.ts +98 -3
  100. package/dist/types/livekit.d.ts +16 -1
  101. package/dist/types/loopEngine.d.ts +16 -1
  102. package/dist/types/observability.d.ts +38 -5
  103. package/dist/types/proxy.d.ts +2 -0
  104. package/dist/types/span.d.ts +26 -0
  105. package/dist/utils/timeout.js +4 -2
  106. package/dist/voice/livekit/realtimeMcpTools.js +11 -2
  107. package/dist/voice/livekit/realtimeMemory.d.ts +20 -2
  108. package/dist/voice/livekit/realtimeMemory.js +139 -46
  109. package/dist/voice/livekit/realtimeVoiceAgent.js +20 -8
  110. package/dist/voice/livekit/turnRecorder.js +22 -9
  111. package/dist/workflow/core/ensembleExecutor.js +2 -0
  112. package/dist/workflow/core/judgeScorer.js +1 -0
  113. package/dist/workflow/core/responseConditioner.js +1 -0
  114. package/dist/workflow/core/workflowRunner.js +6 -1
  115. package/dist/workflow/index.d.ts +1 -0
  116. package/dist/workflow/index.js +4 -0
  117. package/docs-site/static/search-index.json +32 -27
  118. package/package.json +21 -2
@@ -11,7 +11,8 @@ import chalk from "chalk";
11
11
  import ora from "ora";
12
12
  import { logger } from "../../utils/logger.js";
13
13
  import { NeuroLink } from "../../neurolink.js";
14
- import { formatRow, formatCost } from "../utils/formatters.js";
14
+ import { ensureTelemetry } from "../../services/server/ai/observability/instrumentation.js";
15
+ import { formatRow, formatCost, formatExporterLines, formatOtelStatus, } from "../utils/formatters.js";
15
16
  import { redactUrlCredentials } from "../../utils/logSanitize.js";
16
17
  /**
17
18
  * Observability Command Factory
@@ -66,6 +67,9 @@ export class ObservabilityCommandFactory {
66
67
  ? null
67
68
  : ora("Checking observability status...").start();
68
69
  try {
70
+ // A one-shot process only starts telemetry with its first span, so
71
+ // start it here to report the exporters this process would run.
72
+ ensureTelemetry();
69
73
  const neurolink = new NeuroLink();
70
74
  const status = neurolink.getTelemetryStatus();
71
75
  if (spinner) {
@@ -105,9 +109,7 @@ export class ObservabilityCommandFactory {
105
109
  if (status.openTelemetry) {
106
110
  logger.always("");
107
111
  logger.always(chalk.bold("OpenTelemetry:"));
108
- const otelStatus = status.openTelemetry.enabled
109
- ? chalk.green("Enabled")
110
- : chalk.gray("Disabled");
112
+ const otelStatus = formatOtelStatus(status.openTelemetry, "Enabled", "Disabled");
111
113
  logger.always(formatRow(" Status:", otelStatus));
112
114
  if (status.openTelemetry.endpoint) {
113
115
  logger.always(formatRow(" Endpoint:", redactUrlCredentials(status.openTelemetry.endpoint)));
@@ -117,6 +119,11 @@ export class ObservabilityCommandFactory {
117
119
  }
118
120
  }
119
121
  logger.always("");
122
+ logger.always(chalk.bold("Exporters:"));
123
+ for (const line of formatExporterLines(status.exporters)) {
124
+ logger.always(line);
125
+ }
126
+ logger.always("");
120
127
  }
121
128
  }
122
129
  catch (error) {
@@ -11,10 +11,20 @@ import chalk from "chalk";
11
11
  import ora from "ora";
12
12
  import { logger } from "../../utils/logger.js";
13
13
  import { NeuroLink } from "../../neurolink.js";
14
- import { flushOpenTelemetry } from "../../services/server/ai/observability/instrumentation.js";
15
- import { formatRow, formatCost } from "../utils/formatters.js";
14
+ import { ensureTelemetry, flushOpenTelemetry, } from "../../services/server/ai/observability/instrumentation.js";
15
+ import { getExporterRegistry } from "../../observability/exporterRegistry.js";
16
+ import { formatRow, formatCost, formatExporterLines, formatOtelStatus, } from "../utils/formatters.js";
16
17
  import { redactUrlCredentials } from "../../utils/logSanitize.js";
17
18
  import { prepareNativeTelemetry, validateNativeTelemetry, } from "../utils/nativeTelemetry.js";
19
+ function exporterTotals() {
20
+ return getExporterRegistry()
21
+ .getStatuses()
22
+ .reduce((sum, exporter) => ({
23
+ pending: sum.pending + exporter.pendingSpans,
24
+ delivered: sum.delivered + exporter.deliveredSpans,
25
+ failed: sum.failed + exporter.failedSpans,
26
+ }), { pending: 0, delivered: 0, failed: 0 });
27
+ }
18
28
  /**
19
29
  * Telemetry Command Factory
20
30
  */
@@ -180,6 +190,9 @@ export class TelemetryCommandFactory {
180
190
  ? null
181
191
  : ora("Checking telemetry status...").start();
182
192
  try {
193
+ // A one-shot process only starts telemetry with its first span, so
194
+ // start it here to report the exporters this process would run.
195
+ ensureTelemetry();
183
196
  const neurolink = new NeuroLink();
184
197
  const status = neurolink.getTelemetryStatus();
185
198
  if (spinner) {
@@ -204,9 +217,7 @@ export class TelemetryCommandFactory {
204
217
  if (status.openTelemetry) {
205
218
  logger.always("");
206
219
  logger.always(chalk.bold("OpenTelemetry:"));
207
- const otelStatus = status.openTelemetry.enabled
208
- ? chalk.green("Active")
209
- : chalk.gray("Inactive");
220
+ const otelStatus = formatOtelStatus(status.openTelemetry, "Active", "Inactive");
210
221
  logger.always(formatRow(" Status:", otelStatus));
211
222
  if (status.openTelemetry.endpoint) {
212
223
  logger.always(formatRow(" Endpoint:", redactUrlCredentials(status.openTelemetry.endpoint)));
@@ -231,6 +242,11 @@ export class TelemetryCommandFactory {
231
242
  }
232
243
  }
233
244
  logger.always("");
245
+ logger.always(chalk.bold("Exporters:"));
246
+ for (const line of formatExporterLines(status.exporters)) {
247
+ logger.always(line);
248
+ }
249
+ logger.always("");
234
250
  }
235
251
  }
236
252
  catch (error) {
@@ -277,6 +293,7 @@ export class TelemetryCommandFactory {
277
293
  ? null
278
294
  : ora("Flushing pending spans...").start();
279
295
  try {
296
+ const before = exporterTotals();
280
297
  // Create a timeout promise
281
298
  let flushTimer;
282
299
  const timeoutPromise = new Promise((_, reject) => {
@@ -293,13 +310,26 @@ export class TelemetryCommandFactory {
293
310
  if (spinner) {
294
311
  spinner.succeed("Flush completed");
295
312
  }
313
+ const after = exporterTotals();
314
+ const flushed = after.delivered - before.delivered;
315
+ const dropped = after.failed - before.failed;
296
316
  if (args.format === "json") {
297
- logger.always(JSON.stringify({ success: true }, null, 2));
317
+ logger.always(JSON.stringify({
318
+ success: true,
319
+ pendingBefore: before.pending,
320
+ pendingAfter: after.pending,
321
+ flushed,
322
+ dropped,
323
+ }, null, 2));
298
324
  }
299
325
  else {
300
326
  logger.always("");
301
327
  logger.always(chalk.bold.cyan("=== Flush Complete ==="));
302
328
  logger.always("");
329
+ logger.always(formatRow("Spans before:", before.pending.toString()));
330
+ logger.always(formatRow("Spans after:", after.pending.toString()));
331
+ logger.always(formatRow("Flushed:", chalk.green(flushed.toString())));
332
+ logger.always(formatRow("Dropped:", dropped > 0 ? chalk.red(dropped.toString()) : "0"));
303
333
  logger.always("");
304
334
  }
305
335
  }
@@ -3,10 +3,27 @@
3
3
  *
4
4
  * Common formatting functions used across CLI command modules.
5
5
  */
6
+ import type { ExporterStatus } from "../../types/index.js";
6
7
  /**
7
8
  * Format a table row with padding
8
9
  */
9
10
  export declare function formatRow(label: string, value: string, width?: number): string;
11
+ /**
12
+ * Format the status rows of the registered exporters
13
+ */
14
+ export declare function formatExporterLines(exporters: ExporterStatus[]): string[];
15
+ /**
16
+ * @description Labels NeuroLink's OpenTelemetry export for the status commands. Beside a host TracerProvider NeuroLink exports nothing to the endpoint, so it is shown as ignored rather than active.
17
+ * @param otel {{ enabled: boolean; endpoint?: string; usingExternalProvider?: boolean }} - `getTelemetryStatus().openTelemetry`.
18
+ * @param on {string} - Label for an export NeuroLink runs.
19
+ * @param off {string} - Label for no export.
20
+ * @returns {string} The coloured label.
21
+ */
22
+ export declare function formatOtelStatus(otel: {
23
+ enabled: boolean;
24
+ endpoint?: string;
25
+ usingExternalProvider?: boolean;
26
+ }, on: string, off: string): string;
10
27
  /**
11
28
  * Format currency value
12
29
  */
@@ -3,6 +3,7 @@
3
3
  *
4
4
  * Common formatting functions used across CLI command modules.
5
5
  */
6
+ import chalk from "chalk";
6
7
  /**
7
8
  * Format a table row with padding
8
9
  */
@@ -10,6 +11,44 @@ export function formatRow(label, value, width = 30) {
10
11
  const paddedLabel = label.padEnd(width);
11
12
  return `${paddedLabel} ${value}`;
12
13
  }
14
+ /**
15
+ * Format the status rows of the registered exporters
16
+ */
17
+ export function formatExporterLines(exporters) {
18
+ if (exporters.length === 0) {
19
+ return [" None registered"];
20
+ }
21
+ return exporters.flatMap((exporter) => {
22
+ const state = !exporter.enabled
23
+ ? "Stopped"
24
+ : exporter.healthy
25
+ ? "Active"
26
+ : "Failing";
27
+ const counts = `pending ${exporter.pendingSpans}, delivered ${exporter.deliveredSpans}, failed ${exporter.failedSpans}`;
28
+ return [
29
+ formatRow(` ${exporter.name}:`, `${state} (${counts})`),
30
+ ...(exporter.lastExportTime
31
+ ? [formatRow(" Last export:", exporter.lastExportTime)]
32
+ : []),
33
+ ...(exporter.errors ?? []).map((error) => formatRow(" Error:", error)),
34
+ ];
35
+ });
36
+ }
37
+ /**
38
+ * @description Labels NeuroLink's OpenTelemetry export for the status commands. Beside a host TracerProvider NeuroLink exports nothing to the endpoint, so it is shown as ignored rather than active.
39
+ * @param otel {{ enabled: boolean; endpoint?: string; usingExternalProvider?: boolean }} - `getTelemetryStatus().openTelemetry`.
40
+ * @param on {string} - Label for an export NeuroLink runs.
41
+ * @param off {string} - Label for no export.
42
+ * @returns {string} The coloured label.
43
+ */
44
+ export function formatOtelStatus(otel, on, off) {
45
+ if (otel.usingExternalProvider) {
46
+ return chalk.yellow(otel.endpoint
47
+ ? "Ignored (a host TracerProvider is registered)"
48
+ : "Host TracerProvider");
49
+ }
50
+ return otel.enabled ? chalk.green(on) : chalk.gray(off);
51
+ }
13
52
  /**
14
53
  * Format currency value
15
54
  */
@@ -11,15 +11,31 @@ export declare const DEFAULT_FLUSH_AT = 15;
11
11
  export declare const DEFAULT_FLUSH_INTERVAL_MS = 5000;
12
12
  /** @description Milliseconds before an export request is aborted. */
13
13
  export declare const DEFAULT_TIMEOUT_MS = 30000;
14
+ /** @description Milliseconds a flush or shutdown waits for delivery before it abandons what is still in flight. */
15
+ export declare const DEFAULT_FLUSH_BUDGET_MS = 3000;
14
16
  /** @description Maximum spans sent in one export request. */
15
17
  export declare const MAX_SPANS_PER_REQUEST = 500;
18
+ /** @description Export requests to Langfuse in flight at once. Spans that end while every one is busy wait in the buffer. */
19
+ export declare const MAX_CONCURRENT_DELIVERIES = 4;
20
+ /** @description Spans that may wait for a delivery slot. Beyond it the oldest waiting spans are dropped and counted, so a Langfuse that stops answering cannot hold an unbounded backlog in memory. */
21
+ export declare const MAX_QUEUED_SPANS = 10000;
22
+ /** @description Milliseconds between warn-level logs about waiting spans dropped for lack of a delivery slot. */
23
+ export declare const QUEUE_DROP_WARN_INTERVAL_MS = 60000;
24
+ /** @description Longest `Retry-After` a 429 or 503 from Langfuse is waited out before the retry. */
25
+ export declare const MAX_EXPORT_RETRY_AFTER_MS = 60000;
16
26
  /** @description Maximum serialized bytes sent in one export request. */
17
27
  export declare const MAX_BYTES_PER_REQUEST = 3500000;
18
28
  /** @description Placeholder that replaces a value fully removed by the user's mask. */
19
29
  export declare const FULLY_MASKED = "<fully masked>";
20
- /** @description Span attributes holding trace and observation input or output, which the mask applies to. */
30
+ /** @description Span attributes holding trace and observation input or output, which the Langfuse exporter's mask and `redactIO` apply to. */
21
31
  export declare const CONTENT_KEYS: Set<string>;
22
32
  /** @description Attribute prefixes whose values are metadata entries. */
23
33
  export declare const METADATA_PREFIXES: string[];
34
+ /** @description Observation types that hold no model, usage or cost in Langfuse. GenAI model and usage keys on them are left out of the Langfuse export, so a request's generation stays its only priced record. */
35
+ export declare const UNPRICED_OBSERVATION_TYPES: ReadonlySet<string>;
36
+ /** @description GenAI attributes Langfuse reads a model name from. */
37
+ export declare const MODEL_ATTRIBUTE_KEYS: ReadonlySet<string>;
38
+ /** @description Prefix of the GenAI attributes Langfuse reads usage and cost from. */
39
+ export declare const USAGE_ATTRIBUTE_PREFIX = "gen_ai.usage.";
24
40
  /** @description OTLP numeric status code for each span status. */
25
41
  export declare const OTLP_STATUS_CODES: Record<SpanStatus, number>;
@@ -11,13 +11,23 @@ export const DEFAULT_FLUSH_AT = 15;
11
11
  export const DEFAULT_FLUSH_INTERVAL_MS = 5_000;
12
12
  /** @description Milliseconds before an export request is aborted. */
13
13
  export const DEFAULT_TIMEOUT_MS = 30_000;
14
+ /** @description Milliseconds a flush or shutdown waits for delivery before it abandons what is still in flight. */
15
+ export const DEFAULT_FLUSH_BUDGET_MS = 3_000;
14
16
  /** @description Maximum spans sent in one export request. */
15
17
  export const MAX_SPANS_PER_REQUEST = 500;
18
+ /** @description Export requests to Langfuse in flight at once. Spans that end while every one is busy wait in the buffer. */
19
+ export const MAX_CONCURRENT_DELIVERIES = 4;
20
+ /** @description Spans that may wait for a delivery slot. Beyond it the oldest waiting spans are dropped and counted, so a Langfuse that stops answering cannot hold an unbounded backlog in memory. */
21
+ export const MAX_QUEUED_SPANS = 10_000;
22
+ /** @description Milliseconds between warn-level logs about waiting spans dropped for lack of a delivery slot. */
23
+ export const QUEUE_DROP_WARN_INTERVAL_MS = 60_000;
24
+ /** @description Longest `Retry-After` a 429 or 503 from Langfuse is waited out before the retry. */
25
+ export const MAX_EXPORT_RETRY_AFTER_MS = 60_000;
16
26
  /** @description Maximum serialized bytes sent in one export request. */
17
27
  export const MAX_BYTES_PER_REQUEST = 3_500_000;
18
28
  /** @description Placeholder that replaces a value fully removed by the user's mask. */
19
29
  export const FULLY_MASKED = "<fully masked>";
20
- /** @description Span attributes holding trace and observation input or output, which the mask applies to. */
30
+ /** @description Span attributes holding trace and observation input or output, which the Langfuse exporter's mask and `redactIO` apply to. */
21
31
  export const CONTENT_KEYS = new Set([
22
32
  "langfuse.observation.input",
23
33
  "langfuse.observation.output",
@@ -29,6 +39,18 @@ export const METADATA_PREFIXES = [
29
39
  "langfuse.observation.metadata.",
30
40
  "langfuse.trace.metadata.",
31
41
  ];
42
+ /** @description Observation types that hold no model, usage or cost in Langfuse. GenAI model and usage keys on them are left out of the Langfuse export, so a request's generation stays its only priced record. */
43
+ export const UNPRICED_OBSERVATION_TYPES = new Set([
44
+ "span",
45
+ "event",
46
+ ]);
47
+ /** @description GenAI attributes Langfuse reads a model name from. */
48
+ export const MODEL_ATTRIBUTE_KEYS = new Set([
49
+ "gen_ai.request.model",
50
+ "gen_ai.response.model",
51
+ ]);
52
+ /** @description Prefix of the GenAI attributes Langfuse reads usage and cost from. */
53
+ export const USAGE_ATTRIBUTE_PREFIX = "gen_ai.usage.";
32
54
  /** @description OTLP numeric status code for each span status. */
33
55
  export const OTLP_STATUS_CODES = {
34
56
  [SpanStatus.UNSET]: 0,
@@ -18,3 +18,9 @@ export declare const TRACE_METADATA_CONTEXT_KEY = "neurolink.trace.metadata";
18
18
  export declare const OTEL_LOG_PREFIX = "[OpenTelemetry]";
19
19
  /** @description Most open and most ended spans the exporter span processor remembers for re-parenting; the oldest are forgotten first. */
20
20
  export declare const MAX_REMEMBERED_SPANS = 5000;
21
+ /** @description Most redacted span attribute strings remembered so `observe` does not redact them a second time. */
22
+ export declare const MAX_REDACTED_ATTRIBUTES = 64;
23
+ /** @description Span attribute that keeps a wrapper span out of every exporter. */
24
+ export declare const INTERNAL_SPAN_ATTRIBUTE = "langfuse.internal";
25
+ /** @description Instrumentation scope of the proxy's tracer. Of the proxy's spans only the ones it marks as observations (the request root and its generation) reach the exporters, although its spans also carry `gen_ai.*` keys for collectors. */
26
+ export declare const PROXY_TRACER_SCOPE = "neurolink.proxy";
@@ -32,3 +32,9 @@ export const TRACE_METADATA_CONTEXT_KEY = "neurolink.trace.metadata";
32
32
  export const OTEL_LOG_PREFIX = "[OpenTelemetry]";
33
33
  /** @description Most open and most ended spans the exporter span processor remembers for re-parenting; the oldest are forgotten first. */
34
34
  export const MAX_REMEMBERED_SPANS = 5_000;
35
+ /** @description Most redacted span attribute strings remembered so `observe` does not redact them a second time. */
36
+ export const MAX_REDACTED_ATTRIBUTES = 64;
37
+ /** @description Span attribute that keeps a wrapper span out of every exporter. */
38
+ export const INTERNAL_SPAN_ATTRIBUTE = "langfuse.internal";
39
+ /** @description Instrumentation scope of the proxy's tracer. Of the proxy's spans only the ones it marks as observations (the request root and its generation) reach the exporters, although its spans also carry `gen_ai.*` keys for collectors. */
40
+ export const PROXY_TRACER_SCOPE = "neurolink.proxy";
@@ -1760,6 +1760,7 @@ export class BaseProvider {
1760
1760
  error,
1761
1761
  latency,
1762
1762
  };
1763
+ this.telemetryHandler.recordTTSFailure(ttsProvider, error, latency);
1763
1764
  logger.error(`TTS synthesis failed in Mode 1 (direct input synthesis):`, ttsError);
1764
1765
  }
1765
1766
  return this.enhanceResult(baseResult, options, startTime);
@@ -2117,6 +2118,7 @@ export class BaseProvider {
2117
2118
  catch (ttsError) {
2118
2119
  const latency = Date.now() - ttsStartTime;
2119
2120
  const error = this.getTTSErrorDetails(ttsError);
2121
+ this.telemetryHandler.recordTTSFailure(ttsProvider, error, latency);
2120
2122
  logger.error(`TTS synthesis failed in Mode 2 (AI response synthesis):`, ttsError);
2121
2123
  return {
2122
2124
  ...enhancedResult,
@@ -242,6 +242,25 @@ export function createGeminiLoopAdapter(config) {
242
242
  });
243
243
  return next;
244
244
  },
245
+ // Gemini's prompt count already contains the cached tokens, and its
246
+ // candidate count leaves out the thinking tokens. The step's usage keeps
247
+ // that raw shape because the provider clients reconcile the turn totals
248
+ // from it once, and callers get those counts as the API reported them;
249
+ // only the generation observation is rebased, so a cached token is not
250
+ // billed at the input rate as well as the cache rate, a cached count above
251
+ // the prompt is not billed as extra tokens, and a thinking token is billed
252
+ // at the output rate.
253
+ observedUsage(usage) {
254
+ const cached = usage.cacheReadTokens === undefined
255
+ ? undefined
256
+ : Math.min(usage.cacheReadTokens, usage.inputTokens);
257
+ return {
258
+ ...usage,
259
+ inputTokens: Math.max(0, usage.inputTokens - (cached ?? 0)),
260
+ outputTokens: usage.outputTokens + (usage.reasoningTokens ?? 0),
261
+ ...(cached !== undefined && { cacheReadTokens: cached }),
262
+ };
263
+ },
245
264
  mapFinishReason(rawStopReason, hadToolCallsAtCap) {
246
265
  const mapped = mapGeminiFinishReason(rawStopReason);
247
266
  // "tool-calls" from this hook means the step cap cut the turn off with
@@ -306,6 +306,7 @@ async function executeObservedStep(adapter, request, channel, signal, span) {
306
306
  catch (err) {
307
307
  throw err instanceof PostEmissionStepError ? err.cause : err;
308
308
  }
309
+ const observed = adapter.observedUsage?.(result.usage) ?? result.usage;
309
310
  observeGeneration(generationSpan, {
310
311
  provider: request.generation?.provider,
311
312
  model: request.generation?.model,
@@ -316,13 +317,15 @@ async function executeObservedStep(adapter, request, channel, signal, span) {
316
317
  ...(result.toolCalls.length > 0 ? { toolCalls: result.toolCalls } : {}),
317
318
  },
318
319
  usage: {
319
- inputTokens: result.usage.inputTokens,
320
- outputTokens: result.usage.outputTokens,
321
- cacheReadTokens: result.usage.cacheReadTokens,
322
- cacheCreationTokens: result.usage.cacheWriteTokens,
323
- reasoningTokens: result.usage.reasoningTokens,
320
+ inputTokens: observed.inputTokens,
321
+ outputTokens: observed.outputTokens,
322
+ cacheReadTokens: observed.cacheReadTokens,
323
+ cacheCreationTokens: observed.cacheWriteTokens,
324
+ reasoningTokens: observed.reasoningTokens,
324
325
  },
325
- finishReason: result.rawStopReason,
326
+ // A step cut short before the provider's terminal event has no stop
327
+ // reason; record "other", as the turn's own finishReason does.
328
+ finishReason: result.rawStopReason ?? (signal.aborted ? "other" : undefined),
326
329
  aborted: signal.aborted,
327
330
  completionStartTime: firstChunkAt,
328
331
  });
@@ -370,7 +373,7 @@ export function runAgenticLoop(adapter, initialConversation, options) {
370
373
  // ever writes to it, so the loop is the same loop it was.
371
374
  let stepCap = adapter.maxSteps;
372
375
  const turnStartedAt = Date.now();
373
- const stepSpans = new StepSpans();
376
+ const stepSpans = new StepSpans(options.operation);
374
377
  try {
375
378
  for (let step = 0; step < stepCap; step++) {
376
379
  if (internalAbort.signal.aborted) {
@@ -129,10 +129,11 @@ export class StreamHandler {
129
129
  // captured upstream error (if any) so providerError /
130
130
  // modelResponseRaw carry the real cause.
131
131
  const sentinel = await buildNoOutputSentinel(error, result, getUnderlyingError?.());
132
- // Curator P2-5 + P3-6: stamp the active OTel span so
133
- // ContextEnricher.onEnd() surfaces a WARNING-level Langfuse
134
- // observation with finishReason + token usage. Centralized in
135
- // stampNoOutputSpan so every wired site stamps consistently.
132
+ // Curator P2-5 + P3-6: stamp the active OTel span with the
133
+ // WARNING observation level and a status message carrying
134
+ // finishReason + token usage, which ExporterSpanProcessor exports
135
+ // with the span. Centralized in stampNoOutputSpan so every wired
136
+ // site stamps consistently.
136
137
  stampNoOutputSpan(sentinel);
137
138
  // S4 fix: yield a sentinel chunk so Pipeline B can detect the empty stream
138
139
  // and set the span to WARNING status instead of OK
@@ -13,7 +13,7 @@
13
13
  * @module core/modules/TelemetryHandler
14
14
  */
15
15
  import type { NeuroLink } from "../../neurolink.js";
16
- import type { StreamOptions, AIProviderName, AnalyticsData, EnhancedGenerateResult, EvaluationData, MemoryToolCallRecord, MemoryToolResultRecord, RawUsageObject, TextGenerationOptions, TokenUsage } from "../../types/index.js";
16
+ import type { StreamOptions, AIProviderName, AnalyticsData, EnhancedGenerateResult, EvaluationData, MemoryToolCallRecord, MemoryToolResultRecord, RawUsageObject, TextGenerationOptions, TokenUsage, TTSMetadata } from "../../types/index.js";
17
17
  /**
18
18
  * TelemetryHandler class - Handles analytics and telemetry for AI providers
19
19
  */
@@ -34,6 +34,10 @@ export declare class TelemetryHandler {
34
34
  * Record performance metrics for a generation
35
35
  */
36
36
  recordPerformanceMetrics(usage: RawUsageObject | undefined, responseTime: number): Promise<void>;
37
+ /**
38
+ * Record a TTS synthesis failure without affecting the generation result.
39
+ */
40
+ recordTTSFailure(ttsProvider: string, error: NonNullable<TTSMetadata["error"]>, latency: number): void;
37
41
  /**
38
42
  * Estimate cost only from known per-model rates. Unknown pricing remains
39
43
  * absent from cost metrics instead of using another model's default rate.
@@ -16,6 +16,7 @@ import { nanoid } from "nanoid";
16
16
  import { extractTokenUsage } from "../../utils/tokenUtils.js";
17
17
  import { logger } from "../../utils/logger.js";
18
18
  import { recordProviderPerformanceFromMetrics } from "../evaluationProviders.js";
19
+ import { recordAIRequest, recordCustomHistogram, recordCustomMetric, } from "../../telemetry/aiMetrics.js";
19
20
  import { calculateCost, hasPricing } from "../../utils/pricing.js";
20
21
  /**
21
22
  * TelemetryHandler class - Handles analytics and telemetry for AI providers
@@ -84,6 +85,7 @@ export class TelemetryHandler {
84
85
  cost: actualCost,
85
86
  success: true,
86
87
  });
88
+ recordAIRequest(this.providerName, this.modelName, totalTokens, responseTime, actualCost !== undefined && actualCost > 0 ? actualCost : undefined);
87
89
  logger.debug(`Performance recorded for ${this.providerName}`, {
88
90
  responseTime: `${responseTime}ms`,
89
91
  tokens: totalTokens,
@@ -94,6 +96,20 @@ export class TelemetryHandler {
94
96
  logger.warn("⚠️ Performance recording failed:", perfError);
95
97
  }
96
98
  }
99
+ /**
100
+ * Record a TTS synthesis failure without affecting the generation result.
101
+ */
102
+ recordTTSFailure(ttsProvider, error, latency) {
103
+ const labels = {
104
+ provider: ttsProvider,
105
+ error_code: error.code,
106
+ ...(error.retriable !== undefined
107
+ ? { retriable: error.retriable.toString() }
108
+ : {}),
109
+ };
110
+ recordCustomMetric("tts_failures", 1, labels);
111
+ recordCustomHistogram("tts_failure_latency_ms", latency, labels);
112
+ }
97
113
  /**
98
114
  * Estimate cost only from known per-model rates. Unknown pricing remains
99
115
  * absent from cost metrics instead of using another model's default rate.
@@ -860,6 +860,9 @@ export class ToolsManager {
860
860
  attributes: {
861
861
  "tool.name": toolName,
862
862
  "tool.type": "custom",
863
+ // Pure wrapper around the `tool` observation of the same
864
+ // call: it stays an OpenTelemetry span but is not exported.
865
+ [ATTR.NL_INTERNAL]: true,
863
866
  },
864
867
  });
865
868
  const startTime = Date.now();
@@ -447,7 +447,7 @@ function generateObservedStep(args, request) {
447
447
  });
448
448
  }
449
449
  export async function runNativeGenerateLoop(args, toolExecutionSummaries) {
450
- const stepSpans = new StepSpans();
450
+ const stepSpans = new StepSpans("generate");
451
451
  try {
452
452
  return await runGenerateSteps(args, toolExecutionSummaries, stepSpans);
453
453
  }
@@ -1,3 +1,4 @@
1
+ import "../services/server/ai/observability/instrumentation.js";
1
2
  import type { EvaluationData, GenerateResult, AutoEvaluationConfig, EvaluationConfig } from "../types/index.js";
2
3
  import type { LanguageModelV3CallOptions } from "../types/index.js";
3
4
  export * from "./errors/index.js";
@@ -1,3 +1,7 @@
1
+ // Imported on its own, this entry starts tracing at its first span, as the
2
+ // main entry does. The import sits here rather than in the shared tracers so
3
+ // that the client entry stays free of the OpenTelemetry Node SDK.
4
+ import "../services/server/ai/observability/instrumentation.js";
1
5
  import { ContextBuilder } from "./contextBuilder.js";
2
6
  import { RAGASEvaluator } from "./ragasEvaluator.js";
3
7
  import { mapToEvaluationData } from "./scoring.js";
@@ -38,6 +38,7 @@ export class RAGASEvaluator {
38
38
  return withSpan({
39
39
  name: "neurolink.evaluation.ragas",
40
40
  tracer: tracers.sdk,
41
+ observationType: "span",
41
42
  attributes: {
42
43
  "evaluation.provider": this.providerName,
43
44
  "evaluation.model": this.evaluationModel,
@@ -14,6 +14,7 @@ import { toolRegistry } from "./toolRegistry.js";
14
14
  import { HITLUserRejectedError, HITLTimeoutError } from "../hitl/hitlErrors.js";
15
15
  import { detectCategory } from "../utils/mcpDefaults.js";
16
16
  import { isObject, isNonNullObject } from "../utils/typeUtils.js";
17
+ import { recordMCPToolCall } from "../telemetry/aiMetrics.js";
17
18
  import { tracers } from "../telemetry/tracers.js";
18
19
  import { SpanStatusCode } from "@opentelemetry/api";
19
20
  // `@modelcontextprotocol/sdk`'s stdio transport (imported above) does
@@ -2045,8 +2046,9 @@ export class ExternalServerManager extends EventEmitter {
2045
2046
  if (result.success) {
2046
2047
  // A resolved `{ isError: true }` still comes back with success:true so
2047
2048
  // the caller receives the MCP error payload as-is, but it is a failed
2048
- // call for telemetry: label mcp_tool_calls_total accordingly instead
2049
- // of counting every resolved error as a success.
2049
+ // call for telemetry: count it in the server's totalErrors and as
2050
+ // success=false in mcp_tool_calls_total instead of counting every
2051
+ // resolved error as a success.
2050
2052
  const resolvedError = result.isErrorResult === true;
2051
2053
  if (resolvedError) {
2052
2054
  instance.metrics.totalErrors++;
@@ -2056,6 +2058,7 @@ export class ExternalServerManager extends EventEmitter {
2056
2058
  : `[ExternalServerManager] Tool executed successfully: ${toolName} on ${serverId}`, {
2057
2059
  duration,
2058
2060
  });
2061
+ recordMCPToolCall(toolName, duration, !resolvedError);
2059
2062
  return result.data;
2060
2063
  }
2061
2064
  else {
@@ -2064,6 +2067,7 @@ export class ExternalServerManager extends EventEmitter {
2064
2067
  }
2065
2068
  catch (error) {
2066
2069
  instance.metrics.totalErrors++;
2070
+ recordMCPToolCall(toolName, Date.now() - startTime, false);
2067
2071
  mcpLogger.error(`[ExternalServerManager] Tool execution failed: ${toolName} on ${serverId}`, error);
2068
2072
  // The SDK rejects with its connection-closed error when the server is
2069
2073
  // gone. Without this transition the instance stayed "connected" and
@@ -293,6 +293,9 @@ export class MCPToolRegistry extends MCPRegistry {
293
293
  attributes: {
294
294
  "tool.name": toolName,
295
295
  [ATTR.MCP_SERVER_ID]: preResolvedServerId || "builtin",
296
+ // Registry-level wrapper around the `tool` observation of the same
297
+ // call: it stays an OpenTelemetry span but is not exported.
298
+ [ATTR.NL_INTERNAL]: true,
296
299
  },
297
300
  }, async (span) => {
298
301
  try {
@@ -1,5 +1,5 @@
1
1
  import type { DecisionCallerOptions, DecisionLimitsQuery, DecisionLimitsReading, DecisionOptions, DecisionResult, AgentDefinition, AgentNetworkConfig, AgentRunOptions, AgentRunOutcome, AgentToolRegistrationOptions, IsolatedAgentDefinition, NetworkExecutionInput, NetworkExecutionOptions, NetworkExecutionResult, NetworkStreamChunk, WorkerInstanceOptions } from "./types/index.js";
2
- import type { CompactionConfig, CompactionResult, ArtifactStore, SpanData, ObservabilityConfig, MetricsSummary, MCPToolAnnotations, TraceView, AuthenticatedContext, AuthProvider, JsonObject, NeuroLinkEvents, TypedEventEmitter, MCPEnhancementsConfig, NeuroLinkAuthConfig, NeurolinkConstructorConfig, ChatMessage, ConversationSeedMessage, ConversationSeedOptions, RecordConversationTurnOptions, RecordConversationTurnResult, ExternalMCPOperationResult, ExternalMCPServerInstance, ExternalMCPServerHealth, ExternalMCPToolInfo, GenerateOptions, GenerateResult, ProviderStatus, TextGenerationOptions, TextGenerationResult, MCPExecutableTool, MCPServerInfo, MCPStatus, StreamOptions, StreamResult, ToolExecutionContext, ToolExecutionSummary, ToolInfo, ToolRegistrationOptions, BatchOperationResult, StreamGenerationEndContext, ToolRoutingServerDescriptor, ToolDedupConfig, ToolConfig, KnowledgeEngineStatus, ChecklistState, ArtifactPageRequest, BankArtifactOptions, BankedArtifactRef, DelegateCollectRequest, DelegateCollectResult, DelegateHandle, DelegateRegistrationOptions, DelegateSpawnOptions, BackgroundCommandHandle, BackgroundCommandOptions, BackgroundCommandOutputPage, BackgroundCommandPageRequest, BackgroundCommandPolicy, BackgroundCommandStatus, GitToolResult, GitToolsetOptions } from "./types/index.js";
2
+ import type { CompactionConfig, CompactionResult, ArtifactStore, SpanData, ExporterStatus, ObservabilityConfig, MetricsSummary, MCPToolAnnotations, TraceView, AuthenticatedContext, AuthProvider, JsonObject, NeuroLinkEvents, TypedEventEmitter, MCPEnhancementsConfig, NeuroLinkAuthConfig, NeurolinkConstructorConfig, ChatMessage, ConversationSeedMessage, ConversationSeedOptions, RecordConversationTurnOptions, RecordConversationTurnResult, ExternalMCPOperationResult, ExternalMCPServerInstance, ExternalMCPServerHealth, ExternalMCPToolInfo, GenerateOptions, GenerateResult, ProviderStatus, TextGenerationOptions, TextGenerationResult, MCPExecutableTool, MCPServerInfo, MCPStatus, StreamOptions, StreamResult, ToolExecutionContext, ToolExecutionSummary, ToolInfo, ToolRegistrationOptions, BatchOperationResult, StreamGenerationEndContext, ToolRoutingServerDescriptor, ToolDedupConfig, ToolConfig, KnowledgeEngineStatus, ChecklistState, ArtifactPageRequest, BankArtifactOptions, BankedArtifactRef, DelegateCollectRequest, DelegateCollectResult, DelegateHandle, DelegateRegistrationOptions, DelegateSpawnOptions, BackgroundCommandHandle, BackgroundCommandOptions, BackgroundCommandOutputPage, BackgroundCommandPageRequest, BackgroundCommandPolicy, BackgroundCommandStatus, GitToolResult, GitToolsetOptions } from "./types/index.js";
3
3
  import { ConversationMemoryManager } from "./core/conversationMemoryManager.js";
4
4
  import type { RedisConversationMemoryManager } from "./core/redisConversationMemoryManager.js";
5
5
  import { ExternalServerManager } from "./mcp/externalServerManager.js";
@@ -687,6 +687,10 @@ export declare class NeuroLink {
687
687
  isTelemetryEnabled(): boolean;
688
688
  /**
689
689
  * Get comprehensive telemetry status including Langfuse and OpenTelemetry state
690
+ * and every registered exporter with its queued, delivered and dropped spans.
691
+ * `openTelemetry.enabled` is whether tracing is initialized, as it always
692
+ * was. Beside a host TracerProvider, `openTelemetry.usingExternalProvider`
693
+ * is `true`: NeuroLink exports nothing to the endpoint.
690
694
  */
691
695
  getTelemetryStatus(): {
692
696
  enabled: boolean;
@@ -699,7 +703,9 @@ export declare class NeuroLink {
699
703
  enabled: boolean;
700
704
  endpoint?: string;
701
705
  serviceName?: string;
706
+ usingExternalProvider?: boolean;
702
707
  };
708
+ exporters: ExporterStatus[];
703
709
  };
704
710
  /**
705
711
  * Get aggregated observability metrics (latency, tokens, cost, success rate)
@@ -2714,13 +2720,15 @@ export declare class NeuroLink {
2714
2720
  * on the wire can differ from the instance's.
2715
2721
  *
2716
2722
  * Fail-open by construction, in three layers. Without a decision provider
2717
- * nothing runs at all — no hook, no event — and the consumer gets the null
2718
- * it always got. Everything a host can reach (its hook's return value, the
2719
- * event payloads, the answer split, `onAnswers`) runs inside one `try`;
2720
- * whatever a host throws, the consumer gets what `tryRunDecide` alone
2721
- * would have given it. And every host-facing value is a `structuredClone`,
2722
- * so a hook or listener that edits what it is handed edits neither the
2723
- * wire request nor the consumer's own question objects.
2723
+ * no hook runs and no event is emitted: the call goes to `tryRunDecide`
2724
+ * alone, which records the WARNING `decide` observation it always did, and
2725
+ * the consumer gets the null it always got. Everything a host can reach
2726
+ * (its hook's return value, the event payloads, the answer split,
2727
+ * `onAnswers`) runs inside one `try`; whatever a host throws, the consumer
2728
+ * gets what `tryRunDecide` alone would have given it. And every
2729
+ * host-facing value is a `structuredClone`, so a hook or listener that
2730
+ * edits what it is handed edits neither the wire request nor the
2731
+ * consumer's own question objects.
2724
2732
  *
2725
2733
  * `outer` is what the surrounding `generate()` / `stream()` supplies —
2726
2734
  * per-call credentials, abort signal, ids — and wins over the same fields