@juspay/neurolink 13.4.10 → 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 (145) hide show
  1. package/CHANGELOG.md +2 -3
  2. package/README.md +3 -2
  3. package/dist/adapters/video/videoAnalyzer.d.ts +1 -0
  4. package/dist/adapters/video/videoAnalyzer.js +4 -0
  5. package/dist/autoresearch/index.d.ts +1 -0
  6. package/dist/autoresearch/index.js +4 -0
  7. package/dist/autoresearch/worker.js +4 -0
  8. package/dist/browser/neurolink.min.js +466 -466
  9. package/dist/cli/commands/decide.js +25 -5
  10. package/dist/cli/commands/observability.js +11 -4
  11. package/dist/cli/commands/telemetry.js +36 -6
  12. package/dist/cli/utils/formatters.d.ts +17 -0
  13. package/dist/cli/utils/formatters.js +39 -0
  14. package/dist/constants/langfuse.d.ts +17 -1
  15. package/dist/constants/langfuse.js +23 -1
  16. package/dist/constants/telemetry.d.ts +6 -0
  17. package/dist/constants/telemetry.js +6 -0
  18. package/dist/core/baseProvider.js +2 -0
  19. package/dist/core/geminiLoopAdapter.js +19 -0
  20. package/dist/core/loopEngine.js +10 -7
  21. package/dist/core/modules/StreamHandler.js +5 -4
  22. package/dist/core/modules/TelemetryHandler.d.ts +5 -1
  23. package/dist/core/modules/TelemetryHandler.js +16 -0
  24. package/dist/core/modules/ToolsManager.js +3 -0
  25. package/dist/core/nativeGenerateLoop.js +1 -1
  26. package/dist/evaluation/index.d.ts +1 -0
  27. package/dist/evaluation/index.js +4 -0
  28. package/dist/evaluation/ragasEvaluator.js +1 -0
  29. package/dist/factories/providerDescriptors.d.ts +4 -3
  30. package/dist/factories/providerDescriptors.js +6 -4
  31. package/dist/index.d.ts +1 -1
  32. package/dist/index.js +1 -1
  33. package/dist/mcp/externalServerManager.js +6 -2
  34. package/dist/mcp/toolRegistry.js +3 -0
  35. package/dist/neurolink.d.ts +20 -10
  36. package/dist/neurolink.js +134 -65
  37. package/dist/observability/FEATURE-STATUS.md +0 -4
  38. package/dist/observability/exporterRegistry.d.ts +6 -1
  39. package/dist/observability/exporterRegistry.js +18 -0
  40. package/dist/observability/exporterSpanProcessor.d.ts +20 -6
  41. package/dist/observability/exporterSpanProcessor.js +166 -34
  42. package/dist/observability/exporters/baseExporter.d.ts +22 -3
  43. package/dist/observability/exporters/baseExporter.js +87 -5
  44. package/dist/observability/exporters/langfuseExporter.d.ts +29 -3
  45. package/dist/observability/exporters/langfuseExporter.js +251 -41
  46. package/dist/observability/langfuseMasking.d.ts +1 -1
  47. package/dist/observability/langfuseMasking.js +1 -1
  48. package/dist/observability/otlpEncoding.d.ts +18 -5
  49. package/dist/observability/otlpEncoding.js +69 -12
  50. package/dist/observability/utils/spanSerializer.d.ts +3 -2
  51. package/dist/observability/utils/spanSerializer.js +5 -3
  52. package/dist/processors/base/BaseFileProcessor.d.ts +1 -0
  53. package/dist/processors/base/BaseFileProcessor.js +4 -0
  54. package/dist/processors/index.d.ts +1 -0
  55. package/dist/processors/index.js +4 -0
  56. package/dist/processors/registry/ProcessorRegistry.d.ts +1 -0
  57. package/dist/processors/registry/ProcessorRegistry.js +5 -0
  58. package/dist/providers/amazonBedrock/client.js +7 -5
  59. package/dist/providers/amazonSagemaker.js +32 -12
  60. package/dist/providers/anthropic/client.js +1 -0
  61. package/dist/providers/anthropic/loopAdapter.js +7 -3
  62. package/dist/providers/catalog/provider-catalog.schema.json +64 -3
  63. package/dist/providers/catalog/schema.js +81 -1
  64. package/dist/providers/catalog/xor.json +30 -10
  65. package/dist/providers/configuredDecision.d.ts +8 -1
  66. package/dist/providers/configuredDecision.js +22 -0
  67. package/dist/providers/decide/dialects/openaiDecisions.js +8 -0
  68. package/dist/providers/decide/dialects/systemOne.js +28 -2
  69. package/dist/providers/decide/guards.d.ts +6 -0
  70. package/dist/providers/decide/guards.js +22 -0
  71. package/dist/providers/googleAiStudio/client.js +2 -0
  72. package/dist/providers/googleVertex/client.d.ts +6 -5
  73. package/dist/providers/googleVertex/client.js +46 -19
  74. package/dist/providers/openaiChatCompletionsBase.js +4 -4
  75. package/dist/providers/systemOneDecision.d.ts +30 -7
  76. package/dist/providers/systemOneDecision.js +71 -14
  77. package/dist/proxy/codexUsage.js +59 -2
  78. package/dist/proxy/proxyTracer.d.ts +25 -1
  79. package/dist/proxy/proxyTracer.js +146 -22
  80. package/dist/proxy/proxyTranslationEngine.js +4 -2
  81. package/dist/proxy/sseInterceptor.js +6 -4
  82. package/dist/rag/chunkers/BaseChunker.js +1 -0
  83. package/dist/rag/index.d.ts +1 -0
  84. package/dist/rag/index.js +4 -0
  85. package/dist/rag/ragIntegration.js +1 -0
  86. package/dist/rag/reranker/reranker.js +2 -0
  87. package/dist/rag/retrieval/vectorQueryTool.js +1 -0
  88. package/dist/server/routes/codexProxyRoutes.js +3 -1
  89. package/dist/server/utils/validation.d.ts +3 -3
  90. package/dist/services/server/ai/observability/instrumentation.d.ts +8 -7
  91. package/dist/services/server/ai/observability/instrumentation.js +217 -47
  92. package/dist/services/server/ai/observability/langfuseSetup.d.ts +19 -1
  93. package/dist/services/server/ai/observability/langfuseSetup.js +62 -4
  94. package/dist/services/server/ai/observability/otlpSetup.d.ts +12 -2
  95. package/dist/services/server/ai/observability/otlpSetup.js +55 -4
  96. package/dist/telemetry/aiMetrics.d.ts +56 -0
  97. package/dist/telemetry/aiMetrics.js +229 -0
  98. package/dist/telemetry/attributes.d.ts +24 -1
  99. package/dist/telemetry/attributes.js +94 -9
  100. package/dist/telemetry/chatMessages.d.ts +6 -0
  101. package/dist/telemetry/chatMessages.js +37 -0
  102. package/dist/telemetry/index.d.ts +1 -1
  103. package/dist/telemetry/index.js +1 -1
  104. package/dist/telemetry/observation.d.ts +5 -5
  105. package/dist/telemetry/observation.js +128 -39
  106. package/dist/telemetry/spanSanitizer.d.ts +2 -2
  107. package/dist/telemetry/spanSanitizer.js +76 -8
  108. package/dist/telemetry/telemetryStarter.d.ts +16 -0
  109. package/dist/telemetry/telemetryStarter.js +28 -0
  110. package/dist/telemetry/traceContext.d.ts +3 -3
  111. package/dist/telemetry/traceContext.js +3 -3
  112. package/dist/telemetry/tracers.d.ts +13 -1
  113. package/dist/telemetry/tracers.js +49 -10
  114. package/dist/telemetry/withSpan.d.ts +19 -3
  115. package/dist/telemetry/withSpan.js +149 -20
  116. package/dist/types/cli.d.ts +2 -0
  117. package/dist/types/decision.d.ts +117 -10
  118. package/dist/types/exporter.d.ts +98 -3
  119. package/dist/types/livekit.d.ts +16 -1
  120. package/dist/types/loopEngine.d.ts +16 -1
  121. package/dist/types/observability.d.ts +38 -5
  122. package/dist/types/providerCatalog.d.ts +12 -1
  123. package/dist/types/providers.d.ts +2 -0
  124. package/dist/types/proxy.d.ts +2 -0
  125. package/dist/types/span.d.ts +26 -0
  126. package/dist/utils/decisionAnswers.d.ts +7 -1
  127. package/dist/utils/decisionAnswers.js +43 -2
  128. package/dist/utils/decisionLimits.d.ts +13 -1
  129. package/dist/utils/decisionLimits.js +52 -1
  130. package/dist/utils/decisionMedia.d.ts +4 -2
  131. package/dist/utils/decisionMedia.js +216 -14
  132. package/dist/utils/timeout.js +4 -2
  133. package/dist/voice/livekit/realtimeMcpTools.js +11 -2
  134. package/dist/voice/livekit/realtimeMemory.d.ts +20 -2
  135. package/dist/voice/livekit/realtimeMemory.js +139 -46
  136. package/dist/voice/livekit/realtimeVoiceAgent.js +20 -8
  137. package/dist/voice/livekit/turnRecorder.js +22 -9
  138. package/dist/workflow/core/ensembleExecutor.js +2 -0
  139. package/dist/workflow/core/judgeScorer.js +1 -0
  140. package/dist/workflow/core/responseConditioner.js +1 -0
  141. package/dist/workflow/core/workflowRunner.js +6 -1
  142. package/dist/workflow/index.d.ts +1 -0
  143. package/dist/workflow/index.js +4 -0
  144. package/docs-site/static/search-index.json +56 -48
  145. package/package.json +21 -2
