@juspay/neurolink 12.58.1 → 13.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/CHANGELOG.md +14 -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/common.js +1 -1
  81. package/dist/server/routes/agentRoutes.js +4 -4
  82. package/dist/server/routes/claudeProxyRoutes.js +99 -196
  83. package/dist/server/routes/codexProxyRoutes.js +621 -591
  84. package/dist/server/routes/healthRoutes.js +6 -6
  85. package/dist/server/routes/mcpRoutes.js +7 -7
  86. package/dist/server/routes/memoryRoutes.js +7 -7
  87. package/dist/server/routes/openApiRoutes.js +3 -3
  88. package/dist/server/routes/toolRoutes.js +5 -5
  89. package/dist/server/routes/transcribeRoutes.js +1 -1
  90. package/dist/server/utils/validation.d.ts +3 -3
  91. package/dist/services/server/ai/observability/instrumentation.d.ts +34 -201
  92. package/dist/services/server/ai/observability/instrumentation.js +220 -1267
  93. package/dist/services/server/ai/observability/langfuseSetup.d.ts +14 -0
  94. package/dist/services/server/ai/observability/langfuseSetup.js +55 -0
  95. package/dist/services/server/ai/observability/otlpSetup.d.ts +16 -0
  96. package/dist/services/server/ai/observability/otlpSetup.js +58 -0
  97. package/dist/session/globalSessionState.js +0 -11
  98. package/dist/telemetry/attributes.d.ts +23 -46
  99. package/dist/telemetry/attributes.js +76 -162
  100. package/dist/telemetry/chatMessages.d.ts +13 -0
  101. package/dist/telemetry/chatMessages.js +59 -0
  102. package/dist/telemetry/index.d.ts +5 -19
  103. package/dist/telemetry/index.js +5 -26
  104. package/dist/telemetry/observation.d.ts +37 -0
  105. package/dist/telemetry/observation.js +112 -0
  106. package/dist/telemetry/observationNames.d.ts +18 -0
  107. package/dist/telemetry/observationNames.js +24 -0
  108. package/dist/telemetry/spanSanitizer.d.ts +39 -0
  109. package/dist/telemetry/spanSanitizer.js +92 -0
  110. package/dist/telemetry/spanSerialization.d.ts +13 -0
  111. package/dist/telemetry/spanSerialization.js +46 -0
  112. package/dist/telemetry/tracers.d.ts +37 -18
  113. package/dist/telemetry/tracers.js +97 -19
  114. package/dist/telemetry/withSpan.d.ts +47 -25
  115. package/dist/telemetry/withSpan.js +158 -71
  116. package/dist/types/cli.d.ts +0 -11
  117. package/dist/types/decision.d.ts +3 -0
  118. package/dist/types/exporter.d.ts +54 -0
  119. package/dist/types/exporter.js +0 -4
  120. package/dist/types/generate.d.ts +4 -1
  121. package/dist/types/isolatedAgent.d.ts +5 -4
  122. package/dist/types/loopEngine.d.ts +18 -28
  123. package/dist/types/observability.d.ts +127 -160
  124. package/dist/types/providers.d.ts +15 -0
  125. package/dist/types/proxy.d.ts +41 -2
  126. package/dist/utils/anthropicTraceSanitizer.d.ts +2 -2
  127. package/dist/utils/anthropicTraceSanitizer.js +3 -1
  128. package/dist/utils/conversationMemory.js +4 -4
  129. package/dist/utils/fileDetector.js +1 -1
  130. package/dist/utils/messageBuilder.js +1 -1
  131. package/dist/utils/noOutputSentinel.d.ts +4 -16
  132. package/dist/utils/noOutputSentinel.js +12 -28
  133. package/dist/utils/providerRetry.js +4 -4
  134. package/dist/voice/livekit/brain.js +1 -1
  135. package/dist/voice/livekit/realtimeMemory.d.ts +1 -1
  136. package/dist/voice/livekit/realtimeMemory.js +66 -1
  137. package/dist/voice/livekit/realtimeVoiceAgent.js +1 -1
  138. package/docs-site/static/search-index.json +42 -63
  139. package/package.json +10 -4
  140. package/dist/telemetry/telemetryService.d.ts +0 -79
  141. package/dist/telemetry/telemetryService.js +0 -572
  142. package/dist/utils/observabilityHelpers.d.ts +0 -27
  143. package/dist/utils/observabilityHelpers.js +0 -47
@@ -6,22 +6,20 @@
6
6
  * receive -> account_selection -> upstream (per retry) -> stream -> end
7
7
  *
