@juspay/neurolink 12.59.0 → 13.0.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 (146) hide show
  1. package/CHANGELOG.md +3 -3
  2. package/dist/auth/anthropicOAuth.js +3 -3
  3. package/dist/auth/middleware/AuthMiddleware.d.ts +1 -1
  4. package/dist/auth/sessionManager.js +4 -4
  5. package/dist/auth/tokenStore.js +4 -4
  6. package/dist/browser/neurolink.min.js +474 -473
  7. package/dist/cli/commands/observability.d.ts +1 -6
  8. package/dist/cli/commands/observability.js +0 -106
  9. package/dist/cli/commands/proxy.js +9 -18
  10. package/dist/cli/commands/telemetry.d.ts +3 -13
  11. package/dist/cli/commands/telemetry.js +7 -354
  12. package/dist/cli/loop/session.js +1 -1
  13. package/dist/client/httpClient.js +1 -1
  14. package/dist/client/sseClient.js +1 -1
  15. package/dist/client/streamingClient.js +2 -2
  16. package/dist/client/wsClient.js +1 -1
  17. package/dist/constants/langfuse.d.ts +25 -0
  18. package/dist/constants/langfuse.js +38 -0
  19. package/dist/constants/telemetry.d.ts +20 -0
  20. package/dist/constants/telemetry.js +34 -0
  21. package/dist/core/baseProvider.d.ts +2 -1
  22. package/dist/core/baseProvider.js +4 -11
  23. package/dist/core/conversationMemoryManager.js +5 -8
  24. package/dist/core/factory.js +1 -1
  25. package/dist/core/geminiLoopAdapter.d.ts +8 -1
  26. package/dist/core/geminiLoopAdapter.js +25 -1
  27. package/dist/core/loopEngine.js +69 -40
  28. package/dist/core/modules/MessageBuilder.js +2 -2
  29. package/dist/core/modules/StreamHandler.js +2 -2
  30. package/dist/core/modules/TelemetryHandler.d.ts +2 -18
  31. package/dist/core/modules/TelemetryHandler.js +1 -66
  32. package/dist/core/modules/ToolsManager.js +40 -31
  33. package/dist/core/nativeGenerateLoop.js +78 -8
  34. package/dist/core/redisConversationMemoryManager.js +8 -15
  35. package/dist/core/toolExecutionGuards.js +1 -13
  36. package/dist/index.d.ts +5 -12
  37. package/dist/index.js +5 -37
  38. package/dist/mcp/batching/requestBatcher.js +1 -1
  39. package/dist/mcp/externalServerManager.js +3 -21
  40. package/dist/mcp/toolDiscoveryService.js +4 -16
  41. package/dist/mcp/toolRegistry.js +2 -5
  42. package/dist/memory/memoryRetrievalTools.js +1 -1
  43. package/dist/neurolink.d.ts +16 -27
  44. package/dist/neurolink.js +788 -792
  45. package/dist/observability/exporterSpanProcessor.d.ts +50 -0
  46. package/dist/observability/exporterSpanProcessor.js +181 -0
  47. package/dist/observability/exporters/langfuseExporter.d.ts +13 -34
  48. package/dist/observability/exporters/langfuseExporter.js +101 -154
  49. package/dist/observability/index.d.ts +1 -0
  50. package/dist/observability/index.js +1 -0
  51. package/dist/observability/langfuseMasking.d.ts +8 -0
  52. package/dist/observability/langfuseMasking.js +47 -0
  53. package/dist/observability/otlpEncoding.d.ts +20 -0
  54. package/dist/observability/otlpEncoding.js +85 -0
  55. package/dist/providers/amazonBedrock/client.js +142 -176
  56. package/dist/providers/amazonBedrock/loopAdapter.js +14 -1
  57. package/dist/providers/amazonSagemaker.js +118 -80
  58. package/dist/providers/anthropic/client.js +3 -52
  59. package/dist/providers/anthropic/loopAdapter.js +24 -1
  60. package/dist/providers/anthropic/rateLimitCapture.js +9 -12
  61. package/dist/providers/googleAiStudio/client.d.ts +4 -5
  62. package/dist/providers/googleAiStudio/client.js +38 -40
  63. package/dist/providers/googleVertex/client.d.ts +2 -7
  64. package/dist/providers/googleVertex/client.js +282 -378
  65. package/dist/providers/googleVertex/constants.js +2 -2
  66. package/dist/providers/litellm/client.d.ts +2 -9
  67. package/dist/providers/litellm/client.js +1 -63
  68. package/dist/providers/openAI/client.d.ts +1 -8
  69. package/dist/providers/openAI/client.js +0 -65
  70. package/dist/providers/openaiChatCompletionsBase.d.ts +1 -0
  71. package/dist/providers/openaiChatCompletionsBase.js +61 -4
  72. package/dist/providers/replicate.js +22 -4
  73. package/dist/proxy/codexUsage.js +7 -0
  74. package/dist/proxy/proxyFetch.js +19 -25
  75. package/dist/proxy/proxyTracer.d.ts +14 -7
  76. package/dist/proxy/proxyTracer.js +129 -83
  77. package/dist/proxy/proxyTranslationEngine.js +0 -1
  78. package/dist/proxy/requestLogger.d.ts +1 -1
  79. package/dist/proxy/requestLogger.js +39 -45
  80. package/dist/server/middleware/auth.d.ts +5 -1
  81. package/dist/server/middleware/auth.js +10 -3
  82. package/dist/server/middleware/common.js +1 -1
  83. package/dist/server/routes/agentRoutes.js +4 -4
  84. package/dist/server/routes/claudeProxyRoutes.js +99 -196
  85. package/dist/server/routes/codexProxyRoutes.js +621 -591
  86. package/dist/server/routes/healthRoutes.js +6 -6
  87. package/dist/server/routes/mcpRoutes.js +7 -7
  88. package/dist/server/routes/memoryRoutes.js +7 -7
  89. package/dist/server/routes/openApiRoutes.js +3 -3
  90. package/dist/server/routes/toolRoutes.js +5 -5
  91. package/dist/server/routes/transcribeRoutes.js +1 -1
  92. package/dist/server/utils/validation.d.ts +3 -3
  93. package/dist/services/server/ai/observability/instrumentation.d.ts +34 -201
  94. package/dist/services/server/ai/observability/instrumentation.js +220 -1267
  95. package/dist/services/server/ai/observability/langfuseSetup.d.ts +14 -0
  96. package/dist/services/server/ai/observability/langfuseSetup.js +55 -0
  97. package/dist/services/server/ai/observability/otlpSetup.d.ts +16 -0
  98. package/dist/services/server/ai/observability/otlpSetup.js +58 -0
  99. package/dist/session/globalSessionState.js +0 -11
  100. package/dist/telemetry/attributes.d.ts +23 -46
  101. package/dist/telemetry/attributes.js +76 -162
  102. package/dist/telemetry/chatMessages.d.ts +13 -0
  103. package/dist/telemetry/chatMessages.js +59 -0
  104. package/dist/telemetry/index.d.ts +5 -19
  105. package/dist/telemetry/index.js +5 -26
  106. package/dist/telemetry/observation.d.ts +37 -0
  107. package/dist/telemetry/observation.js +112 -0
  108. package/dist/telemetry/observationNames.d.ts +18 -0
  109. package/dist/telemetry/observationNames.js +24 -0
  110. package/dist/telemetry/spanSanitizer.d.ts +39 -0
  111. package/dist/telemetry/spanSanitizer.js +92 -0
  112. package/dist/telemetry/spanSerialization.d.ts +13 -0
  113. package/dist/telemetry/spanSerialization.js +46 -0
  114. package/dist/telemetry/tracers.d.ts +37 -18
  115. package/dist/telemetry/tracers.js +97 -19
  116. package/dist/telemetry/withSpan.d.ts +47 -25
  117. package/dist/telemetry/withSpan.js +158 -71
  118. package/dist/types/cli.d.ts +0 -11
  119. package/dist/types/decision.d.ts +3 -0
  120. package/dist/types/exporter.d.ts +54 -0
  121. package/dist/types/exporter.js +0 -4
  122. package/dist/types/generate.d.ts +4 -1
  123. package/dist/types/isolatedAgent.d.ts +5 -4
  124. package/dist/types/loopEngine.d.ts +18 -28
  125. package/dist/types/observability.d.ts +127 -160
  126. package/dist/types/providers.d.ts +15 -0
  127. package/dist/types/proxy.d.ts +41 -2
  128. package/dist/types/server.d.ts +9 -5
  129. package/dist/utils/anthropicTraceSanitizer.d.ts +2 -2
  130. package/dist/utils/anthropicTraceSanitizer.js +3 -1
  131. package/dist/utils/conversationMemory.js +4 -4
  132. package/dist/utils/fileDetector.js +1 -1
  133. package/dist/utils/messageBuilder.js +1 -1
  134. package/dist/utils/noOutputSentinel.d.ts +4 -16
  135. package/dist/utils/noOutputSentinel.js +12 -28
  136. package/dist/utils/providerRetry.js +4 -4
  137. package/dist/voice/livekit/brain.js +1 -1
  138. package/dist/voice/livekit/realtimeMemory.d.ts +1 -1
  139. package/dist/voice/livekit/realtimeMemory.js +66 -1
  140. package/dist/voice/livekit/realtimeVoiceAgent.js +1 -1
  141. package/docs-site/static/search-index.json +42 -64
  142. package/package.json +5 -4
  143. package/dist/telemetry/telemetryService.d.ts +0 -79
  144. package/dist/telemetry/telemetryService.js +0 -572
  145. package/dist/utils/observabilityHelpers.d.ts +0 -27
  146. package/dist/utils/observabilityHelpers.js +0 -47