@@ -23,7 +23,7 @@ import { NeuroLink } from "../../neurolink.js";
23
23
  import { readDecisionChoice } from "../../utils/decisionAnswers.js";
24
24
  import { calculateCost, hasPricing } from "../../utils/pricing.js";
25
25
  import { logger } from "../../utils/logger.js";
26
- const VALID_QUESTION_TYPES = new Set(["boolean", "choice", "score"]);
26
+ const VALID_QUESTION_TYPES = new Set(["boolean", "choice", "score", "value"]);
27
27
  function isRecord(value) {
28
28
  return typeof value === "object" && value !== null && !Array.isArray(value);
29
29
  }
@@ -123,6 +123,10 @@ function formatAnswerLine(id, answers) {
123
123
  }
124
124
  case "score":
125
125
  return `${id}: score ${formatNumber(answer.score)}, confidence ${formatNumber(answer.confidence)}`;
126
+ case "value":
127
+ return answer.value === null
128
+ ? `${id}: no value found, confidence ${formatNumber(answer.confidence)}`
129
+ : `${id}: value ${JSON.stringify(answer.value)}, confidence ${formatNumber(answer.confidence)}`;
126
130
  default:
127
131
  return `${id}: (unrecognised answer type)`;
128
132
  }
@@ -195,12 +199,17 @@ export const decideCommand = {
195
199
  type: "string",
196
200
  array: true,
197
201
  nargs: 1,
198
- describe: "Image for the model to read: a file path or a data: URL. Repeat for several (XOR, Perplexity and OpenAI Decisions take up to 8, Cloudflare Clef up to 4)",
202
+ describe: "Image for the model to read: a file path or a data: URL. Repeat for several (Perplexity and OpenAI Decisions take up to 8, Cloudflare Clef up to 4, XOR 1)",
199
203
  })