8
8
  * Uses the existing instrumentation infrastructure:
9
- * - getTracer() from instrumentation.ts for span creation
10
9
  * - setLangfuseContext() for Langfuse enrichment
11
- * - OtelBridge for context propagation to/from upstream
10
+ * - OTel propagation API for context propagation to/from upstream
12
11
  * - SpanAttributes from spanTypes.ts for attribute naming
13
12
  * - calculateCost() from pricing.ts for cost tracking
14
- * - TelemetryService for metrics recording
15
13
  */
16
14
  import { SpanStatusCode, SpanKind, ROOT_CONTEXT, isSpanContextValid, propagation, context, metrics, trace, } from "@opentelemetry/api";
17
- import { getTracer, setLangfuseContext, } from "../services/server/ai/observability/instrumentation.js";
18
- import { OtelBridge } from "../observability/otelBridge.js";
15
+ import { setLangfuseContext } from "../services/server/ai/observability/instrumentation.js";
16
+ import { spanContentAttribute } from "../telemetry/attributes.js";
17
+ import { observationLevel, observe, observeTrace, usageDetails, } from "../telemetry/observation.js";
18
+ import { generationName } from "../telemetry/observationNames.js";
19
19
  import { proxyTokenUsage } from "./proxyTokenUsage.js";
20
20
  import { calculateCost, hasPricing, isExactPricingMatch, } from "../utils/pricing.js";
21
- import { TelemetryService } from "../telemetry/telemetryService.js";
22
21
  import { logger } from "../utils/logger.js";
23
22
  import { registerProxyRequestTraceContext, releaseProxyRequestTraceContext, } from "./proxyTraceContext.js";
24
- const LOG_PREFIX = "[ProxyTracer]";
25
23
  // ---------------------------------------------------------------------------
26
24
  // OTEL Metric Instruments — lazy singleton
27
25
  //
