@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
@@ -1,8 +1,13 @@
1
+ import { MIN_STRING_BUDGET_CHARS, REDACTED_VALUE, TRUNCATION_NOTE_ALLOWANCE_CHARS, } from "../constants/telemetry.js";
2
+ import { messageRole } from "./chatMessages.js";
3
+ import { binaryPlaceholder, declaredMimeType, describeDataUrls, isBinaryKey, redactSecretAssignments, redactSensitiveEntry, } from "./spanSanitizer.js";
4
+ import { capStrings, stringify } from "./spanSerialization.js";
1
5
  export const ATTR = {
2
6
  // GenAI standard (OpenTelemetry semantic conventions)
3
7
  GEN_AI_SYSTEM: "gen_ai.system",
4
8
  GEN_AI_MODEL: "gen_ai.request.model",
5
9
  GEN_AI_OPERATION: "gen_ai.operation.name",
10
+ GEN_AI_OBSERVATION: "gen_ai.observation.type",
6
11
  GEN_AI_INPUT_TOKENS: "gen_ai.usage.input_tokens",
7
12
  GEN_AI_OUTPUT_TOKENS: "gen_ai.usage.output_tokens",
8
13
  GEN_AI_FINISH_REASON: "gen_ai.response.finish_reason",
@@ -10,22 +15,20 @@ export const ATTR = {
10
15
  GEN_AI_TOOL_NAME: "gen_ai.tool.name",
11
16
  GEN_AI_TEMPERATURE: "gen_ai.request.temperature",
12
17
  GEN_AI_MAX_TOKENS: "gen_ai.request.max_tokens",
13
- // NeuroLink custom
14
- NL_PROVIDER: "neurolink.provider",
15
- NL_MODEL: "neurolink.model",
16
- NL_STREAM_MODE: "neurolink.stream_mode",
17
- NL_TOOL_COUNT: "neurolink.tool_count",
18
- NL_MESSAGE_COUNT: "neurolink.message_count",
19
- NL_HAS_TOOLS: "neurolink.has_tools",
20
- NL_INPUT_LENGTH: "neurolink.input_length",
21
- NL_OUTPUT_LENGTH: "neurolink.output_length",
22
- NL_REQUEST_ID: "neurolink.request_id",
23
- NL_PATH: "neurolink.path",
24
- NL_HAS_MEMORY: "neurolink.has_conversation_memory",
25
- NL_COST: "neurolink.cost",
26
- NL_STRUCTURED_OUTPUT: "neurolink.structured_output",
27
- NL_HAS_FALLBACK: "neurolink.has_fallback",
28
- // MCP
18
+ NL_PROVIDER: "sdk.provider",
19
+ NL_MODEL: "sdk.model",
20
+ NL_STREAM_MODE: "stream_mode",
21
+ NL_TOOL_COUNT: "tool_count",
22
+ NL_MESSAGE_COUNT: "message_count",
23
+ NL_HAS_TOOLS: "has_tools",
24
+ NL_INPUT_LENGTH: "input_length",
25
+ NL_OUTPUT_LENGTH: "output_length",
26
+ NL_REQUEST_ID: "request_id",
27
+ NL_PATH: "path",
28
+ NL_HAS_MEMORY: "has_conversation_memory",
29
+ NL_COST: "cost",
30
+ NL_STRUCTURED_OUTPUT: "structured_output",
31
+ NL_HAS_FALLBACK: "has_fallback",
29
32
  MCP_SERVER_ID: "mcp.server_id",
30
33
  MCP_TOOL_NAME: "mcp.tool_name",
31
34
  MCP_TIMEOUT_MS: "mcp.timeout_ms",
@@ -114,30 +117,6 @@ export const ATTR = {
114
117
  AR_DESCRIPTION: "autoresearch.description",
115
118
  AR_ERROR_CODE: "autoresearch.error_code",
116
119
  };
117
- /**
118
- * Langfuse observation/trace attribute names recognised by `@langfuse/otel`'s
119
- * LangfuseSpanProcessor (already registered on the global TracerProvider). They
120
- * let native (non-AI-SDK) provider paths emit spans that render as proper
121
- * generation / tool observations — the same data the Vercel AI SDK's
122
- * `experimental_telemetry` produced before providers moved to native SDKs.
123
- */
124
- export const LANGFUSE_ATTR = {
125
- TRACE_NAME: "langfuse.trace.name",
126
- TRACE_INPUT: "langfuse.trace.input",
127
- TRACE_OUTPUT: "langfuse.trace.output",
128
- TRACE_TAGS: "langfuse.trace.tags",
129
- TRACE_METADATA: "langfuse.trace.metadata",
130
- OBSERVATION_TYPE: "langfuse.observation.type",
131
- OBSERVATION_INPUT: "langfuse.observation.input",
132
- OBSERVATION_OUTPUT: "langfuse.observation.output",
133
- OBSERVATION_METADATA: "langfuse.observation.metadata",
134
- OBSERVATION_MODEL_NAME: "langfuse.observation.model.name",
135
- OBSERVATION_MODEL_PARAMETERS: "langfuse.observation.model.parameters",
136
- OBSERVATION_USAGE_DETAILS: "langfuse.observation.usage_details",
137
- OBSERVATION_LEVEL: "langfuse.observation.level",
138
- OBSERVATION_STATUS_MESSAGE: "langfuse.observation.status_message",
139
- OBSERVATION_COMPLETION_START_TIME: "langfuse.observation.completion_start_time",
140
- };
141
120
  /** Default ceiling for serialized span attribute values. */
142
121
  export const SPAN_ATTRIBUTE_MAX_CHARS = 40_000;
143
122
  /** Smallest ceiling accepted, so the system and question openings still fit. */
@@ -158,16 +137,12 @@ export function spanAttributeMaxChars() {
158
137
  const SYSTEM_PROMPT_FLOOR_CHARS = 2_000;
159
138
  /** Room the current question always gets in a model-request attribute. */
160
139
  const QUESTION_FLOOR_CHARS = 4_000;
161
- /** Room for the "...[truncated N chars]" note, kept outside that opening. */
162
- const TRUNCATION_NOTE_ALLOWANCE_CHARS = 48;
163
- const MIN_SHRINKABLE_STRING_CHARS = 64;
164
- const REDACTED_VALUE = "[REDACTED]";
165
140
  const REDACTION_FAILED_VALUE = "[content omitted: redaction failed]";
166
- const SENSITIVE_KEY_PATTERN = /(authorization|password|passwd|secret|apikey|privatekey|token|credential|signature)/;
141
+ const SENSITIVE_KEY_PATTERN = /(authorization|password|passwd|secret|apikey|privatekey|tokens?$|credential|signature)/;
167
142
  const COOKIE_KEY_PATTERN = /^(set)?cookies?$/;
168
143
  // Cheap pre-check: text without any of these is returned untouched, so the
169
144
  // common case costs one regex scan and stays byte-identical.
170
- const SENSITIVE_HINT_PATTERN = /authorization|password|passwd|secret|api[_-]?key|private[_-]?key|token|credential|signature|cookie|bearer/i;
145
+ const SENSITIVE_HINT_PATTERN = /authorization|password|passwd|secret|api[_-]?key|private[_-]?key|token|credential|signature|cookie|bearer|sk-|gh[pousr]_|github_pat_|AKIA|xox[baprs]-/i;
171
146
  function isSensitiveKey(key) {
172
147
  const normalized = key.replace(/[_-]/g, "").toLowerCase();
173
148
  return (SENSITIVE_KEY_PATTERN.test(normalized) ||
@@ -190,10 +165,7 @@ function redactText(text) {
190
165
  if (!SENSITIVE_HINT_PATTERN.test(text)) {
191
166
  return text;
192
167
  }
193
- let redacted = text
194
- .replace(/\bBearer\s+[\w.\-+/=]{16,}/gi, `Bearer ${REDACTED_VALUE}`)
195
- .replace(/\b(access_token|refresh_token|id_token|api_key|apikey|client_secret|password|signature|credential)=([^&\s"]+)/gi, `$1=${REDACTED_VALUE}`)
196
- .replace(/"([^"\\]{1,80})"(\s*:\s*)"(?:[^"\\]|\\.)*"/g, (match, key, separator) => isSensitiveKey(key) ? `"${key}"${separator}"${REDACTED_VALUE}"` : match);
168
+ let redacted = redactSecretAssignments(text.replace(/\bBearer\s+[\w.\-+/=]{16,}/gi, `Bearer ${REDACTED_VALUE}`)).replace(/"([^"\\]{1,80})"(\s*:\s*)"(?:[^"\\]|\\.)*"/g, (match, key, separator) => isSensitiveKey(key) ? `"${key}"${separator}"${REDACTED_VALUE}"` : match);
197
169
  ESCAPED_QUOTE_BACKSLASHES.forEach((backslashes, index) => {
198
170
  const quote = `${"\\".repeat(backslashes)}"`;
199
171
  redacted = redacted.replace(ESCAPED_PAIR_PATTERNS[index], (match, key, separator) => isSensitiveKey(key)
@@ -204,7 +176,7 @@ function redactText(text) {
204
176
  }
205
177
  function redactNode(value, ancestors) {
206
178
  if (typeof value === "string") {
207
- return redactText(value);
179
+ return redactText(describeDataUrls(value));
208
180
  }
209
181
  if (value === null || typeof value !== "object") {
210
182
  return value;
@@ -212,6 +184,9 @@ function redactNode(value, ancestors) {
212
184
  if (ancestors.has(value)) {
213
185
  return "[circular]";
214
186
  }
187
+ if (value instanceof ArrayBuffer || ArrayBuffer.isView(value)) {
188
+ return binaryPlaceholder(value, undefined);
189
+ }
215
190
  ancestors.add(value);
216
191
  try {
217
192
  if ("toJSON" in value && typeof value.toJSON === "function") {
@@ -222,15 +197,18 @@ function redactNode(value, ancestors) {
222
197
  }
223
198
  // No prototype, so a key named __proto__ is data rather than an assignment.
224
199
  const redacted = Object.create(null);
200
+ const mimeType = declaredMimeType(value);
225
201
  for (const [key, entry] of Object.entries(value)) {
226
- // Counts and flags under a sensitive-looking name (input_tokens: 5) are
227
- // not credentials; only text and structures are masked.
228
- redacted[key] =
229
- isSensitiveKey(key) &&
230
- (typeof entry === "string" ||
231
- (typeof entry === "object" && entry !== null))
232
- ? REDACTED_VALUE
233
- : redactNode(entry, ancestors);
202
+ if (isSensitiveKey(key) &&
203
+ (typeof entry === "string" ||
204
+ (typeof entry === "object" && entry !== null))) {
205
+ redacted[key] = redactSensitiveEntry(key, entry);
206
+ continue;
207
+ }
208
+ const placeholder = isBinaryKey(key)
209
+ ? binaryPlaceholder(entry, mimeType)
210
+ : undefined;
211
+ redacted[key] = placeholder ?? redactNode(entry, ancestors);
234
212
  }
235
213
  return redacted;
236
214
  }
@@ -239,12 +217,10 @@ function redactNode(value, ancestors) {
239
217
  }
240
218
  }
241
219
  /**
242
- * Returns a copy of `value` with credentials masked: values under keys such as
243
- * `access_token`, `password`, `authorization` or `set-cookie`, bearer tokens
244
- * and `name=value` credentials inside text. Content with nothing sensitive in
245
- * it serializes exactly as it did before. Throws only if the value cannot be
246
- * walked; callers on the tracing path use `spanContentAttribute`, which turns
247
- * that into an omitted value instead of a leak or a failed turn.
220
+ * @description Returns a copy of a value with credentials masked: values under keys such as `access_token`, `password`, `authorization` or `set-cookie`, bearer tokens, and `name=value` credentials inside text. Purely numeric values after a credential-looking name (e.g. `max_tokens: 4096`) are left alone.
221
+ * @param value {JsonValue | object} - Content to redact; objects and arrays are walked recursively.
222
+ * @returns {JsonValue | object} A redacted copy, serializing exactly as the input when nothing in it is sensitive.
223
+ * @throws {Error} When the value cannot be walked; tracing callers use `spanContentAttribute`, which turns that into an omitted value.
248
224
  */
249
225
  export function redactSpanContent(value) {
250
226
  return redactNode(value, new WeakSet());
@@ -257,70 +233,26 @@ function redactSafely(value) {
257
233
  return REDACTION_FAILED_VALUE;
258
234
  }
259
235
  }
260
- const MAX_SHRINK_ATTEMPTS = 200;
261
- /**
262
- * Cuts the longest string inside `root` down by at least `excess` characters,
263
- * in place, and says whether it found one worth cutting. Only ever called on
264
- * the copy `redactSpanContent` made, never on the caller's own data.
265
- */
266
- function shrinkLongestString(root, excess) {
267
- let owner;
268
- let ownerKey = "";
269
- let longestText = "";
270
- const visited = new Set();
271
- const pending = [root];
272
- while (pending.length > 0) {
273
- const node = pending.pop();
274
- if (node === undefined || visited.has(node)) {
275
- continue;
276
- }
277
- visited.add(node);
278
- for (const [key, entry] of Object.entries(node)) {
279
- if (typeof entry === "string") {
280
- if (entry.length > longestText.length &&
281
- entry.length > MIN_SHRINKABLE_STRING_CHARS) {
282
- longestText = entry;
283
- owner = node;
284
- ownerKey = key;
285
- }
286
- }
287
- else if (entry !== null && typeof entry === "object") {
288
- pending.push(entry);
289
- }
290
- }
291
- }
292
- if (owner === undefined) {
293
- return false;
294
- }
295
- const keep = Math.max(MIN_SHRINKABLE_STRING_CHARS / 2, longestText.length - excess - TRUNCATION_NOTE_ALLOWANCE_CHARS);
296
- Reflect.set(owner, ownerKey, `${longestText.slice(0, keep)}...[truncated ${longestText.length - keep} chars]`);
297
- return true;
298
- }
299
236
  /**
300
237
  * `spanJsonAttribute` for content a person or model wrote or a tool returned
301
238
  * (arguments, results, answers), with credentials masked first. Use the plain
302
239
  * form for metadata such as usage counts and server ids.
303
240
  *
304
- * A structured value over the cap is shortened by trimming its longest
305
- * strings, so the attribute stays valid JSON instead of ending mid-token.
241
+ * A structured value over the cap is shortened by cutting every string to a
242
+ * per-string budget in one walk, tightening the budget only if the result
243
+ * still does not fit, so the attribute stays valid JSON instead of ending
244
+ * mid-token.
306
245
  */
307
246
  export function spanContentAttribute(value, maxChars = spanAttributeMaxChars()) {
308
247
  const redacted = redactSafely(value);
309
248
  if (redacted === null || typeof redacted !== "object") {
310
249
  return spanJsonAttribute(redacted, maxChars);
311
250
  }
312
- let serialized;
313
- try {
314
- serialized = JSON.stringify(redacted) ?? String(redacted);
315
- }
316
- catch {
317
- return spanJsonAttribute(redacted, maxChars);
318
- }
319
- for (let attempt = 0; serialized.length > maxChars && attempt < MAX_SHRINK_ATTEMPTS; attempt++) {
320
- if (!shrinkLongestString(redacted, serialized.length - maxChars)) {
321
- break;
322
- }
323
- serialized = JSON.stringify(redacted);
251
+ let serialized = stringify(redacted);
252
+ let budget = Math.floor(maxChars / 4);
253
+ while (serialized.length > maxChars && budget >= MIN_STRING_BUDGET_CHARS) {
254
+ serialized = stringify(capStrings(redacted, budget));
255
+ budget = Math.floor(budget / 4);
324
256
  }
325
257
  return spanJsonAttribute(serialized, maxChars);
326
258
  }
@@ -331,16 +263,7 @@ export function spanContentAttribute(value, maxChars = spanAttributeMaxChars())
331
263
  * JSON-stringified with a String() fallback for circular structures.
332
264
  */
333
265
  export function spanJsonAttribute(value, maxChars = spanAttributeMaxChars()) {
334
- let serialized;
335
- try {
336
- serialized =
337
- typeof value === "string"
338
- ? value
339
- : (JSON.stringify(value) ?? String(value));
340
- }
341
- catch {
342
- serialized = String(value);
343
- }
266
+ const serialized = stringify(value);
344
267
  if (serialized.length > maxChars) {
345
268
  const truncationSuffix = `...[truncated ${serialized.length - maxChars} chars]`;
346
269
  const keepLength = Math.max(0, maxChars - truncationSuffix.length);
@@ -427,21 +350,9 @@ export function spanModelRequestAttribute(rawRequest, maxChars = spanAttributeMa
427
350
  }
428
351
  : {}),
429
352
  ...(rawRequest.prompt !== undefined
430
- ? { prompt: redactText(rawRequest.prompt) }
353
+ ? { prompt: redactText(describeDataUrls(rawRequest.prompt)) }
431
354
  : {}),
432
355
  };
433
- const serialize = (value) => {
434
- try {
435
- return typeof value === "string"
436
- ? value
437
- : (JSON.stringify(value) ?? String(value));
438
- }
439
- catch {
440
- return String(value);
441
- }
442
- };
443
- // Sizes are measured as JSON, which is what the attribute carries: escaping
444
- // makes a JSON string longer than its text.
445
356
  const jsonLength = (value) => {
446
357
  try {
447
358
  return (JSON.stringify(value) ?? "null").length;
@@ -488,23 +399,22 @@ export function spanModelRequestAttribute(rawRequest, maxChars = spanAttributeMa
488
399
  }
489
400
  let copy;
490
401
  try {
491
- copy = JSON.parse(serialize(message));
402
+ copy = JSON.parse(stringify(message));
492
403
  }
493
404
  catch {
494
405
  copy = undefined;
495
406
  }
496
407
  if (copy !== null && typeof copy === "object") {
497
- for (let attempt = 0; jsonLength(copy) > budget && attempt < MAX_SHRINK_ATTEMPTS; attempt++) {
498
- if (!shrinkLongestString(copy, jsonLength(copy) - budget)) {
499
- break;
500
- }
408
+ for (let stringBudget = Math.floor(budget / 4); jsonLength(copy) > budget && stringBudget >= MIN_STRING_BUDGET_CHARS; stringBudget = Math.floor(stringBudget / 4)) {
409
+ capStrings(copy, stringBudget);
501
410
  }
502
411
  if (jsonLength(copy) <= budget) {
503
412
  return copy;
504
413
  }
505
414
  }
506
415
  return {
507
- truncatedMessage: fitText(serialize(message), Math.max(0, budget - 32)),
416
+ role: messageRole(message),
417
+ content: fitText(stringify(message), Math.max(0, budget - 32)),
508
418
  };
509
419
  };
510
420
  const messages = request.messages ?? [];
@@ -532,8 +442,8 @@ export function spanModelRequestAttribute(rawRequest, maxChars = spanAttributeMa
532
442
  }
533
443
  turnComplete = false;
534
444
  if (turn.length === 0 && remaining > 64) {
535
- const excerpt = fitText(serialize(messages[index]), remaining - 32);
536
- turn.unshift({ truncatedMessage: excerpt });
445
+ const excerpt = fitText(stringify(messages[index]), remaining - 32);
446
+ turn.unshift({ role: messageRole(messages[index]), content: excerpt });
537
447
  remaining -= jsonLength(excerpt) + 32;
538
448
  }
539
449
  break;
@@ -560,26 +470,30 @@ export function spanModelRequestAttribute(rawRequest, maxChars = spanAttributeMa
560
470
  }
561
471
  const omittedMessages = messages.length - kept.length;
562
472
  remaining += systemFloor;
563
- const body = {};
473
+ const leading = [];
564
474
  if (request.system !== undefined) {
565
- body.system =
566
- jsonLength(request.system) <= remaining
475
+ leading.push({
476
+ role: "system",
477
+ content: jsonLength(request.system) <= remaining
567
478
  ? request.system
568
- : fitText(serialize(request.system), Math.max(0, remaining));
569
- }
570
- if (request.messages !== undefined) {
571
- body.messages = kept;
479
+ : fitText(stringify(request.system), Math.max(0, remaining)),
480
+ });
572
481
  }
573
482
  if (omittedMessages > 0) {
574
- body.omittedMessages = omittedMessages;
575
- }
576
- if (prompt !== undefined) {
577
- body.prompt = prompt;
483
+ leading.push({
484
+ role: "system",
485
+ content: `[${omittedMessages} earlier messages omitted]`,
486
+ });
578
487
  }
488
+ const conversation = [
489
+ ...leading,
490
+ ...kept,
491
+ ...(prompt !== undefined ? [{ role: "user", content: prompt }] : []),
492
+ ];
579
493
  try {
580
- return JSON.stringify(body);
494
+ return JSON.stringify(conversation);
581
495
  }
582
496
  catch {
583
- return spanJsonAttribute(body, maxChars);
497
+ return spanJsonAttribute(conversation, maxChars);
584
498
  }
585
499
  }
@@ -0,0 +1,13 @@
1
+ import type { AssistantOutput } from "../types/index.js";
2
+ /**
3
+ * @description Reads the role of a chat message, treating anything without a string role as a user message.
4
+ * @param message {T} - Message of any provider shape.
5
+ * @returns {string} The message role, or `user` when it declares none.
6
+ */
7
+ export declare function messageRole<T>(message: T): string;
8
+ /**
9
+ * @description Shapes a model response as an assistant message array that Langfuse renders as a conversation.
10
+ * @param output {AssistantOutput | undefined} - A text string, a reply with `text`, `reasoning` and `toolCalls`, a message array, or provider content parts.
11
+ * @returns {object[] | undefined} `[{ role: "assistant", content, tool_calls? }]`, the input untouched when it is already a message array, or `undefined` when there is no output.
12
+ */
13
+ export declare function assistantMessages(output: AssistantOutput | undefined): object[] | undefined;
@@ -0,0 +1,59 @@
1
+ import { stringify } from "./spanSerialization.js";
2
+ function chatToolCall(call) {
3
+ const args = call.args ?? call.input ?? call.arguments ?? call.function?.arguments;
4
+ return {
5
+ id: call.id ?? call.toolCallId,
6
+ type: "function",
7
+ function: {
8
+ name: call.name ?? call.toolName ?? call.function?.name,
9
+ arguments: typeof args === "string" ? args : stringify(args ?? {}),
10
+ },
11
+ };
12
+ }
13
+ function isChatMessages(output) {
14
+ return (output.length > 0 &&
15
+ output.every((entry) => "role" in entry && typeof entry.role === "string"));
16
+ }
17
+ function isAssistantReply(output) {
18
+ return (typeof output === "object" &&
19
+ !Array.isArray(output) &&
20
+ ("text" in output || "toolCalls" in output));
21
+ }
22
+ /**
23
+ * @description Reads the role of a chat message, treating anything without a string role as a user message.
24
+ * @param message {T} - Message of any provider shape.
25
+ * @returns {string} The message role, or `user` when it declares none.
26
+ */
27
+ export function messageRole(message) {
28
+ return typeof message === "object" &&
29
+ message !== null &&
30
+ "role" in message &&
31
+ typeof message.role === "string"
32
+ ? message.role
33
+ : "user";
34
+ }
35
+ /**
36
+ * @description Shapes a model response as an assistant message array that Langfuse renders as a conversation.
37
+ * @param output {AssistantOutput | undefined} - A text string, a reply with `text`, `reasoning` and `toolCalls`, a message array, or provider content parts.
38
+ * @returns {object[] | undefined} `[{ role: "assistant", content, tool_calls? }]`, the input untouched when it is already a message array, or `undefined` when there is no output.
39
+ */
40
+ export function assistantMessages(output) {
41
+ if (output === undefined) {
42
+ return undefined;
43
+ }
44
+ if (Array.isArray(output) && isChatMessages(output)) {
45
+ return output;
46
+ }
47
+ if (!isAssistantReply(output)) {
48
+ return [{ role: "assistant", content: output }];
49
+ }
50
+ const toolCalls = (output.toolCalls ?? []).map(chatToolCall);
51
+ return [
52
+ {
53
+ role: "assistant",
54
+ content: output.text ?? "",
55
+ ...(output.reasoning ? { reasoning: output.reasoning } : {}),
56
+ ...(toolCalls.length > 0 ? { tool_calls: toolCalls } : {}),
57
+ },
58
+ ];
59
+ }
@@ -1,20 +1,6 @@
1
- export { TelemetryService } from "./telemetryService.js";
2
1
  export { tracers } from "./tracers.js";
3
- export { withSpan, withClientSpan, withStreamSpan, withClientStreamSpan, } from "./withSpan.js";
4
- export { ATTR, LANGFUSE_ATTR, SPAN_ATTRIBUTE_MAX_CHARS, redactSpanContent, spanAttributeMaxChars, spanContentAttribute, spanJsonAttribute, spanModelRequestAttribute, } from "./attributes.js";
5
- /**
6
- * Initialize telemetry for NeuroLink
7
- * Reuses an existing global TracerProvider when one is already registered,
8
- * otherwise bootstraps Neurolink telemetry when an exporter endpoint is configured.
9
- */
10
- export declare function initializeTelemetry(): Promise<import("./telemetryService.js").TelemetryService>;
11
- /**
12
- * Get telemetry status
13
- */
14
- export declare function getTelemetryStatus(): Promise<{
15
- enabled: boolean;
16
- initialized: boolean;
17
- endpoint?: string;
18
- service?: string;
19
- version?: string;
20
- }>;
2
+ export { withSpan, withClientSpan, withStreamSpan, withClientStreamSpan, StepSpans, withStepSpan, } from "./withSpan.js";
3
+ export { ATTR, SPAN_ATTRIBUTE_MAX_CHARS, redactSpanContent, spanAttributeMaxChars, spanContentAttribute, spanJsonAttribute, spanModelRequestAttribute, } from "./attributes.js";
4
+ export { assistantMessages } from "./chatMessages.js";
5
+ export { observationLevel, observe, observeGeneration, observeTrace, usageDetails, } from "./observation.js";
6
+ export { generationName, stepSpanName, toolSpanName, } from "./observationNames.js";
@@ -1,27 +1,6 @@
1
- // Optional Telemetry Infrastructure (Phase 2)
2
- export { TelemetryService } from "./telemetryService.js";
3
1
  export { tracers } from "./tracers.js";
4
- export { withSpan, withClientSpan, withStreamSpan, withClientStreamSpan, } from "./withSpan.js";
5
- export { ATTR, LANGFUSE_ATTR, SPAN_ATTRIBUTE_MAX_CHARS, redactSpanContent, spanAttributeMaxChars, spanContentAttribute, spanJsonAttribute, spanModelRequestAttribute, } from "./attributes.js";
6
- import { logger } from "../utils/logger.js";
7
- /**
8
- * Initialize telemetry for NeuroLink
9
- * Reuses an existing global TracerProvider when one is already registered,
10
- * otherwise bootstraps Neurolink telemetry when an exporter endpoint is configured.
11
- */
12
- export async function initializeTelemetry() {
13
- const { TelemetryService } = await import("./telemetryService.js");
14
- const telemetry = TelemetryService.getInstance();
15
- if (telemetry.isEnabled()) {
16
- await telemetry.initialize();
17
- logger.info("[NeuroLink] Telemetry initialized");
18
- }
19
- return telemetry;
20
- }
21
- /**
22
- * Get telemetry status
23
- */
24
- export async function getTelemetryStatus() {
25
- const { TelemetryService } = await import("./telemetryService.js");
26
- return TelemetryService.getInstance().getStatus();
27
- }
2
+ export { withSpan, withClientSpan, withStreamSpan, withClientStreamSpan, StepSpans, withStepSpan, } from "./withSpan.js";
3
+ export { ATTR, SPAN_ATTRIBUTE_MAX_CHARS, redactSpanContent, spanAttributeMaxChars, spanContentAttribute, spanJsonAttribute, spanModelRequestAttribute, } from "./attributes.js";
4
+ export { assistantMessages } from "./chatMessages.js";
5
+ export { observationLevel, observe, observeGeneration, observeTrace, usageDetails, } from "./observation.js";
6
+ export { generationName, stepSpanName, toolSpanName, } from "./observationNames.js";
@@ -0,0 +1,37 @@
1
+ import type { Span } from "@opentelemetry/api";
2
+ import type { GenerationRecord, ObservationAttributes, ObservationLevel, ObservationType, RawUsageObject, TraceContent } from "../types/index.js";
3
+ /**
4
+ * @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.
5
+ * @param span {Span} - Span to describe.
6
+ * @param type {ObservationType} - Observation type Langfuse renders the span as.
7
+ * @param attrs {ObservationAttributes} - Input, output, model, parameters, usage, cost, level and metadata to record.
8
+ * @returns {void} Nothing.
9
+ */
10
+ export declare function observe(span: Span, type: ObservationType, attrs: ObservationAttributes): void;
11
+ /**
12
+ * @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.
13
+ * @param span {Span} - Span whose trace is described.
14
+ * @param io {TraceContent} - Trace input, output and metadata.
15
+ * @returns {void} Nothing.
16
+ */
17
+ export declare function observeTrace(span: Span, io: TraceContent): void;
18
+ /**
19
+ * @description Maps any provider's token usage onto Langfuse usage-detail keys.
20
+ * @param usage {RawUsageObject | undefined | null} - Token usage in any naming convention NeuroLink normalizes.
21
+ * @returns {Record<string, number>} `input`, `output` and `total`, plus `cache_read_input_tokens`, `cache_creation_input_tokens` and `reasoning` when reported.
22
+ */
23
+ export declare function usageDetails(usage: RawUsageObject | undefined | null): Record<string, number>;
24
+ /**
25
+ * @description Picks the Langfuse level for a model call that finished without throwing.
26
+ * @param finishReason {string | undefined} - Provider finish reason, in any casing.
27
+ * @param aborted {boolean | undefined} - Whether the caller aborted or closed the call early.
28
+ * @returns {ObservationLevel | undefined} `"WARNING"` for truncated, filtered, aborted or empty output, otherwise `undefined`.
29
+ */
30
+ export declare function observationLevel(finishReason?: string, aborted?: boolean): ObservationLevel | undefined;
31
+ /**
32
+ * @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.
33
+ * @param span {Span} - Span of one model request.
34
+ * @param call {GenerationRecord} - What was sent and what came back.
35
+ * @returns {void} Nothing.
36
+ */
37
+ export declare function observeGeneration(span: Span, call: GenerationRecord): void;