200
204
  .option("video", {
201
205
  type: "string",
202
206
  nargs: 1,
203
- describe: "One video for the model to read: a file path or a data: URL",
207
+ describe: "One video for the model to read: a file path or a data: URL. No current decision provider reads video, so it is refused before any request",
208
+ })
209
+ .option("audio", {
210
+ type: "string",
211
+ nargs: 1,
212
+ describe: "One audio clip for the model to hear: a WAV, FLAC, Ogg or MP3 file path or a data: URL (XOR with its xor-omni model; the first 30 seconds are heard). State may then be omitted",
204
213
  })
205
214
  .option("timeout", {
206
215
  type: "number",
@@ -221,7 +230,8 @@ export const decideCommand = {
221
230
  })
222
231
  .example('$0 decide "Refund request for a damaged item" --questions \'{"urgent":{"type":"boolean","instructions":"Is this urgent?"}}\'', "Ask a single yes/no question")
223
232
  .example("$0 decide --state-file ticket.json --questions-file questions.json --format json", "Read state and questions from files, emit raw JSON")
224
- .example('$0 decide "What color is this?" --provider xor --model xor-omni --image ./photo.png --questions \'{"color":{"type":"choice","instructions":"What color is the image?","criteria":{"red":"red","blue":"blue"}}}\'', "Ask about an image (XOR with its xor-omni model, Perplexity, Cloudflare Clef and OpenAI Decisions read images)"),
233
+ .example('$0 decide "What color is this?" --provider xor --model xor-omni --image ./photo.png --questions \'{"color":{"type":"choice","instructions":"What color is the image?","criteria":{"red":"red","blue":"blue"}}}\'', "Ask about an image (XOR with its xor-omni model, Perplexity, Cloudflare Clef and OpenAI Decisions read images)")
234
+ .example('$0 decide "Invoice INV-2024-0892 for 18,275 rupees" --provider xor --model xor-omni --questions \'{"amount":{"type":"value","dtype":"int","instructions":"Invoice amount?"}}\'', "Extract a typed value (XOR)"),
225
235
  handler: async (argv) => {
226
236
  const outputFormat = argv.format ?? "text";
227
237
  // --- Validate before any provider work ---------------------------------
@@ -256,8 +266,13 @@ export const decideCommand = {
256
266
  logger.error(chalk.red("Error: pass at most one --video."));
257
267
  process.exit(1);
258
268
  }
269
+ const audios = [].concat(argv.audio ?? []);
270
+ if (audios.length > 1) {
271
+ logger.error(chalk.red("Error: pass at most one --audio."));
272
+ process.exit(1);
273
+ }
259
274
  if (!isDecisionQuestionMap(parsedQuestions)) {
260
- logger.error(chalk.red('Error: questions must be a non-empty object of { type: "boolean" | "choice" | "score", ... }.'));
275
+ logger.error(chalk.red('Error: questions must be a non-empty object of { type: "boolean" | "choice" | "score" | "value", ... }.'));
261
276
  process.exit(1);
262
277
  }
263
278
  let state;
@@ -268,6 +283,10 @@ export const decideCommand = {
268
283
  logger.error(chalk.red(`Error: could not read state file — ${error instanceof Error ? error.message : String(error)}`));
269
284
  process.exit(1);
270
285
  }
286
+ // An audio clip can be the whole input.
287
+ if (state === undefined && audios.length === 1) {
288
+ state = "";
289
+ }
271
290
  if (state === undefined) {
272
291
  logger.error(chalk.red("Error: provide state as a positional argument or via --state-file."));
273
292
  process.exit(1);
@@ -284,6 +303,7 @@ export const decideCommand = {
284
303
  timeoutMs: argv.timeout,
285
304
  ...(argv.image && argv.image.length > 0 ? { images: argv.image } : {}),
286
305
  ...(videos.length === 1 ? { video: videos[0] } : {}),
306
+ ...(audios.length === 1 ? { audio: audios[0] } : {}),
287
307
  });
288
308
  spinner?.succeed("Decision complete");
289
309
  if (outputFormat === "json") {
@@ -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,
@@ -45,12 +45,13 @@ export declare const DECISION_PROVIDERS: readonly ProviderDescriptor[];
45
45
  */
46
46
  export declare function resolveDefaultDecisionProvider(credentials?: NeurolinkCredentials): string | undefined;
47
47
  /**
48
- * Names of the decision providers that accept images, or, with `video`, the
49
- * ones that accept video too: a hint that names a provider which would refuse
50
- * the same request is worse than none.
48
+ * Names of the decision providers that accept images, or, with `video` or
49
+ * `audio`, the ones that accept that too: a hint that names a provider which
50
+ * would refuse the same request is worse than none.
51
51
  */
52
52
  export declare function listMediaDecisionProviders(needs?: {
53
53
  video?: boolean;
54
+ audio?: boolean;
54
55
  }): string[];
55
56
  /**
56
57
  * The variables that would configure each decision provider, one clause per