@@ -36,7 +34,7 @@ function getProxyMetrics() {
36
34
  if (_metrics) {
37
35
  return _metrics;
38
36
  }
39
- const meter = metrics.getMeter("neurolink.proxy", "1.0.0");
37
+ const meter = metrics.getMeter("proxy", "1.0.0");
40
38
  const createdMetrics = {
41
39
  requestsTotal: meter.createCounter("proxy_requests_total", {
42
40
  description: "Total number of proxy requests",
@@ -178,6 +176,14 @@ function buildBodyEventAttributes(body) {
178
176
  "proxy.body.truncated": body.length > MAX_BODY_LOG_SIZE,
179
177
  };
180
178
  }
179
+ function parseBody(body) {
180
+ try {
181
+ return JSON.parse(body);
182
+ }
183
+ catch {
184
+ return body;
185
+ }
186
+ }
181
187
  // ---------------------------------------------------------------------------
182
188
  // Client app detection
183
189
  // ---------------------------------------------------------------------------
@@ -198,8 +204,8 @@ function detectClientApp(userAgent) {
198
204
  // ---------------------------------------------------------------------------
199
205
  class ProxyTracer {
200
206
  rootSpan;
201
- proxyTracer = getTracer("neurolink.proxy");
202
- bridge = new OtelBridge();
207
+ traceContext;
208
+ proxyTracer = trace.getTracer("proxy");
203
209
  requestId;
204
210
  /**
205
211
  * Model used for costing. Starts as the model the client asked for and is
@@ -223,10 +229,21 @@ class ProxyTracer {
223
229
  mode = "full";
224
230
  /** Where the Claude-shaped request originated, for cache/cost attribution. */
225
231
  requestOrigin = "native";
226
- constructor(rootSpan, requestId, model, stream, billingProvider) {
232
+ requestedModel;
233
+ input;
234
+ output;
235
+ responseModel;
236
+ finishReason;
237
+ firstOutputAt;
238
+ upstreamStartedAt;
239
+ cost;
240
+ statusMessage;
241
+ constructor(rootSpan, traceContext, requestId, model, stream, billingProvider) {
227
242
  this.rootSpan = rootSpan;
243
+ this.traceContext = traceContext;
228
244
  this.requestId = requestId;
229
245
  this.model = model;
246
+ this.requestedModel = model;
230
247
  this.billingProvider = billingProvider;
231
248
  this.startTime = Date.now();
232
249
  this.isStream = stream;
@@ -235,35 +252,47 @@ class ProxyTracer {
235
252
  * Create a root span for a proxy request and set Langfuse context.
236
253
  *
237
254
  * If the incoming request carries a `traceparent` header, the root span
238
- * will be linked to the caller's trace via OtelBridge.extractContext().
255
+ * will be linked to the caller's trace via the OTel propagation API.
239
256
  */
240
257
  static startRequest(ctx, incomingHeaders) {
241
- const tracer = getTracer("neurolink.proxy");
258
+ const tracer = trace.getTracer("proxy");
242
259
  // Extract parent context from incoming headers (Claude Code may send traceparent)
243
260
  let parentContext = context.active();
244
261
  if (incomingHeaders && !trace.getSpan(context.active())) {
245
- const bridge = new OtelBridge();
246
- const extracted = bridge.extractContext(incomingHeaders);
262
+ const headers = { ...incomingHeaders };
263
+ if (headers["traceparent"]?.includes(", ")) {
264
+ headers["traceparent"] = headers["traceparent"].split(", ")[0];
265
+ }
266
+ const extracted = trace.getSpanContext(propagation.extract(context.active(), headers));
247
267
  if (extracted) {
248
268
  // Create a remote span context so the root span becomes a child of the caller
249
269
  parentContext = trace.setSpanContext(context.active(), extracted);
250
270
  }
251
271
  }
252
272
  const clientApp = ctx.clientApp ?? detectClientApp(ctx.userAgent);
273
+ const traceContext = context.with(parentContext, () => setLangfuseContext({
274
+ userId: incomingHeaders?.["x-neurolink-user-id"],
275
+ sessionId: incomingHeaders?.["x-neurolink-session-id"] ?? ctx.sessionId,
276
+ conversationId: incomingHeaders?.["x-neurolink-conversation-id"],
277
+ requestId: ctx.requestId,
278
+ traceName: `proxy:${ctx.model}`,
279
+ metadata: {
280
+ clientApp,
281
+ stream: ctx.stream,
282
+ toolCount: ctx.toolCount,
283
+ },
284
+ }, () => context.active()));
253
285
  const rootSpan = tracer.startSpan("proxy.request", {
254
286
  attributes: {
255
287
  "proxy.request_id": ctx.requestId,
256
288
  "http.method": ctx.method,
257
289
  "http.target": ctx.path,
258
- "gen_ai.request.model": ctx.model,
290
+ "proxy.request.model": ctx.model,
259
291
  "proxy.stream": ctx.stream,
260
292
  "proxy.tool_count": ctx.toolCount,
261
293
  "proxy.client_app": clientApp,
262
294
  },
263
- }, parentContext);
264
- if (ctx.sessionId) {
265
- rootSpan.setAttribute("session.id", ctx.sessionId);
266
- }
295
+ }, traceContext);
267
296
  if (ctx.userAgent) {
268
297
  rootSpan.setAttribute("http.user_agent", ctx.userAgent);
269
298
  }
@@ -271,42 +300,10 @@ class ProxyTracer {
271
300
  // What the caller exposed to the model (tool catalogue for this request).
272
301
  rootSpan.setAttribute("gen_ai.request.tool_names", JSON.stringify(ctx.toolNames));
273
302
  }
274
- // Read x-neurolink-* context headers from calling SDK (e.g., Curator)
275
- const nlSessionId = incomingHeaders?.["x-neurolink-session-id"];
276
- const nlUserId = incomingHeaders?.["x-neurolink-user-id"];
277
- const nlConversationId = incomingHeaders?.["x-neurolink-conversation-id"];
278
- if (nlSessionId) {
279
- rootSpan.setAttribute("neurolink.session_id", nlSessionId);
280
- }
281
- if (nlUserId) {
282
- rootSpan.setAttribute("neurolink.user_id", nlUserId);
283
- }
284
- if (nlConversationId) {
285
- rootSpan.setAttribute("neurolink.conversation_id", nlConversationId);
286
- }
287
- const instance = new ProxyTracer(rootSpan, ctx.requestId, ctx.model, ctx.stream, ctx.provider ?? "anthropic");
303
+ const instance = new ProxyTracer(rootSpan, trace.setSpan(traceContext, rootSpan), ctx.requestId, ctx.model, ctx.stream, ctx.provider ?? "anthropic");
288
304
  instance.recordRequestMetrics = ctx.recordRequestMetrics !== false;
289
305
  instance.recordUsageMetrics =
290
306
  ctx.recordUsageMetrics ?? instance.recordRequestMetrics;
291
- // Set Langfuse context (fire-and-forget — non-blocking)
292
- // Prefer NeuroLink session/user from calling SDK over Claude Code session
293
- setLangfuseContext({
294
- sessionId: nlSessionId ?? ctx.sessionId,
295
- userId: nlUserId,
296
- conversationId: nlConversationId,
297
- requestId: ctx.requestId,
298
- traceName: `proxy:${ctx.model}`,
299
- operationName: "proxy.request",
300
- metadata: {
301
- clientApp,
302
- stream: ctx.stream,
303
- toolCount: ctx.toolCount,
304
- },
305
- }).catch((err) => {
306
- logger.debug(`${LOG_PREFIX} Failed to set Langfuse context`, {
307
- error: err instanceof Error ? err.message : String(err),
308
- });
309
- });
310
307
  return instance;
311
308
  }
312
309
  // -------------------------------------------------------------------------
@@ -318,7 +315,7 @@ class ProxyTracer {
318
315
  attributes: {
319
316
  "proxy.request_id": this.requestId,
320
317
  },
321
- }, trace.setSpan(context.active(), this.rootSpan));
318
+ }, this.traceContext);
322
319
  }
323
320
  /** Span covering account selection logic (fill-first / round-robin). */
324
321
  startAccountSelection() {
@@ -326,10 +323,11 @@ class ProxyTracer {
326
323
  attributes: {
327
324
  "proxy.request_id": this.requestId,
328
325
  },
329
- }, trace.setSpan(context.active(), this.rootSpan));
326
+ }, this.traceContext);
330
327
  }