@@ -0,0 +1,112 @@
1
+ import { createObservationAttributes, createTraceAttributes, LangfuseOtelSpanAttributes, } from "@langfuse/tracing";
2
+ import { WARNING_FINISH_REASONS } from "../constants/telemetry.js";
3
+ import { calculateCost } from "../utils/pricing.js";
4
+ import { extractTokenUsage } from "../utils/tokenUtils.js";
5
+ import { ATTR, spanContentAttribute, spanModelRequestAttribute, } from "./attributes.js";
6
+ import { assistantMessages } from "./chatMessages.js";
7
+ function redactedIO(attrs) {
8
+ return {
9
+ ...attrs,
10
+ ...(attrs.input !== undefined && {
11
+ input: spanContentAttribute(attrs.input),
12
+ }),
13
+ ...(attrs.output !== undefined && {
14
+ output: spanContentAttribute(attrs.output),
15
+ }),
16
+ };
17
+ }
18
+ /**
19
+ * @description Stamps Langfuse observation attributes on a span, plus a `gen_ai.observation.type` marker so the exporter pipeline recognizes it whatever its tracer scope. Input and output are credential-masked and capped at `SPAN_ATTRIBUTE_MAX_CHARS` before serialization.
20
+ * @param span {Span} - Span to describe.
21
+ * @param type {ObservationType} - Observation type Langfuse renders the span as.
22
+ * @param attrs {ObservationAttributes} - Input, output, model, parameters, usage, cost, level and metadata to record.
23
+ * @returns {void} Nothing.
24
+ */
25
+ export function observe(span, type, attrs) {
26
+ span.setAttributes({
27
+ ...createObservationAttributes(type, redactedIO(attrs)),
28
+ [ATTR.GEN_AI_OBSERVATION]: type,
29
+ });
30
+ }
31
+ /**
32
+ * @description Sets the trace-level input, output and metadata on a span, credential-masked and capped like `observe`. Each metadata entry is written as its own `langfuse.trace.metadata.<key>` attribute, redacted like input and output.
33
+ * @param span {Span} - Span whose trace is described.
34
+ * @param io {TraceContent} - Trace input, output and metadata.
35
+ * @returns {void} Nothing.
36
+ */
37
+ export function observeTrace(span, io) {
38
+ const { metadata, ...content } = io;
39
+ span.setAttributes(createTraceAttributes(redactedIO(content)));
40
+ for (const [key, value] of Object.entries(metadata ?? {})) {
41
+ if (value !== undefined) {
42
+ span.setAttribute(`${LangfuseOtelSpanAttributes.TRACE_METADATA}.${key}`, spanContentAttribute(value));
43
+ }
44
+ }
45
+ }
46
+ /**
47
+ * @description Maps any provider's token usage onto Langfuse usage-detail keys.
48
+ * @param usage {RawUsageObject | undefined | null} - Token usage in any naming convention NeuroLink normalizes.
49
+ * @returns {Record<string, number>} `input`, `output` and `total`, plus `cache_read_input_tokens`, `cache_creation_input_tokens` and `reasoning` when reported.
50
+ */
51
+ export function usageDetails(usage) {
52
+ const tokens = extractTokenUsage(usage, { calculateCacheSavings: false });
53
+ const details = {
54
+ input: tokens.input,
55
+ output: tokens.output,
56
+ total: tokens.total,
57
+ };
58
+ if (tokens.cacheReadTokens !== undefined) {
59
+ details.cache_read_input_tokens = tokens.cacheReadTokens;
60
+ }
61
+ if (tokens.cacheCreationTokens !== undefined) {
62
+ details.cache_creation_input_tokens = tokens.cacheCreationTokens;
63
+ }
64
+ if (tokens.reasoning !== undefined) {
65
+ details.reasoning = tokens.reasoning;
66
+ }
67
+ return details;
68
+ }
69
+ /**
70
+ * @description Picks the Langfuse level for a model call that finished without throwing.
71
+ * @param finishReason {string | undefined} - Provider finish reason, in any casing.
72
+ * @param aborted {boolean | undefined} - Whether the caller aborted or closed the call early.
73
+ * @returns {ObservationLevel | undefined} `"WARNING"` for truncated, filtered, aborted or empty output, otherwise `undefined`.
74
+ */
75
+ export function observationLevel(finishReason, aborted) {
76
+ if (aborted ||
77
+ WARNING_FINISH_REASONS.has(finishReason?.toLowerCase() ?? "")) {
78
+ return "WARNING";
79
+ }
80
+ return undefined;
81
+ }
82
+ /**
83
+ * @description Records a model request on its span as a Langfuse generation: model, defined parameters, input, output, usage, cost and level. Safe to call once with the request and again with the response; unset fields leave earlier values in place.
84
+ * @param span {Span} - Span of one model request.
85
+ * @param call {GenerationRecord} - What was sent and what came back.
86
+ * @returns {void} Nothing.
87
+ */
88
+ export function observeGeneration(span, call) {
89
+ const parameters = {};
90
+ for (const [key, value] of Object.entries(call.modelParameters ?? {})) {
91
+ if (value !== undefined) {
92
+ parameters[key] = value;
93
+ }
94
+ }
95
+ const pricedModel = call.model ?? call.responseModel;
96
+ const cost = call.usage && call.provider && pricedModel
97
+ ? calculateCost(call.provider, pricedModel, extractTokenUsage(call.usage, { calculateCacheSavings: false }))
98
+ : 0;
99
+ observe(span, "generation", {
100
+ model: call.responseModel ?? call.model,
101
+ modelParameters: Object.keys(parameters).length > 0 ? parameters : undefined,
102
+ input: call.input === undefined
103
+ ? undefined
104
+ : spanModelRequestAttribute(call.input),
105
+ output: assistantMessages(call.output),
106
+ usageDetails: call.usage ? usageDetails(call.usage) : undefined,
107
+ costDetails: cost > 0 ? { total: cost } : undefined,
108
+ completionStartTime: call.completionStartTime,
109
+ level: observationLevel(call.finishReason, call.aborted),
110
+ metadata: call.provider ? { provider: call.provider } : undefined,
111
+ });
112
+ }
@@ -0,0 +1,18 @@
1
+ import type { GenerationOperation } from "../types/index.js";
2
+ /**
3
+ * @description Names a model-call observation after the public API the caller used.
4
+ * @param operation {GenerationOperation | undefined} - Explicit operation; inferred from the active span's ancestry when unset.
5
+ * @returns {string} `generate` or `stream`; `generate` when the operation cannot be inferred.
6
+ */
7
+ export declare function generationName(operation?: GenerationOperation): string;
8
+ /**
9
+ * @description Names a tool observation after the tool, as the model sees it.
10
+ * @param toolName {string} - Tool name.
11
+ * @returns {string} The observation name, which is the bare tool name.
12
+ */
13
+ export declare function toolSpanName(toolName: string): string;
14
+ /**
15
+ * @description Names one agentic loop step. The one-based index travels as `step_index` metadata so every step of every run aggregates under the same name.
16
+ * @returns {string} The observation name, `step`.
17
+ */
18
+ export declare function stepSpanName(): string;
@@ -0,0 +1,24 @@
1
+ import { activeOperation } from "./tracers.js";
2
+ /**
3
+ * @description Names a model-call observation after the public API the caller used.
4
+ * @param operation {GenerationOperation | undefined} - Explicit operation; inferred from the active span's ancestry when unset.
5
+ * @returns {string} `generate` or `stream`; `generate` when the operation cannot be inferred.
6
+ */
7
+ export function generationName(operation) {
8
+ return operation ?? activeOperation() ?? "generate";
9
+ }
10
+ /**
11
+ * @description Names a tool observation after the tool, as the model sees it.
12
+ * @param toolName {string} - Tool name.
13
+ * @returns {string} The observation name, which is the bare tool name.
14
+ */
15
+ export function toolSpanName(toolName) {
16
+ return toolName;
17
+ }
18
+ /**
19
+ * @description Names one agentic loop step. The one-based index travels as `step_index` metadata so every step of every run aggregates under the same name.
20
+ * @returns {string} The observation name, `step`.
21
+ */
22
+ export function stepSpanName() {
23
+ return "step";
24
+ }
@@ -0,0 +1,39 @@
1
+ import type { JsonValue } from "../types/index.js";
2
+ /**
3
+ * @description Masks the value stored under a credential-looking key. Structures under a plural token key (`tokens`) keep their shape with every string masked; anything else collapses to the placeholder.
4
+ * @param key {string} - Property name that matched the sensitive-key pattern.
5
+ * @param entry {JsonValue} - Value stored under that key.
6
+ * @returns {JsonValue} The masked value.
7
+ */
8
+ export declare function redactSensitiveEntry(key: string, entry: JsonValue): JsonValue;
9
+ /**
10
+ * @description Masks credentials written as `name=value`, `name: value` or as a recognizable bare key (OpenAI, GitHub, AWS, Slack). Purely numeric values after a credential-looking name are left alone.
11
+ * @param text {string} - Text to scan.
12
+ * @returns {string} The text with credential values replaced.
13
+ */
14
+ export declare function redactSecretAssignments(text: string): string;
15
+ /**
16
+ * @description Replaces inline base64 data URLs with a size and MIME-type placeholder.
17
+ * @param text {string} - Text to scan.
18
+ * @returns {string} The text with each data URL payload replaced by `[N bytes mime/type]`.
19
+ */
20
+ export declare function describeDataUrls(text: string): string;
21
+ /**
22
+ * @description Tells whether an object key conventionally holds a binary payload.
23
+ * @param key {string} - Property name.
24
+ * @returns {boolean} `true` for keys such as `data`, `image`, `bytes` and `base64`.
25
+ */
26
+ export declare function isBinaryKey(key: string): boolean;
27
+ /**
28
+ * @description Reads the MIME type an object declares for its payload.
29
+ * @param node {object} - Object that may carry `mimeType`, `mediaType`, `media_type` or `mime_type`.
30
+ * @returns {string | undefined} The declared MIME type, or `undefined` when none is a string.
31
+ */
32
+ export declare function declaredMimeType(node: object): string | undefined;
33
+ /**
34
+ * @description Describes a binary payload by size instead of content.
35
+ * @param value {ArrayBuffer | ArrayBufferView | string} - Raw bytes, or a base64 string long enough to be treated as bytes.
36
+ * @param mimeType {string | undefined} - MIME type to include in the placeholder.
37
+ * @returns {string | undefined} `[N bytes mime/type]`, or `undefined` when the value is not binary.
38
+ */
39
+ export declare function binaryPlaceholder(value: ArrayBuffer | ArrayBufferView | string, mimeType: string | undefined): string | undefined;
@@ -0,0 +1,92 @@
1
+ import { BINARY_KEYS, BINARY_PAYLOAD_MIN_CHARS, REDACTED_VALUE, } from "../constants/telemetry.js";
2
+ const TOKEN_KEY_PATTERN = /tokens?$/;
3
+ const BASE64_PATTERN = /^[A-Za-z0-9+/_\-=\s]+$/;
4
+ const MIME_TYPE_KEYS = ["mimeType", "mediaType", "media_type", "mime_type"];
5
+ const DATA_URL_PATTERN = new RegExp(`data:([\\w.+-]+/[\\w.+-]+)?(?:;[\\w=.+-]+)*;base64,[A-Za-z0-9+/_\\-=]{${BINARY_PAYLOAD_MIN_CHARS},}`, "g");
6
+ const ENV_SECRET_PATTERN = /\b([\w.-]*(?:api[_-]?key|secret|password|passwd|token|credential|private[_-]?key|signature)[\w.-]*)(\s*[=:]\s*)(?!\d+\b)[^\s&"',;]+/gi;
7
+ const BARE_SECRET_PATTERN = /\b(?:sk-[A-Za-z0-9_-]{16,}|gh[pousr]_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,}|AKIA[0-9A-Z]{16}|xox[baprs]-[A-Za-z0-9-]{10,})/g;
8
+ function normalizeKey(key) {
9
+ return key.replace(/[_-]/g, "").toLowerCase();
10
+ }
11
+ function maskStrings(value) {
12
+ if (typeof value === "string") {
13
+ return REDACTED_VALUE;
14
+ }
15
+ if (Array.isArray(value)) {
16
+ return value.map(maskStrings);
17
+ }
18
+ if (value === null || typeof value !== "object") {
19
+ return value;
20
+ }
21
+ return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, maskStrings(entry)]));
22
+ }
23
+ /**
24
+ * @description Masks the value stored under a credential-looking key. Structures under a plural token key (`tokens`) keep their shape with every string masked; anything else collapses to the placeholder.
25
+ * @param key {string} - Property name that matched the sensitive-key pattern.
26
+ * @param entry {JsonValue} - Value stored under that key.
27
+ * @returns {JsonValue} The masked value.
28
+ */
29
+ export function redactSensitiveEntry(key, entry) {
30
+ const isStructure = typeof entry === "object" && entry !== null;
31
+ return isStructure && TOKEN_KEY_PATTERN.test(normalizeKey(key))
32
+ ? maskStrings(entry)
33
+ : REDACTED_VALUE;
34
+ }
35
+ /**
36
+ * @description Masks credentials written as `name=value`, `name: value` or as a recognizable bare key (OpenAI, GitHub, AWS, Slack). Purely numeric values after a credential-looking name are left alone.
37
+ * @param text {string} - Text to scan.
38
+ * @returns {string} The text with credential values replaced.
39
+ */
40
+ export function redactSecretAssignments(text) {
41
+ return text
42
+ .replace(ENV_SECRET_PATTERN, (_match, key, separator) => `${key}${separator}${REDACTED_VALUE}`)
43
+ .replace(BARE_SECRET_PATTERN, REDACTED_VALUE);
44
+ }
45
+ /**
46
+ * @description Replaces inline base64 data URLs with a size and MIME-type placeholder.
47
+ * @param text {string} - Text to scan.
48
+ * @returns {string} The text with each data URL payload replaced by `[N bytes mime/type]`.
49
+ */
50
+ export function describeDataUrls(text) {
51
+ return text.replace(DATA_URL_PATTERN, (match, mimeType) => `[${Math.floor(((match.length - match.indexOf(",") - 1) * 3) / 4)} bytes${mimeType ? ` ${mimeType}` : ""}]`);
52
+ }
53
+ /**
54
+ * @description Tells whether an object key conventionally holds a binary payload.
55
+ * @param key {string} - Property name.
56
+ * @returns {boolean} `true` for keys such as `data`, `image`, `bytes` and `base64`.
57
+ */
58
+ export function isBinaryKey(key) {
59
+ return BINARY_KEYS.has(normalizeKey(key));
60
+ }
61
+ /**
62
+ * @description Reads the MIME type an object declares for its payload.
63
+ * @param node {object} - Object that may carry `mimeType`, `mediaType`, `media_type` or `mime_type`.
64
+ * @returns {string | undefined} The declared MIME type, or `undefined` when none is a string.
65
+ */
66
+ export function declaredMimeType(node) {
67
+ for (const key of MIME_TYPE_KEYS) {
68
+ const declared = Reflect.get(node, key);
69
+ if (typeof declared === "string") {
70
+ return declared;
71
+ }
72
+ }
73
+ return undefined;
74
+ }
75
+ /**
76
+ * @description Describes a binary payload by size instead of content.
77
+ * @param value {ArrayBuffer | ArrayBufferView | string} - Raw bytes, or a base64 string long enough to be treated as bytes.
78
+ * @param mimeType {string | undefined} - MIME type to include in the placeholder.
79
+ * @returns {string | undefined} `[N bytes mime/type]`, or `undefined` when the value is not binary.
80
+ */
81
+ export function binaryPlaceholder(value, mimeType) {
82
+ const suffix = mimeType ? ` ${mimeType}` : "";
83
+ if (typeof value !== "string") {
84
+ return value instanceof ArrayBuffer || ArrayBuffer.isView(value)
85
+ ? `[${value.byteLength} bytes${suffix}]`
86
+ : undefined;
87
+ }
88
+ if (value.length > BINARY_PAYLOAD_MIN_CHARS && BASE64_PATTERN.test(value)) {
89
+ return `[${Math.floor((value.length * 3) / 4)} bytes${suffix}]`;
90
+ }
91
+ return undefined;
92
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * @description Serializes a value for a span attribute: strings pass through, everything else is JSON, with a `String()` fallback for circular structures and `[unserializable]` when even that throws.
3
+ * @param value {T} - Value to serialize.
4
+ * @returns {string} The serialized value; never throws.
5
+ */
6
+ export declare function stringify<T>(value: T): string;
7
+ /**
8
+ * @description Cuts every string inside a structure down to a per-string character budget, in place, leaving a truncation note on each cut string. Only call it on a copy the caller owns.
9
+ * @param node {T} - String or structure to shorten.
10
+ * @param budget {number} - Maximum characters kept per string, note included.
11
+ * @returns {T | string} The shortened string, or the same structure after its strings were cut.
12
+ */
13
+ export declare function capStrings<T>(node: T, budget: number): T | string;
@@ -0,0 +1,46 @@
1
+ import { TRUNCATION_NOTE_ALLOWANCE_CHARS, UNSERIALIZABLE_VALUE, } from "../constants/telemetry.js";
2
+ function safeString(value) {
3
+ try {
4
+ return String(value);
5
+ }
6
+ catch {
7
+ return UNSERIALIZABLE_VALUE;
8
+ }
9
+ }
10
+ /**
11
+ * @description Serializes a value for a span attribute: strings pass through, everything else is JSON, with a `String()` fallback for circular structures and `[unserializable]` when even that throws.
12
+ * @param value {T} - Value to serialize.
13
+ * @returns {string} The serialized value; never throws.
14
+ */
15
+ export function stringify(value) {
16
+ try {
17
+ return typeof value === "string"
18
+ ? value
19
+ : (JSON.stringify(value) ?? safeString(value));
20
+ }
21
+ catch {
22
+ return safeString(value);
23
+ }
24
+ }
25
+ /**
26
+ * @description Cuts every string inside a structure down to a per-string character budget, in place, leaving a truncation note on each cut string. Only call it on a copy the caller owns.
27
+ * @param node {T} - String or structure to shorten.
28
+ * @param budget {number} - Maximum characters kept per string, note included.
29
+ * @returns {T | string} The shortened string, or the same structure after its strings were cut.
30
+ */
31
+ export function capStrings(node, budget) {
32
+ if (typeof node === "string") {
33
+ if (node.length <= budget) {
34
+ return node;
35
+ }
36
+ const keep = Math.max(0, budget - TRUNCATION_NOTE_ALLOWANCE_CHARS);
37
+ return `${node.slice(0, keep)}...[truncated ${node.length - keep} chars]`;
38
+ }
39
+ if (node === null || typeof node !== "object") {
40
+ return node;
41
+ }
42
+ for (const [key, entry] of Object.entries(node)) {
43
+ Reflect.set(node, key, capStrings(entry, budget));
44
+ }
45
+ return node;
46
+ }
@@ -1,3 +1,22 @@
1
+ import { type Span, type Tracer } from "@opentelemetry/api";
2
+ import type { GenerationOperation } from "../types/index.js";
3
+ /**
4
+ * @description Reads which public API call the active span belongs to, following the span's ancestry up to a `generate`, `stream`, `provider.generate` or `provider.stream` span.
5
+ * @returns {GenerationOperation | undefined} The enclosing operation, or `undefined` when no such span is active.
6
+ */
7
+ export declare function activeOperation(): GenerationOperation | undefined;
8
+ /**
9
+ * @description Reports whether a `generate` or `stream` span started inside another operation's span tree.
10
+ * @param span {Span} - Span returned by a scrubbed tracer.
11
+ * @returns {boolean} `true` when the span is nested in an enclosing operation, `false` for a trace root.
12
+ */
13
+ export declare function isNestedOperation(span: Span): boolean;
14
+ /**
15
+ * @description Resolves a tracer whose spans record exceptions as type and message only, without stack traces or product-name references that would reach trace backends as event metadata.
16
+ * @param name {string} - Instrumentation scope name.
17
+ * @returns {Tracer} A tracer delegating to the globally registered provider.
18
+ */
19
+ export declare function scrubbedTracer(name: string): Tracer;
1
20
  /**
2
21
  * Pre-resolved tracers cached at module load.
3
22
  *
@@ -18,22 +37,22 @@
18
37
  * this file and switch to lazy accessors.
19
38
  */
20
39
  export declare const tracers: {
21
- readonly sdk: import("@opentelemetry/api").Tracer;
22
- readonly provider: import("@opentelemetry/api").Tracer;
23
- readonly generation: import("@opentelemetry/api").Tracer;
24
- readonly decision: import("@opentelemetry/api").Tracer;
25
- readonly stream: import("@opentelemetry/api").Tracer;
26
- readonly http: import("@opentelemetry/api").Tracer;
27
- readonly mcp: import("@opentelemetry/api").Tracer;
28
- readonly memory: import("@opentelemetry/api").Tracer;
29
- readonly redis: import("@opentelemetry/api").Tracer;
30
- readonly factory: import("@opentelemetry/api").Tracer;
31
- readonly rag: import("@opentelemetry/api").Tracer;
32
- readonly context: import("@opentelemetry/api").Tracer;
33
- readonly middleware: import("@opentelemetry/api").Tracer;
34
- readonly processor: import("@opentelemetry/api").Tracer;
35
- readonly file: import("@opentelemetry/api").Tracer;
36
- readonly autoresearch: import("@opentelemetry/api").Tracer;
37
- readonly auth: import("@opentelemetry/api").Tracer;
38
- readonly workflow: import("@opentelemetry/api").Tracer;
40
+ readonly sdk: Tracer;
41
+ readonly provider: Tracer;
42
+ readonly generation: Tracer;
43
+ readonly decision: Tracer;
44
+ readonly stream: Tracer;
45
+ readonly http: Tracer;
46
+ readonly mcp: Tracer;
47
+ readonly memory: Tracer;
48
+ readonly redis: Tracer;
49
+ readonly factory: Tracer;
50
+ readonly rag: Tracer;
51
+ readonly context: Tracer;
52
+ readonly middleware: Tracer;
53
+ readonly processor: Tracer;
54
+ readonly file: Tracer;
55
+ readonly autoresearch: Tracer;
56
+ readonly auth: Tracer;
57
+ readonly workflow: Tracer;
39
58
  };
@@ -1,4 +1,82 @@
1
- import { trace } from "@opentelemetry/api";
1
+ import { context, trace, } from "@opentelemetry/api";
2
+ import { ensureTelemetry } from "../services/server/ai/observability/instrumentation.js";
3
+ const PRODUCT_NAME = /neurolink/gi;
4
+ const OPERATION_ROOT = /^(?:provider\.)?(generate|stream)(?:\.|$)/;
5
+ const operations = new WeakMap();
6
+ const nestedOperations = new WeakSet();
7
+ function trackOperation(span, name, parent) {
8
+ const root = OPERATION_ROOT.exec(name)?.[1];
9
+ const parentOperation = parent && operations.get(parent);
10
+ if (root && parentOperation) {
11
+ nestedOperations.add(span);
12
+ }
13
+ const operation = root ?? parentOperation;
14
+ if (operation) {
15
+ operations.set(span, operation);
16
+ }
17
+ return span;
18
+ }
19
+ /**
20
+ * @description Reads which public API call the active span belongs to, following the span's ancestry up to a `generate`, `stream`, `provider.generate` or `provider.stream` span.
21
+ * @returns {GenerationOperation | undefined} The enclosing operation, or `undefined` when no such span is active.
22
+ */
23
+ export function activeOperation() {
24
+ const span = trace.getSpan(context.active());
25
+ return span && operations.get(span);
26
+ }
27
+ /**
28
+ * @description Reports whether a `generate` or `stream` span started inside another operation's span tree.
29
+ * @param span {Span} - Span returned by a scrubbed tracer.
30
+ * @returns {boolean} `true` when the span is nested in an enclosing operation, `false` for a trace root.
31
+ */
32
+ export function isNestedOperation(span) {
33
+ return nestedOperations.has(span);
34
+ }
35
+ function scrubException(exception) {
36
+ if (!(exception instanceof Error)) {
37
+ return typeof exception === "string"
38
+ ? exception.replace(PRODUCT_NAME, "ai")
39
+ : exception;
40
+ }
41
+ return {
42
+ name: exception.name.replace(PRODUCT_NAME, "Ai"),
43
+ message: exception.message.replace(PRODUCT_NAME, "ai"),
44
+ };
45
+ }
46
+ function withScrubbedExceptions(span) {
47
+ const record = span.recordException.bind(span);
48
+ span.recordException = (exception, time) => record(scrubException(exception), time);
49
+ return span;
50
+ }
51
+ /**
52
+ * @description Resolves a tracer whose spans record exceptions as type and message only, without stack traces or product-name references that would reach trace backends as event metadata.
53
+ * @param name {string} - Instrumentation scope name.
54
+ * @returns {Tracer} A tracer delegating to the globally registered provider.
55
+ */
56
+ export function scrubbedTracer(name) {
57
+ const inner = trace.getTracer(name);
58
+ return {
59
+ startSpan: (spanName, options, ctx) => {
60
+ ensureTelemetry();
61
+ const span = withScrubbedExceptions(inner.startSpan(spanName, options, ctx));
62
+ return trackOperation(span, spanName, trace.getSpan(ctx ?? context.active()));
63
+ },
64
+ startActiveSpan(spanName, arg2, arg3, arg4) {
65
+ ensureTelemetry();
66
+ const fn = [arg4, arg3, arg2].find((arg) => typeof arg === "function");
67
+ const options = typeof arg2 === "function" ? undefined : arg2;
68
+ const ctx = typeof arg3 === "function" ? undefined : arg3;
69
+ const parent = trace.getSpan(ctx ?? context.active());
70
+ const wrapped = ((span) => fn(trackOperation(withScrubbedExceptions(span), spanName, parent)));
71
+ if (ctx) {
72
+ return inner.startActiveSpan(spanName, options ?? {}, ctx, wrapped);
73
+ }
74
+ return options
75
+ ? inner.startActiveSpan(spanName, options, wrapped)
76
+ : inner.startActiveSpan(spanName, wrapped);
77
+ },
78
+ };
79
+ }
2
80
  /**
3
81
  * Pre-resolved tracers cached at module load.
4
82
  *
@@ -19,22 +97,22 @@ import { trace } from "@opentelemetry/api";
19
97
  * this file and switch to lazy accessors.
20
98
  */
21
99
  export const tracers = {
22
- sdk: trace.getTracer("neurolink"),
23
- provider: trace.getTracer("neurolink.provider"),
24
- generation: trace.getTracer("neurolink.generation"),
25
- decision: trace.getTracer("neurolink.decision"),
26
- stream: trace.getTracer("neurolink.stream"),
27
- http: trace.getTracer("neurolink.http"),
28
- mcp: trace.getTracer("neurolink.mcp"),
29
- memory: trace.getTracer("neurolink.memory"),
30
- redis: trace.getTracer("neurolink.redis"),
31
- factory: trace.getTracer("neurolink.factory"),
32
- rag: trace.getTracer("neurolink.rag"),
33
- context: trace.getTracer("neurolink.context"),
34
- middleware: trace.getTracer("neurolink.middleware"),
35
- processor: trace.getTracer("neurolink.processor"),
36
- file: trace.getTracer("neurolink.file"),
37
- autoresearch: trace.getTracer("neurolink.autoresearch"),
38
- auth: trace.getTracer("neurolink.auth"),
39
- workflow: trace.getTracer("neurolink.workflow"),
100
+ sdk: scrubbedTracer("sdk"),
101
+ provider: scrubbedTracer("provider"),
102
+ generation: scrubbedTracer("generation"),
103
+ decision: scrubbedTracer("decision"),
104
+ stream: scrubbedTracer("stream"),
105
+ http: scrubbedTracer("http"),
106
+ mcp: scrubbedTracer("mcp"),
107
+ memory: scrubbedTracer("memory"),
108
+ redis: scrubbedTracer("redis"),
109
+ factory: scrubbedTracer("factory"),
110
+ rag: scrubbedTracer("rag"),
111
+ context: scrubbedTracer("context"),
112
+ middleware: scrubbedTracer("middleware"),
113
+ processor: scrubbedTracer("processor"),
114
+ file: scrubbedTracer("file"),
115
+ autoresearch: scrubbedTracer("autoresearch"),
116
+ auth: scrubbedTracer("auth"),
117
+ workflow: scrubbedTracer("workflow"),
40
118
  };
@@ -1,39 +1,61 @@
1
1
  import { type Span } from "@opentelemetry/api";
2
- import type { SpanOptions } from "../types/index.js";
2
+ import type { GenerationRecord, SpanOptions } from "../types/index.js";
3
3
  export declare function withSpan<T>(options: SpanOptions, fn: (span: Span) => Promise<T>): Promise<T>;
4
4
  export declare function withClientSpan<T>(options: Omit<SpanOptions, "kind">, fn: (span: Span) => Promise<T>): Promise<T>;
5
5
  /**
6
- * Span wrapper for streaming operations.
7
- *
8
- * Unlike {@link withSpan}, which ends the span when `fn` resolves, this
9
- * helper extends the span's lifetime until the **consumer** of the returned
10
- * iterable reaches end-of-stream (success), throws (error), or aborts.
11
- *
12
- * Required for any operation whose result is a producer (`StreamResult`,
13
- * `AsyncIterable`-returning function, etc.) — ending the span on `fn`
14
- * resolution would capture only the setup phase and report zero tokens /
15
- * meaningless duration.
16
- *
17
- * @param options Span options (name, tracer, attributes, kind).
18
- * @param fn Callback that produces the result. Must NOT depend on
19
- * the iterable being consumed before returning.
20
- * @param getIterable Selector that picks the `AsyncIterable` out of the
21
- * result for wrapping.
22
- * @param setIterable Setter that returns a new result with the iterable
23
- * replaced by the wrapped (span-lifetime-extending)
24
- * iterable. Should be a pure function — clone the
25
- * result rather than mutating in place.
26
- *
6
+ * @description Wraps a streaming operation in a span that stays open until the consumer reaches end-of-stream, throws, or closes the iterable, so the span covers the whole stream rather than only its setup.
7
+ * @param options {SpanOptions} - Span name, tracer, kind and initial attributes.
8
+ * @param fn {(span: Span) => Promise<TResult>} - Produces the result; must not depend on the iterable being consumed before returning.
9
+ * @param getIterable {(result: TResult) => AsyncIterable<TChunk>} - Selects the iterable to wrap out of the result.
10
+ * @param setIterable {(result: TResult, wrapped: AsyncIterableIterator<TChunk>) => TResult} - Returns a new result carrying the wrapped iterable; must clone rather than mutate.
11
+ * @returns {Promise<TResult>} The result with its iterable replaced by the span-lifetime-extending one.
27
12
  * @example
28
13
  * ```ts
29
14
  * return withStreamSpan(
30
- * { name: "neurolink.provider.stream", tracer, attributes: {...} },
15
+ * { name: "provider.stream", tracer, attributes: {...} },
31
16
  * async () => this.executeStreamInner(options),
32
17
  * (r) => r.stream,
33
18
  * (r, wrapped) => ({ ...r, stream: wrapped }),
34
19
  * );
35
20
  * ```
36
21
  */
37
- export declare function withStreamSpan<TResult, TChunk>(options: SpanOptions, fn: (span: Span) => Promise<TResult>, getIterable: (result: TResult) => AsyncIterable<TChunk>, setIterable: (result: TResult, wrapped: AsyncGenerator<TChunk>) => TResult): Promise<TResult>;
22
+ export declare function withStreamSpan<TResult, TChunk>(options: SpanOptions, fn: (span: Span) => Promise<TResult>, getIterable: (result: TResult) => AsyncIterable<TChunk>, setIterable: (result: TResult, wrapped: AsyncIterableIterator<TChunk>) => TResult): Promise<TResult>;
23
+ /**
24
+ * @description Runs one model request inside a Langfuse generation span, recording the request half before `fn` runs. `fn` records the response half with `observeGeneration`; errors mark the span ERROR.
25
+ * @param request {GenerationRecord} - Provider, model, parameters and input sent.
26
+ * @param fn {(span: Span) => Promise<T>} - Issues the request and returns its result.
27
+ * @returns {Promise<T>} Whatever `fn` resolves to.
28
+ */
29
+ export declare function withGeneration<T>(request: GenerationRecord, fn: (span: Span) => Promise<T>): Promise<T>;
38
30
  /** Convenience CLIENT-kind alias matching `withClientSpan`. */
39
- export declare function withClientStreamSpan<TResult, TChunk>(options: Omit<SpanOptions, "kind">, fn: (span: Span) => Promise<TResult>, getIterable: (result: TResult) => AsyncIterable<TChunk>, setIterable: (result: TResult, wrapped: AsyncGenerator<TChunk>) => TResult): Promise<TResult>;
31
+ export declare function withClientStreamSpan<TResult, TChunk>(options: Omit<SpanOptions, "kind">, fn: (span: Span) => Promise<TResult>, getIterable: (result: TResult) => AsyncIterable<TChunk>, setIterable: (result: TResult, wrapped: AsyncIterableIterator<TChunk>) => TResult): Promise<TResult>;
32
+ /**
33
+ * @description Runs `fn` inside a `step` span, for a single model call that belongs to a step outside the shared loop, such as a forced finalization request.
34
+ * @param stepNumber {number} - One-based step number within the turn.
35
+ * @param fn {() => Promise<T>} - The step's work.
36
+ * @returns {Promise<T>} Whatever `fn` resolves to.
37
+ */
38
+ export declare function withStepSpan<T>(stepNumber: number, fn: () => Promise<T>): Promise<T>;
39
+ /**
40
+ * @description Groups the observations of each agentic loop step under one `step` span. Call `begin` at the top of a step, run the step's model call and tool dispatch through `run`, and `end` when the loop is over; `begin` closes the previous step.
41
+ */
42
+ export declare class StepSpans {
43
+ private current;
44
+ /**
45
+ * @description Closes the previous step and opens `step` under the active span.
46
+ * @param stepNumber {number} - One-based step number within the turn.
47
+ * @returns {void} Nothing.
48
+ */
49
+ begin(stepNumber: number): void;
50
+ /**
51
+ * @description Runs `fn` with the open step as the active span, so the observations it creates become the step's children.
52
+ * @param fn {() => T} - Work belonging to the current step.
53
+ * @returns {T} Whatever `fn` returns.
54
+ */
55
+ run<T>(fn: () => T): T;
56
+ /**
57
+ * @description Closes the open step, if any.
58
+ * @returns {void} Nothing.
59
+ */
60
+ end(): void;
61
+ }