331
328
  /** Span covering a single upstream attempt. One per retry. */
332
329
  startUpstreamAttempt(ctx) {
330
+ this.upstreamStartedAt ??= Date.now();
333
331
  return this.proxyTracer.startSpan("proxy.upstream", {
334
332
  attributes: {
335
333
  "proxy.request_id": this.requestId,
@@ -339,16 +337,16 @@ class ProxyTracer {
339
337
  "proxy.upstream.polyfill_body": ctx.polyfillBody,
340
338
  "http.url": ctx.upstreamUrl,
341
339
  },
342
- }, trace.setSpan(context.active(), this.rootSpan));
340
+ }, this.traceContext);
343
341
  }
344
342
  /** Span covering the SSE stream relay phase. */
345
343
  startStream() {
346
344
  return this.proxyTracer.startSpan("proxy.stream", {
347
345
  attributes: {
348
346
  "proxy.request_id": this.requestId,
349
- "gen_ai.request.model": this.model,
347
+ "proxy.request.model": this.model,
350
348
  },
351
- }, trace.setSpan(context.active(), this.rootSpan));
349
+ }, this.traceContext);
352
350
  }
353
351
  // -------------------------------------------------------------------------
354
352
  // Attribute setters
@@ -369,10 +367,6 @@ class ProxyTracer {
369
367
  if (ctx.rateLimitBefore7d !== undefined) {
370
368
  this.rootSpan.setAttribute("proxy.ratelimit.before.7d", ctx.rateLimitBefore7d);
371
369
  }
372
- // Update Langfuse context with account as userId
373
- setLangfuseContext({ userId: ctx.selectedAccount }).catch(() => {
374
- // Non-fatal
375
- });
376
370
  }
377
371
  /** Attribute fallback metrics to the account that actually served them. */
378
372
  setServedAccount(account, accountType) {
@@ -416,11 +410,10 @@ class ProxyTracer {
416
410
  if (ctx.reasoningTokens !== undefined) {
417
411
  this.rootSpan.setAttribute("ai.tokens.reasoning", ctx.reasoningTokens);
418
412
  }
419
- // GenAI semantic convention attributes (for Langfuse compatibility)
420
413
  this.rootSpan.setAttributes({
421
- "gen_ai.usage.input_tokens": totalTokens - ctx.outputTokens,
422
- "gen_ai.usage.output_tokens": ctx.outputTokens,
423
- "gen_ai.usage.total_tokens": totalTokens,
414
+ "proxy.usage.input_tokens": totalTokens - ctx.outputTokens,
415
+ "proxy.usage.output_tokens": ctx.outputTokens,
416
+ "proxy.usage.total_tokens": totalTokens,
424
417
  });
425
418
  // Prefix-inferred prices are not an observed model rate. Keep provenance,
426
419
  // but emit an amount only when this model has an exact price-table entry.
@@ -435,6 +428,7 @@ class ProxyTracer {
435
428
  });
436
429
  if (pricingStatus === "exact") {
437
430
  const cost = calculateCost(this.billingProvider, this.model, billingUsage);
431
+ this.cost = cost;
438
432
  this.rootSpan.setAttributes({
439
433
  "ai.cost.total": cost,
440
434
  "ai.cost.currency": "USD",
@@ -454,8 +448,14 @@ class ProxyTracer {
454
448
  * invoked (tool_use blocks). Uses gen_ai.* semantic-convention attributes.
455
449
  */
456
450
  setResponseInfo(ctx) {
451
+ this.responseModel = ctx.responseModel ?? this.responseModel;
452
+ this.finishReason = ctx.finishReason ?? this.finishReason;
453
+ this.firstOutputAt = ctx.firstOutputAt ?? this.firstOutputAt;
454
+ if (BODY_LOGGING_ENABLED && ctx.output !== undefined) {
455
+ this.output = ctx.output;
456
+ }
457
457
  if (ctx.responseModel) {
458
- this.rootSpan.setAttribute("gen_ai.response.model", ctx.responseModel);
458
+ this.rootSpan.setAttribute("proxy.response.model", ctx.responseModel);
459
459
  }
460
460
  if (ctx.finishReason) {
461
461
  this.rootSpan.setAttribute("gen_ai.response.finish_reason", ctx.finishReason);
@@ -475,6 +475,7 @@ class ProxyTracer {
475
475
  }
476
476
  /** Record an error on the root span. */
477
477
  setError(errorType, errorMessage) {
478
+ this.statusMessage = `${errorType}: ${errorMessage}`;
478
479
  this.rootSpan.setAttributes({
479
480
  "error.type": errorType,
480
481
  "error.message": errorMessage,
@@ -504,7 +505,7 @@ class ProxyTracer {
504
505
  "proxy.model_substituted": true,
505
506
  "proxy.original_model": requestedModel,
506
507
  "proxy.actual_model": actualModel,
507
- "gen_ai.response.model": actualModel,
508
+ "proxy.response.model": actualModel,
508
509
  ...(actualProvider ? { "proxy.actual_provider": actualProvider } : {}),
509
510
  });
510
511
  if (this.recordRequestMetrics) {
@@ -539,6 +540,7 @@ class ProxyTracer {
539
540
  });
540
541
  return;
541
542
  }
543
+ this.input = parseBody(body);
542
544
  this.rootSpan.addEvent("proxy.client.request_body", {
543
545
  ...buildBodyEventAttributes(body),
544
546
  });
@@ -583,6 +585,7 @@ class ProxyTracer {
583
585
  });
584
586
  return;
585
587
  }
588
+ this.output = parseBody(body);
586
589
  this.rootSpan.addEvent("proxy.upstream.response_body", {
587
590
  ...buildBodyEventAttributes(body),
588
591
  });
@@ -624,6 +627,7 @@ class ProxyTracer {
624
627
  // -------------------------------------------------------------------------
625
628
  /** Record an upstream retry attempt. */
626
629
  recordRetry(account, reason) {
630
+ this.statusMessage ??= `retried: ${reason}`;
627
631
  const m = getProxyMetrics();
628
632
  m.retriesTotal.add(1, {
629
633
  model: this.model,
@@ -687,6 +691,7 @@ class ProxyTracer {
687
691
  else {
688
692
  this.rootSpan.setStatus({ code: SpanStatusCode.OK });
689
693
  }
694
+ this.observeOutcome(responseStatus);
690
695
  this.rootSpan.end();
691
696
  if (!this.recordRequestMetrics && !this.recordUsageMetrics) {
692
697
  return;
@@ -759,19 +764,60 @@ class ProxyTracer {
759
764
  });
760
765
  }
761
766
  }
762
- /** Record metrics via TelemetryService (call after setUsage). */
763
- recordMetrics() {
764
- if (!this.recordUsageMetrics || !this.usage) {
767
+ levelFor(responseStatus) {
768
+ if (responseStatus === 499) {
769
+ return "WARNING";
770
+ }
771
+ if (responseStatus >= 400) {
772
+ return "ERROR";
773
+ }
774
+ if (this.statusMessage) {
775
+ return "WARNING";
776
+ }
777
+ return observationLevel(this.finishReason);
778
+ }
779
+ observeOutcome(responseStatus) {
780
+ const level = this.levelFor(responseStatus);
781
+ const statusMessage = this.statusMessage ??
782
+ (responseStatus >= 400 ? `HTTP ${responseStatus}` : undefined);
783
+ const io = {
784
+ input: this.input === undefined ? undefined : spanContentAttribute(this.input),
785
+ output: this.output === undefined
786
+ ? undefined
787
+ : spanContentAttribute(this.output),
788
+ };
789
+ observe(this.rootSpan, "span", {
790
+ ...io,
791
+ level,
792
+ statusMessage,
793
+ metadata: { account: this.accountEmail },
794
+ });
795
+ observeTrace(this.rootSpan, io);
796
+ if (this.upstreamStartedAt === undefined) {
765
797
  return;
766
798
  }
767
- const billingUsage = proxyTokenUsage(this.usage);
768
- const totalTokens = billingUsage.total;
769
- const durationMs = Date.now() - this.startTime;
770
- const cost = isExactPricingMatch(this.billingProvider, this.model) &&
771
- hasPricing(this.billingProvider, this.model)
772
- ? calculateCost(this.billingProvider, this.model, billingUsage)
773
- : undefined;
774
- TelemetryService.getInstance().recordAIRequest(this.billingProvider, this.model, totalTokens, durationMs, cost);
799
+ const generation = this.proxyTracer.startSpan(generationName(this.isStream ? "stream" : "generate"), {
800
+ startTime: this.upstreamStartedAt,
801
+ attributes: { "proxy.request_id": this.requestId },
802
+ }, this.traceContext);
803
+ observe(generation, "generation", {
804
+ ...io,
805
+ model: this.responseModel ?? this.model,
806
+ usageDetails: this.usage && usageDetails(proxyTokenUsage(this.usage)),
807
+ costDetails: this.cost === undefined ? undefined : { total: this.cost },
808
+ completionStartTime: this.firstOutputAt === undefined
809
+ ? undefined
810
+ : new Date(this.firstOutputAt),
811
+ level,
812
+ statusMessage,
813
+ metadata: {
814
+ provider: this.billingProvider,
815
+ account: this.accountEmail,
816
+ requestedModel: this.requestedModel,
817
+ finishReason: this.finishReason,
818
+ },
819
+ });
820
+ generation.end();
775
821
  }
776
822
  // -------------------------------------------------------------------------
777
823
  // Context propagation
@@ -781,7 +827,9 @@ class ProxyTracer {
781
827
  * Injects the current trace's `traceparent` / `tracestate` into a new header map.
782
828
  */
783
829
  getTraceHeaders() {
784
- return this.bridge.injectContext({}, trace.setSpan(context.active(), this.rootSpan));
830
+ const headers = {};
831
+ propagation.inject(this.traceContext, headers);
832
+ return headers;
785
833
  }
786
834
  }
787
835
  /**
@@ -947,8 +995,6 @@ export { ProxyTracer };
947
995
  /** Standard SERVER span covers every proxy door, including parsing and admission failures. */
948
996
  export function startProxyHttpTrace(metadata, headers) {
949
997
  try {
950
- // Match OtelBridge's existing compatibility policy for HTTP-combined
951
- // traceparent values: retain the first injected parent.
952
998
  const normalizedHeaders = { ...headers };
953
999
  if (normalizedHeaders.traceparent?.includes(",")) {
954
1000
  normalizedHeaders.traceparent = normalizedHeaders.traceparent
@@ -956,7 +1002,7 @@ export function startProxyHttpTrace(metadata, headers) {
956
1002
  .trim();
957
1003
  }
958
1004
  const parent = propagation.extract(ROOT_CONTEXT, normalizedHeaders);
959
- const span = getTracer("neurolink.proxy").startSpan("proxy.http.request", {
1005
+ const span = trace.getTracer("proxy").startSpan("proxy.http.request", {
960
1006
  kind: SpanKind.SERVER,
961
1007
  attributes: {
962
1008
  "http.request.method": metadata.method,
@@ -336,7 +336,6 @@ function createTranslationRequest(args) {
336
336
  inputIncludesCachedTokens: e.inputIncludesCachedTokens,
337
337
  reasoningTokens: e.reasoningTokens,
338
338
  });
339
- tracer?.recordMetrics();
340
339
  }
341
340
  const ownsClientOutcome = getProxyRequestAccounting(ctx.requestId)?.accountingScope !== "internal";
342
341
  if (errorType) {
@@ -2,7 +2,7 @@
2
2
  * Proxy Request Logger
3
3
  * Logs proxy request/response metadata to a rotating log file.
4
4
  * Also emits OTLP log records to OpenObserve (or any OTLP-compatible backend)
5
- * when a LoggerProvider is configured via OpenTelemetry instrumentation.
5
+ * when OTEL_EXPORTER_OTLP_ENDPOINT configures an OTLP LoggerProvider.
6
6
  * Useful for debugging and auditing proxy traffic.
7
7
  */
8
8
  import type { ProxyBodyCaptureEntry, RequestAttemptLogEntry, RequestLogEntry, ProxyRequestLoggerSnapshot } from "../types/index.js";
@@ -2,7 +2,7 @@
2
2
  * Proxy Request Logger
3
3
  * Logs proxy request/response metadata to a rotating log file.
4
4
  * Also emits OTLP log records to OpenObserve (or any OTLP-compatible backend)
5
- * when a LoggerProvider is configured via OpenTelemetry instrumentation.
5
+ * when OTEL_EXPORTER_OTLP_ENDPOINT configures an OTLP LoggerProvider.
6
6
  * Useful for debugging and auditing proxy traffic.
7
7
  */
8
8
  import { emitProxyOtelEvent, getProxyOtelLogSnapshot, initializeProxyOtelLogs, isProxyOtelOnly, publishProxyOtelBody, } from "./otelLogSink.js";
@@ -18,9 +18,12 @@ import { writeFile } from "fs/promises";
18
18
  import { setImmediate as yieldToRequests } from "node:timers/promises";
19
19
  import { captureProxyBody, getBodyCaptureWorkerSnapshot, PROXY_BODY_CAPTURE_DEADLINE_MS, } from "./bodyCaptureWorker.js";
20
20
  import { prepareProxyBodyForLogging as prepareRedactedBody, redactProxyHeadersForLogging as redactHeaders, splitUtf8StringByBytes, } from "./bodyCaptureProcessing.js";
21
+ import { trace } from "@opentelemetry/api";
21
22
  import { isBorrowedRequest } from "./shareContext.js";
22
- import { OtelBridge } from "../observability/otelBridge.js";
23
23
  import { createRequire } from "node:module";
24
+ import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-http";
25
+ import { resourceFromAttributes } from "@opentelemetry/resources";
26
+ import { BatchLogRecordProcessor, LoggerProvider, } from "@opentelemetry/sdk-logs";
24
27
  /**
25
28
  * Loaded via require() on first actual use rather than a top-level import,
26
29
  * so requiring this module (reachable eagerly from dist/server/index.js)
@@ -155,6 +158,7 @@ export async function flushRequestLogs(timeoutMs = REQUEST_LOG_FLUSH_TIMEOUT_MS)
155
158
  throw new Error(`Timed out flushing ${remaining} proxy request log operation(s)`);
156
159
  }
157
160
  }
161
+ await otlpLoggerProvider?.forceFlush();
158
162
  }
159
163
  /** @internal Test-only hook for exercising shutdown behavior without real I/O. */
160
164
  export const __requestLoggerTestHooks = {
@@ -175,15 +179,8 @@ export const __requestLoggerTestHooks = {
175
179
  appendMetadataFile = writeFile;
176
180
  },
177
181
  };
178
- /**
179
- * Lazily-resolved LoggerProvider from OTel instrumentation.
180
- * null = not resolved yet (will retry), LoggerProvider = resolved, false = permanently unavailable.
181
- */
182
- let otelLoggerProvider = null;
183
- /** Number of times we've tried to resolve the LoggerProvider. */
184
- let otelResolveAttempts = 0;
185
- /** Max number of resolve attempts before giving up. */
186
- const MAX_RESOLVE_ATTEMPTS = 10;
182
+ let otlpLoggerProvider;
183
+ let otlpLoggerInitialized = false;
187
184
  const BODY_OTLP_CHUNK_SIZE = 16_000;
188
185
  /**
189
186
  * Initialize private request logs and preserve required lifecycle admission on startup failures.
@@ -365,45 +362,43 @@ export async function logRequestAttempt(entry) {
365
362
  // Non-fatal — don't crash proxy for logging failures
366
363
  }
367
364
  }
368
- /**
369
- * Lazily resolve the LoggerProvider from OTel instrumentation.
370
- * Uses dynamic import to avoid hard dependency — if instrumentation.ts
371
- * hasn't been loaded or OTLP is not configured, this is a no-op.
372
- * Retries up to MAX_RESOLVE_ATTEMPTS times to handle race conditions
373
- * where OTel initialization completes after the first log request.
374
- */
375
- async function resolveLoggerProvider() {
376
- if (isProxyOtelOnly()) {
377
- return initializeProxyOtelLogs();
378
- }
379
- if (otelLoggerProvider === false) {
365
+ function createOtlpLoggerProvider() {
366
+ const endpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
367
+ if (!endpoint) {
380
368
  return undefined;
381
- } // permanently unavailable
382
- if (otelLoggerProvider !== null) {
383
- return otelLoggerProvider;
384
369
  }
385
- // Not resolved yet — try to resolve
386
- otelResolveAttempts++;
387
370
  try {
388
- const { getLoggerProvider } = await import("../services/server/ai/observability/instrumentation.js");
389
- const provider = getLoggerProvider();
390
- if (provider) {
391
- otelLoggerProvider = provider;
392
- return provider;
393
- }
394
- // Provider not available yet — if we've exceeded max attempts, give up
395
- if (otelResolveAttempts >= MAX_RESOLVE_ATTEMPTS) {
396
- otelLoggerProvider = false; // permanently unavailable
397
- }
398
- // Otherwise leave as null so we retry next time
399
- return undefined;
371
+ return new LoggerProvider({
372
+ resource: resourceFromAttributes({
373
+ "service.name": process.env.OTEL_SERVICE_NAME || "neurolink",
374
+ }),
375
+ processors: [
376
+ new BatchLogRecordProcessor(new OTLPLogExporter({ url: `${endpoint}/v1/logs` }), {
377
+ maxQueueSize: 2048,
378
+ maxExportBatchSize: 64,
379
+ scheduledDelayMillis: 2000,
380
+ exportTimeoutMillis: 30000,
381
+ }),
382
+ ],
383
+ });
400
384
  }
401
- catch {
402
- // instrumentation.ts not available (e.g. standalone mode) — disable permanently
403
- otelLoggerProvider = false;
385
+ catch (error) {
386
+ logger.warn("[RequestLogger] Failed to create OTLP log exporter", {
387
+ error: error instanceof Error ? error.message : String(error),
388
+ });
404
389
  return undefined;
405
390
  }
406
391
  }
392
+ async function resolveLoggerProvider() {
393
+ if (isProxyOtelOnly()) {
394
+ return initializeProxyOtelLogs();
395
+ }
396
+ if (!otlpLoggerInitialized) {
397
+ otlpLoggerInitialized = true;
398
+ otlpLoggerProvider = createOtlpLoggerProvider();
399
+ }
400
+ return otlpLoggerProvider;
401
+ }
407
402
  /**
408
403
  * Emit a RequestLogEntry as an OTLP log record.
409
404
  * Non-blocking, non-fatal — failures are silently swallowed.
@@ -837,8 +832,7 @@ export async function logStreamError(entry) {
837
832
  if (!logEnabled || (!logDir && !isProxyOtelOnly())) {
838
833
  return;
839
834
  }
840
- const bridge = new OtelBridge();
841
- const traceCtx = bridge.getCurrentTraceContext();
835
+ const traceCtx = trace.getActiveSpan()?.spanContext();
842
836
  const logEntry = {
843
837
  ...entry,
844
838
  responseStatus: 200,
@@ -20,7 +20,7 @@ export function createTimingMiddleware() {
20
20
  name: "timing",
21
21
  order: 0, // Run first
22
22
  handler: async (ctx, next) => {
23
- return tracers.middleware.startActiveSpan("neurolink.middleware.timing", async (otelSpan) => {
23
+ return tracers.middleware.startActiveSpan("middleware.timing", async (otelSpan) => {
24
24
  try {
25
25
  const startTime = Date.now();
26
26
  const startHrTime = process.hrtime.bigint();
@@ -74,7 +74,7 @@ export function createAgentRoutes(basePath = "/api") {
74
74
  }
75
75
  const request = validation.data;
76
76
  return withSpan({
77
- name: "neurolink.http.execute",
77
+ name: "http.execute",
78
78
  tracer: tracers.http,
79
79
  attributes: {
80
80
  "http.route": "/api/agent/execute",
@@ -165,7 +165,7 @@ export function createAgentRoutes(basePath = "/api") {
165
165
  // Wrap stream with a span that stays open for the full consumption
166
166
  // lifetime, not just the generator creation.
167
167
  async function* redactedStream() {
168
- const streamSpan = tracers.http.startSpan("neurolink.http.stream", {
168
+ const streamSpan = tracers.http.startSpan("http.stream", {
169
169
  attributes: {
170
170
  "http.route": "/api/agent/stream",
171
171
  "ai.provider": request.provider || "default",
@@ -221,7 +221,7 @@ export function createAgentRoutes(basePath = "/api") {
221
221
  try {
222
222
  const providerName = request.provider || "openai";
223
223
  return await withSpan({
224
- name: "neurolink.http.embed",
224
+ name: "http.embed",
225
225
  tracer: tracers.http,
226
226
  attributes: {
227
227
  "http.route": "/api/agent/embed",
@@ -260,7 +260,7 @@ export function createAgentRoutes(basePath = "/api") {
260
260
  try {
261
261
  const providerName = request.provider || "openai";
262
262
  return await withSpan({
263
- name: "neurolink.http.embedMany",
263
+ name: "http.embedMany",
264
264
  tracer: tracers.http,
265
265
  attributes: {
266
266
  "http.route": "/api/agent/embed-many",