@juspay/neurolink 13.5.0 → 13.6.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.
- package/CHANGELOG.md +2 -2
- package/dist/adapters/video/videoAnalyzer.d.ts +1 -0
- package/dist/adapters/video/videoAnalyzer.js +4 -0
- package/dist/autoresearch/index.d.ts +1 -0
- package/dist/autoresearch/index.js +4 -0
- package/dist/autoresearch/worker.js +4 -0
- package/dist/browser/neurolink.min.js +476 -476
- package/dist/cli/commands/observability.js +11 -4
- package/dist/cli/commands/telemetry.js +36 -6
- package/dist/cli/utils/formatters.d.ts +17 -0
- package/dist/cli/utils/formatters.js +39 -0
- package/dist/constants/langfuse.d.ts +17 -1
- package/dist/constants/langfuse.js +23 -1
- package/dist/constants/telemetry.d.ts +6 -0
- package/dist/constants/telemetry.js +6 -0
- package/dist/core/baseProvider.d.ts +3 -1
- package/dist/core/baseProvider.js +30 -8
- package/dist/core/geminiLoopAdapter.js +19 -0
- package/dist/core/loopEngine.js +10 -7
- package/dist/core/modules/StreamHandler.js +5 -4
- package/dist/core/modules/TelemetryHandler.d.ts +5 -1
- package/dist/core/modules/TelemetryHandler.js +16 -0
- package/dist/core/modules/ToolsManager.js +14 -3
- package/dist/core/nativeGenerateLoop.d.ts +2 -0
- package/dist/core/nativeGenerateLoop.js +35 -9
- package/dist/evaluation/index.d.ts +1 -0
- package/dist/evaluation/index.js +4 -0
- package/dist/evaluation/ragasEvaluator.js +1 -0
- package/dist/mcp/externalServerManager.js +6 -2
- package/dist/mcp/toolRegistry.js +3 -0
- package/dist/neurolink.d.ts +16 -8
- package/dist/neurolink.js +115 -61
- package/dist/observability/FEATURE-STATUS.md +0 -4
- package/dist/observability/exporterRegistry.d.ts +6 -1
- package/dist/observability/exporterRegistry.js +18 -0
- package/dist/observability/exporterSpanProcessor.d.ts +20 -6
- package/dist/observability/exporterSpanProcessor.js +166 -34
- package/dist/observability/exporters/baseExporter.d.ts +22 -3
- package/dist/observability/exporters/baseExporter.js +87 -5
- package/dist/observability/exporters/langfuseExporter.d.ts +29 -3
- package/dist/observability/exporters/langfuseExporter.js +251 -41
- package/dist/observability/langfuseMasking.d.ts +1 -1
- package/dist/observability/langfuseMasking.js +1 -1
- package/dist/observability/otlpEncoding.d.ts +18 -5
- package/dist/observability/otlpEncoding.js +69 -12
- package/dist/observability/utils/spanSerializer.d.ts +3 -2
- package/dist/observability/utils/spanSerializer.js +5 -3
- package/dist/processors/base/BaseFileProcessor.d.ts +1 -0
- package/dist/processors/base/BaseFileProcessor.js +4 -0
- package/dist/processors/index.d.ts +1 -0
- package/dist/processors/index.js +4 -0
- package/dist/processors/registry/ProcessorRegistry.d.ts +1 -0
- package/dist/processors/registry/ProcessorRegistry.js +5 -0
- package/dist/providers/amazonBedrock/client.js +7 -5
- package/dist/providers/amazonSagemaker.js +32 -12
- package/dist/providers/anthropic/client.js +1 -0
- package/dist/providers/anthropic/loopAdapter.js +7 -3
- package/dist/providers/googleAiStudio/client.js +2 -0
- package/dist/providers/googleVertex/client.d.ts +6 -5
- package/dist/providers/googleVertex/client.js +46 -19
- package/dist/providers/openaiChatCompletionsBase.js +29 -18
- package/dist/proxy/codexUsage.js +59 -2
- package/dist/proxy/proxyTracer.d.ts +25 -1
- package/dist/proxy/proxyTracer.js +146 -22
- package/dist/proxy/proxyTranslationEngine.js +4 -2
- package/dist/proxy/sseInterceptor.js +6 -4
- package/dist/rag/chunkers/BaseChunker.js +1 -0
- package/dist/rag/index.d.ts +1 -0
- package/dist/rag/index.js +4 -0
- package/dist/rag/ragIntegration.js +1 -0
- package/dist/rag/reranker/reranker.js +2 -0
- package/dist/rag/retrieval/vectorQueryTool.js +1 -0
- package/dist/server/routes/codexProxyRoutes.js +3 -1
- package/dist/server/utils/validation.d.ts +3 -3
- package/dist/services/server/ai/observability/instrumentation.d.ts +8 -7
- package/dist/services/server/ai/observability/instrumentation.js +217 -47
- package/dist/services/server/ai/observability/langfuseSetup.d.ts +19 -1
- package/dist/services/server/ai/observability/langfuseSetup.js +62 -4
- package/dist/services/server/ai/observability/otlpSetup.d.ts +12 -2
- package/dist/services/server/ai/observability/otlpSetup.js +55 -4
- package/dist/telemetry/aiMetrics.d.ts +56 -0
- package/dist/telemetry/aiMetrics.js +229 -0
- package/dist/telemetry/attributes.d.ts +24 -1
- package/dist/telemetry/attributes.js +94 -9
- package/dist/telemetry/chatMessages.d.ts +6 -0
- package/dist/telemetry/chatMessages.js +37 -0
- package/dist/telemetry/index.d.ts +1 -1
- package/dist/telemetry/index.js +1 -1
- package/dist/telemetry/observation.d.ts +5 -5
- package/dist/telemetry/observation.js +128 -39
- package/dist/telemetry/spanSanitizer.d.ts +2 -2
- package/dist/telemetry/spanSanitizer.js +76 -8
- package/dist/telemetry/telemetryStarter.d.ts +16 -0
- package/dist/telemetry/telemetryStarter.js +28 -0
- package/dist/telemetry/traceContext.d.ts +3 -3
- package/dist/telemetry/traceContext.js +3 -3
- package/dist/telemetry/tracers.d.ts +13 -1
- package/dist/telemetry/tracers.js +49 -10
- package/dist/telemetry/withSpan.d.ts +19 -3
- package/dist/telemetry/withSpan.js +149 -20
- package/dist/tools/toolDiscovery.js +2 -1
- package/dist/tools/toolPolicy.js +22 -2
- package/dist/types/config.d.ts +19 -0
- package/dist/types/exporter.d.ts +98 -3
- package/dist/types/generate.d.ts +7 -1
- package/dist/types/livekit.d.ts +16 -1
- package/dist/types/loopEngine.d.ts +16 -1
- package/dist/types/observability.d.ts +38 -5
- package/dist/types/proxy.d.ts +2 -0
- package/dist/types/span.d.ts +26 -0
- package/dist/types/toolResolution.d.ts +5 -1
- package/dist/utils/timeout.js +4 -2
- package/dist/voice/livekit/realtimeMcpTools.js +11 -2
- package/dist/voice/livekit/realtimeMemory.d.ts +20 -2
- package/dist/voice/livekit/realtimeMemory.js +139 -46
- package/dist/voice/livekit/realtimeVoiceAgent.js +20 -8
- package/dist/voice/livekit/turnRecorder.js +22 -9
- package/dist/workflow/core/ensembleExecutor.js +2 -0
- package/dist/workflow/core/judgeScorer.js +1 -0
- package/dist/workflow/core/responseConditioner.js +1 -0
- package/dist/workflow/core/workflowRunner.js +6 -1
- package/dist/workflow/index.d.ts +1 -0
- package/dist/workflow/index.js +4 -0
- package/docs-site/static/search-index.json +32 -27
- package/package.json +21 -2
|
@@ -12,10 +12,12 @@
|
|
|
12
12
|
* - calculateCost() from pricing.ts for cost tracking
|
|
13
13
|
*/
|
|
14
14
|
import { SpanStatusCode, SpanKind, ROOT_CONTEXT, isSpanContextValid, propagation, context, metrics, trace, } from "@opentelemetry/api";
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
15
|
+
import { PROXY_TRACER_SCOPE } from "../constants/telemetry.js";
|
|
16
|
+
import { ensureTelemetry, setLangfuseContext, } from "../services/server/ai/observability/instrumentation.js";
|
|
17
|
+
import { spanContentAttribute, withoutSpanContent, } from "../telemetry/attributes.js";
|
|
17
18
|
import { observationLevel, observe, observeTrace, usageDetails, } from "../telemetry/observation.js";
|
|
18
19
|
import { generationName } from "../telemetry/observationNames.js";
|
|
20
|
+
import { recordAIRequest } from "../telemetry/aiMetrics.js";
|
|
19
21
|
import { proxyTokenUsage } from "./proxyTokenUsage.js";
|
|
20
22
|
import { calculateCost, hasPricing, isExactPricingMatch, } from "../utils/pricing.js";
|
|
21
23
|
import { logger } from "../utils/logger.js";
|
|
@@ -23,11 +25,12 @@ import { registerProxyRequestTraceContext, releaseProxyRequestTraceContext, } fr
|
|
|
23
25
|
// ---------------------------------------------------------------------------
|
|
24
26
|
// OTEL Metric Instruments — lazy singleton
|
|
25
27
|
//
|
|
26
|
-
// The MeterProvider is
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
28
|
+
// The MeterProvider is created by ensureTelemetry() (instrumentation.ts) when
|
|
29
|
+
// OTEL_EXPORTER_OTLP_ENDPOINT is set; `neurolink proxy start` calls it at
|
|
30
|
+
// startup, *after* module import time. @opentelemetry/api v1.x getMeter()
|
|
31
|
+
// returns a NoopMeter if called before a real MeterProvider is set. We
|
|
32
|
+
// therefore defer instrument creation until the first ProxyTracer.end()
|
|
33
|
+
// call, at which point the MeterProvider is registered.
|
|
31
34
|
// ---------------------------------------------------------------------------
|
|
32
35
|
let _metrics = null;
|
|
33
36
|
function getProxyMetrics() {
|
|
@@ -141,7 +144,17 @@ function redactHeaders(headers) {
|
|
|
141
144
|
}
|
|
142
145
|
/** Redact sensitive JSON fields in request/response bodies before logging. */
|
|
143
146
|
const SENSITIVE_BODY_KEYS = /api[_-]?key|token|secret|password|credential|authorization/i;
|
|
144
|
-
const
|
|
147
|
+
const envFlag = (name) => process.env[name]?.trim().toLowerCase();
|
|
148
|
+
/**
|
|
149
|
+
* Request and response bodies, both as observation input and output and as raw
|
|
150
|
+
* span events, are recorded only on an explicit
|
|
151
|
+
* `NEUROLINK_PROXY_TRACE_BODY_LOGGING=true`, read per call. Bodies are the
|
|
152
|
+
* most sensitive thing a proxy sees, so tracing the request alone never turns
|
|
153
|
+
* them on.
|
|
154
|
+
*/
|
|
155
|
+
const bodyLoggingEnabled = () => process.env.NEUROLINK_PROXY_TRACE_BODY_LOGGING === "true";
|
|
156
|
+
/** `NEUROLINK_RECORD_INPUTS=false` keeps request content out of every trace. */
|
|
157
|
+
const recordInputsEnabled = () => envFlag("NEUROLINK_RECORD_INPUTS") !== "false";
|
|
145
158
|
const MAX_BODY_LOG_SIZE = Number.parseInt(process.env.NEUROLINK_PROXY_TRACE_BODY_LOG_BYTES ?? "8192", 10);
|
|
146
159
|
const MAX_STREAM_EVENTS_TO_LOG = 200;
|
|
147
160
|
function redactBodyForLogging(body, maxLen = 8192) {
|
|
@@ -205,7 +218,7 @@ function detectClientApp(userAgent) {
|
|
|
205
218
|
class ProxyTracer {
|
|
206
219
|
rootSpan;
|
|
207
220
|
traceContext;
|
|
208
|
-
proxyTracer = trace.getTracer(
|
|
221
|
+
proxyTracer = trace.getTracer(PROXY_TRACER_SCOPE);
|
|
209
222
|
requestId;
|
|
210
223
|
/**
|
|
211
224
|
* Model used for costing. Starts as the model the client asked for and is
|
|
@@ -232,6 +245,12 @@ class ProxyTracer {
|
|
|
232
245
|
requestedModel;
|
|
233
246
|
input;
|
|
234
247
|
output;
|
|
248
|
+
/** The parsed response outranks the raw upstream body that arrives after it. */
|
|
249
|
+
outputIsParsed = false;
|
|
250
|
+
/** A model call made through `runModelCall` records the generation. */
|
|
251
|
+
modelCallDelegated = false;
|
|
252
|
+
/** That call's records carry no content; fixed when the call starts. */
|
|
253
|
+
modelContentWithheld = false;
|
|
235
254
|
responseModel;
|
|
236
255
|
finishReason;
|
|
237
256
|
firstOutputAt;
|
|
@@ -255,7 +274,11 @@ class ProxyTracer {
|
|
|
255
274
|
* will be linked to the caller's trace via the OTel propagation API.
|
|
256
275
|
*/
|
|
257
276
|
static startRequest(ctx, incomingHeaders) {
|
|
258
|
-
|
|
277
|
+
// The proxy tracer is not a scrubbed tracer, so nothing else starts
|
|
278
|
+
// telemetry before its first root span; without this an embedder that only
|
|
279
|
+
// set the Langfuse keys loses that first request.
|
|
280
|
+
ensureTelemetry();
|
|
281
|
+
const tracer = trace.getTracer(PROXY_TRACER_SCOPE);
|
|
259
282
|
// Extract parent context from incoming headers (Claude Code may send traceparent)
|
|
260
283
|
let parentContext = context.active();
|
|
261
284
|
if (incomingHeaders && !trace.getSpan(context.active())) {
|
|
@@ -270,9 +293,10 @@ class ProxyTracer {
|
|
|
270
293
|
}
|
|
271
294
|
}
|
|
272
295
|
const clientApp = ctx.clientApp ?? detectClientApp(ctx.userAgent);
|
|
296
|
+
const sessionId = incomingHeaders?.["x-neurolink-session-id"] ?? ctx.sessionId;
|
|
273
297
|
const traceContext = context.with(parentContext, () => setLangfuseContext({
|
|
274
298
|
userId: incomingHeaders?.["x-neurolink-user-id"],
|
|
275
|
-
sessionId
|
|
299
|
+
sessionId,
|
|
276
300
|
conversationId: incomingHeaders?.["x-neurolink-conversation-id"],
|
|
277
301
|
requestId: ctx.requestId,
|
|
278
302
|
traceName: `proxy:${ctx.model}`,
|
|
@@ -282,20 +306,41 @@ class ProxyTracer {
|
|
|
282
306
|
toolCount: ctx.toolCount,
|
|
283
307
|
},
|
|
284
308
|
}, () => context.active()));
|
|
309
|
+
// Model and usage are written under the gen_ai.* names collectors read
|
|
310
|
+
// before as well as under proxy.*; Langfuse's export leaves the gen_ai.*
|
|
311
|
+
// ones off this plain span, so the generation stays the priced record.
|
|
285
312
|
const rootSpan = tracer.startSpan("proxy.request", {
|
|
286
313
|
attributes: {
|
|
287
314
|
"proxy.request_id": ctx.requestId,
|
|
288
315
|
"http.method": ctx.method,
|
|
289
316
|
"http.target": ctx.path,
|
|
290
317
|
"proxy.request.model": ctx.model,
|
|
318
|
+
"gen_ai.request.model": ctx.model,
|
|
291
319
|
"proxy.stream": ctx.stream,
|
|
292
320
|
"proxy.tool_count": ctx.toolCount,
|
|
293
321
|
"proxy.client_app": clientApp,
|
|
294
322
|
},
|
|
295
323
|
}, traceContext);
|
|
324
|
+
// Marked at the start so a model call that ends before this span (the
|
|
325
|
+
// translated path) is attached to it rather than to the span above it.
|
|
326
|
+
observe(rootSpan, "span", {});
|
|
296
327
|
if (ctx.userAgent) {
|
|
297
328
|
rootSpan.setAttribute("http.user_agent", ctx.userAgent);
|
|
298
329
|
}
|
|
330
|
+
// Plain attributes for backends that index identity by key; Langfuse reads
|
|
331
|
+
// the propagated user and session instead.
|
|
332
|
+
for (const [key, value] of [
|
|
333
|
+
["neurolink.user_id", incomingHeaders?.["x-neurolink-user-id"]],
|
|
334
|
+
["neurolink.session_id", sessionId],
|
|
335
|
+
[
|
|
336
|
+
"neurolink.conversation_id",
|
|
337
|
+
incomingHeaders?.["x-neurolink-conversation-id"],
|
|
338
|
+
],
|
|
339
|
+
]) {
|
|
340
|
+
if (value) {
|
|
341
|
+
rootSpan.setAttribute(key, value);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
299
344
|
if (ctx.toolNames && ctx.toolNames.length > 0) {
|
|
300
345
|
// What the caller exposed to the model (tool catalogue for this request).
|
|
301
346
|
rootSpan.setAttribute("gen_ai.request.tool_names", JSON.stringify(ctx.toolNames));
|
|
@@ -345,9 +390,39 @@ class ProxyTracer {
|
|
|
345
390
|
attributes: {
|
|
346
391
|
"proxy.request_id": this.requestId,
|
|
347
392
|
"proxy.request.model": this.model,
|
|
393
|
+
"gen_ai.request.model": this.model,
|
|
348
394
|
},
|
|
349
395
|
}, this.traceContext);
|
|
350
396
|
}
|
|
397
|
+
/**
|
|
398
|
+
* Run a model call inside this request's trace context, so the spans it
|
|
399
|
+
* starts nest under the proxy root and carry the request's user and session.
|
|
400
|
+
* That call's generation holds usage and cost, so the root records none of
|
|
401
|
+
* its own. Its prompt and answer are request and response bodies, so its
|
|
402
|
+
* records carry them only with `NEUROLINK_PROXY_TRACE_BODY_LOGGING=true`.
|
|
403
|
+
*/
|
|
404
|
+
runModelCall(fn) {
|
|
405
|
+
this.modelCallDelegated = true;
|
|
406
|
+
this.modelContentWithheld = !bodyLoggingEnabled();
|
|
407
|
+
return context.with(this.traceContext, () => this.modelContentWithheld ? withoutSpanContent(fn) : fn());
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* The delegated call's stream, read and closed under the same content rule:
|
|
411
|
+
* the call records its answer while the stream is consumed, which happens
|
|
412
|
+
* after `runModelCall` has returned.
|
|
413
|
+
*/
|
|
414
|
+
modelStream(iterator) {
|
|
415
|
+
if (!this.modelContentWithheld) {
|
|
416
|
+
return iterator;
|
|
417
|
+
}
|
|
418
|
+
const close = iterator.return?.bind(iterator);
|
|
419
|
+
return {
|
|
420
|
+
next: (...value) => withoutSpanContent(() => iterator.next(...value)),
|
|
421
|
+
...(close && {
|
|
422
|
+
return: (value) => withoutSpanContent(() => close(value)),
|
|
423
|
+
}),
|
|
424
|
+
};
|
|
425
|
+
}
|
|
351
426
|
// -------------------------------------------------------------------------
|
|
352
427
|
// Attribute setters
|
|
353
428
|
// -------------------------------------------------------------------------
|
|
@@ -414,6 +489,9 @@ class ProxyTracer {
|
|
|
414
489
|
"proxy.usage.input_tokens": totalTokens - ctx.outputTokens,
|
|
415
490
|
"proxy.usage.output_tokens": ctx.outputTokens,
|
|
416
491
|
"proxy.usage.total_tokens": totalTokens,
|
|
492
|
+
"gen_ai.usage.input_tokens": totalTokens - ctx.outputTokens,
|
|
493
|
+
"gen_ai.usage.output_tokens": ctx.outputTokens,
|
|
494
|
+
"gen_ai.usage.total_tokens": totalTokens,
|
|
417
495
|
});
|
|
418
496
|
// Prefix-inferred prices are not an observed model rate. Keep provenance,
|
|
419
497
|
// but emit an amount only when this model has an exact price-table entry.
|
|
@@ -451,11 +529,17 @@ class ProxyTracer {
|
|
|
451
529
|
this.responseModel = ctx.responseModel ?? this.responseModel;
|
|
452
530
|
this.finishReason = ctx.finishReason ?? this.finishReason;
|
|
453
531
|
this.firstOutputAt = ctx.firstOutputAt ?? this.firstOutputAt;
|
|
454
|
-
|
|
532
|
+
// A reply parsed to nothing (no content block, or an error before any)
|
|
533
|
+
// is still parsed: the raw upstream text must not stand in for it.
|
|
534
|
+
if (ctx.output !== undefined && this.recordsBodies()) {
|
|
455
535
|
this.output = ctx.output;
|
|
536
|
+
this.outputIsParsed = true;
|
|
456
537
|
}
|
|
457
538
|
if (ctx.responseModel) {
|
|
458
|
-
this.rootSpan.
|
|
539
|
+
this.rootSpan.setAttributes({
|
|
540
|
+
"proxy.response.model": ctx.responseModel,
|
|
541
|
+
"gen_ai.response.model": ctx.responseModel,
|
|
542
|
+
});
|
|
459
543
|
}
|
|
460
544
|
if (ctx.finishReason) {
|
|
461
545
|
this.rootSpan.setAttribute("gen_ai.response.finish_reason", ctx.finishReason);
|
|
@@ -506,6 +590,7 @@ class ProxyTracer {
|
|
|
506
590
|
"proxy.original_model": requestedModel,
|
|
507
591
|
"proxy.actual_model": actualModel,
|
|
508
592
|
"proxy.response.model": actualModel,
|
|
593
|
+
"gen_ai.response.model": actualModel,
|
|
509
594
|
...(actualProvider ? { "proxy.actual_provider": actualProvider } : {}),
|
|
510
595
|
});
|
|
511
596
|
if (this.recordRequestMetrics) {
|
|
@@ -531,16 +616,24 @@ class ProxyTracer {
|
|
|
531
616
|
// -------------------------------------------------------------------------
|
|
532
617
|
// Log payloads as span events
|
|
533
618
|
// -------------------------------------------------------------------------
|
|
619
|
+
/** Record the client request body as the input of the root and its generation. */
|
|
620
|
+
setRequestInput(body) {
|
|
621
|
+
if (this.recordsBodies() && recordInputsEnabled()) {
|
|
622
|
+
this.input = body;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
534
625
|
/** Log the incoming client request body (redacted). */
|
|
535
626
|
logRequestBody(body) {
|
|
536
|
-
if (
|
|
627
|
+
if (this.recordsBodies() && recordInputsEnabled()) {
|
|
628
|
+
this.input = parseBody(body);
|
|
629
|
+
}
|
|
630
|
+
if (!bodyLoggingEnabled() || !recordInputsEnabled()) {
|
|
537
631
|
this.rootSpan.addEvent("proxy.client.request_body", {
|
|
538
632
|
"proxy.body.size": body.length,
|
|
539
633
|
"proxy.body.logged": false,
|
|
540
634
|
});
|
|
541
635
|
return;
|
|
542
636
|
}
|
|
543
|
-
this.input = parseBody(body);
|
|
544
637
|
this.rootSpan.addEvent("proxy.client.request_body", {
|
|
545
638
|
...buildBodyEventAttributes(body),
|
|
546
639
|
});
|
|
@@ -553,7 +646,7 @@ class ProxyTracer {
|
|
|
553
646
|
}
|
|
554
647
|
/** Log the upstream request body (redacted, as sent to Anthropic). */
|
|
555
648
|
logUpstreamRequestBody(body) {
|
|
556
|
-
if (!
|
|
649
|
+
if (!bodyLoggingEnabled() || !recordInputsEnabled()) {
|
|
557
650
|
this.rootSpan.addEvent("proxy.upstream.request_body", {
|
|
558
651
|
"proxy.body.size": body.length,
|
|
559
652
|
"proxy.body.logged": false,
|
|
@@ -578,21 +671,23 @@ class ProxyTracer {
|
|
|
578
671
|
}
|
|
579
672
|
/** Log the upstream response body (redacted). */
|
|
580
673
|
logUpstreamResponseBody(body) {
|
|
581
|
-
if (!
|
|
674
|
+
if (this.recordsBodies() && !this.outputIsParsed) {
|
|
675
|
+
this.output = parseBody(body);
|
|
676
|
+
}
|
|
677
|
+
if (!bodyLoggingEnabled()) {
|
|
582
678
|
this.rootSpan.addEvent("proxy.upstream.response_body", {
|
|
583
679
|
"proxy.body.size": body.length,
|
|
584
680
|
"proxy.body.logged": false,
|
|
585
681
|
});
|
|
586
682
|
return;
|
|
587
683
|
}
|
|
588
|
-
this.output = parseBody(body);
|
|
589
684
|
this.rootSpan.addEvent("proxy.upstream.response_body", {
|
|
590
685
|
...buildBodyEventAttributes(body),
|
|
591
686
|
});
|
|
592
687
|
}
|
|
593
688
|
/** Log SSE stream events (each event has type, timestamp, data). */
|
|
594
689
|
logStreamEvents(events) {
|
|
595
|
-
if (!
|
|
690
|
+
if (!bodyLoggingEnabled()) {
|
|
596
691
|
this.rootSpan.addEvent("proxy.stream.events", {
|
|
597
692
|
"proxy.stream.event_count": events.length,
|
|
598
693
|
"proxy.body.logged": false,
|
|
@@ -746,6 +841,7 @@ class ProxyTracer {
|
|
|
746
841
|
if (cost !== undefined) {
|
|
747
842
|
m.costTotal.add(cost, tokenLabels);
|
|
748
843
|
}
|
|
844
|
+
recordAIRequest(this.billingProvider, this.model, billingUsage.total, durationMs, cost);
|
|
749
845
|
}
|
|
750
846
|
// Error metrics
|
|
751
847
|
if (this.recordRequestMetrics && responseStatus >= 400) {
|
|
@@ -764,6 +860,10 @@ class ProxyTracer {
|
|
|
764
860
|
});
|
|
765
861
|
}
|
|
766
862
|
}
|
|
863
|
+
/** Bodies are only worth keeping while something is recording the span. */
|
|
864
|
+
recordsBodies() {
|
|
865
|
+
return bodyLoggingEnabled() && this.rootSpan.isRecording();
|
|
866
|
+
}
|
|
767
867
|
levelFor(responseStatus) {
|
|
768
868
|
if (responseStatus === 499) {
|
|
769
869
|
return "WARNING";
|
|
@@ -793,7 +893,7 @@ class ProxyTracer {
|
|
|
793
893
|
metadata: { account: this.accountEmail },
|
|
794
894
|
});
|
|
795
895
|
observeTrace(this.rootSpan, io);
|
|
796
|
-
if (this.upstreamStartedAt === undefined) {
|
|
896
|
+
if (this.upstreamStartedAt === undefined || this.modelCallDelegated) {
|
|
797
897
|
return;
|
|
798
898
|
}
|
|
799
899
|
const generation = this.proxyTracer.startSpan(generationName(this.isStream ? "stream" : "generate"), {
|
|
@@ -913,6 +1013,29 @@ function trackFallbackLegHealth(legKey, status, errorMessage) {
|
|
|
913
1013
|
(errorMessage ? `; last error: ${errorMessage.slice(0, 200)}` : ""));
|
|
914
1014
|
reportedDeadFallbackLegs.add(legKey);
|
|
915
1015
|
}
|
|
1016
|
+
/** Leg failure classes the fallback paths put in brackets before a message. */
|
|
1017
|
+
const FALLBACK_ERROR_CLASSES = new Set([
|
|
1018
|
+
"unknown",
|
|
1019
|
+
"unavailable",
|
|
1020
|
+
"rate_limit",
|
|
1021
|
+
"context_overflow",
|
|
1022
|
+
"empty_response",
|
|
1023
|
+
"schema_mismatch",
|
|
1024
|
+
"provider_quota",
|
|
1025
|
+
]);
|
|
1026
|
+
/**
|
|
1027
|
+
* A metric label from a fixed set: an error message is unbounded cardinality
|
|
1028
|
+
* and can echo the prompt or a credential.
|
|
1029
|
+
*/
|
|
1030
|
+
function fallbackErrorLabel(errorMessage) {
|
|
1031
|
+
if (errorMessage === undefined) {
|
|
1032
|
+
return "unknown";
|
|
1033
|
+
}
|
|
1034
|
+
const named = /^\[([a-z_]+)\]/.exec(errorMessage)?.[1];
|
|
1035
|
+
return named !== undefined && FALLBACK_ERROR_CLASSES.has(named)
|
|
1036
|
+
? named
|
|
1037
|
+
: "other";
|
|
1038
|
+
}
|
|
916
1039
|
export function recordFallbackAttempt(attrs) {
|
|
917
1040
|
// Leg health is tracked outside the metrics try/catch: a telemetry failure
|
|
918
1041
|
// must not be the reason an operator never hears that a leg is dead.
|
|
@@ -934,7 +1057,7 @@ export function recordFallbackAttempt(attrs) {
|
|
|
934
1057
|
else {
|
|
935
1058
|
m.fallbackFailureTotal.add(1, {
|
|
936
1059
|
...labels,
|
|
937
|
-
error: attrs.errorMessage
|
|
1060
|
+
error: fallbackErrorLabel(attrs.errorMessage),
|
|
938
1061
|
});
|
|
939
1062
|
}
|
|
940
1063
|
}
|
|
@@ -995,6 +1118,7 @@ export { ProxyTracer };
|
|
|
995
1118
|
/** Standard SERVER span covers every proxy door, including parsing and admission failures. */
|
|
996
1119
|
export function startProxyHttpTrace(metadata, headers) {
|
|
997
1120
|
try {
|
|
1121
|
+
ensureTelemetry();
|
|
998
1122
|
const normalizedHeaders = { ...headers };
|
|
999
1123
|
if (normalizedHeaders.traceparent?.includes(",")) {
|
|
1000
1124
|
normalizedHeaders.traceparent = normalizedHeaders.traceparent
|
|
@@ -1002,7 +1126,7 @@ export function startProxyHttpTrace(metadata, headers) {
|
|
|
1002
1126
|
.trim();
|
|
1003
1127
|
}
|
|
1004
1128
|
const parent = propagation.extract(ROOT_CONTEXT, normalizedHeaders);
|
|
1005
|
-
const span = trace.getTracer(
|
|
1129
|
+
const span = trace.getTracer(PROXY_TRACER_SCOPE).startSpan("proxy.http.request", {
|
|
1006
1130
|
kind: SpanKind.SERVER,
|
|
1007
1131
|
attributes: {
|
|
1008
1132
|
"http.request.method": metadata.method,
|
|
@@ -582,9 +582,11 @@ async function* runTranslationAttempts(args) {
|
|
|
582
582
|
},
|
|
583
583
|
});
|
|
584
584
|
recordAttempt(attempt.label ?? "translation", "translation");
|
|
585
|
-
const
|
|
585
|
+
const modelCall = () => ctx.neurolink.stream(request.withVettedImages(prepared.body));
|
|
586
|
+
const result = await raceWithAbort(args.tracer ? args.tracer.runModelCall(modelCall) : modelCall(), signal);
|
|
586
587
|
request.setResult(result);
|
|
587
|
-
const
|
|
588
|
+
const source = result.stream[Symbol.asyncIterator]();
|
|
589
|
+
const iterator = args.tracer ? args.tracer.modelStream(source) : source;
|
|
588
590
|
request.setIterator(iterator);
|
|
589
591
|
let hasText = false;
|
|
590
592
|
while (true) {
|
|
@@ -293,10 +293,12 @@ function processMessageDelta(acc, parsed) {
|
|
|
293
293
|
}
|
|
294
294
|
const usage = parsed.usage;
|
|
295
295
|
if (usage) {
|
|
296
|
-
// message_delta
|
|
297
|
-
//
|
|
298
|
-
//
|
|
299
|
-
|
|
296
|
+
// message_delta reports the cumulative output_tokens for the whole
|
|
297
|
+
// message, including the token message_start already counted (it reports
|
|
298
|
+
// 1, not 0), so it replaces that count rather than adding to it.
|
|
299
|
+
if (typeof usage.output_tokens === "number") {
|
|
300
|
+
acc.outputTokens = usage.output_tokens;
|
|
301
|
+
}
|
|
300
302
|
}
|
|
301
303
|
}
|
|
302
304
|
/* eslint-enable @typescript-eslint/no-explicit-any */
|
package/dist/rag/index.d.ts
CHANGED
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
* const chunks = await chunker.chunk(text);
|
|
44
44
|
* ```
|
|
45
45
|
*/
|
|
46
|
+
import "../services/server/ai/observability/instrumentation.js";
|
|
46
47
|
export { ChunkerFactory, chunkerFactory, createChunker, getAvailableStrategies as getFactoryStrategies, getDefaultConfig as getFactoryDefaultConfig, } from "./ChunkerFactory.js";
|
|
47
48
|
export { ChunkerRegistry as ChunkerRegistryV2, chunkerRegistry, getAvailableChunkers, getChunker, getChunkerMetadata, } from "./ChunkerRegistry.js";
|
|
48
49
|
export * from "./chunkers/index.js";
|
package/dist/rag/index.js
CHANGED
|
@@ -43,6 +43,10 @@
|
|
|
43
43
|
* const chunks = await chunker.chunk(text);
|
|
44
44
|
* ```
|
|
45
45
|
*/
|
|
46
|
+
// Imported on its own, this entry starts tracing at its first span, as the
|
|
47
|
+
// main entry does. The import sits here rather than in the shared tracers so
|
|
48
|
+
// that the client entry stays free of the OpenTelemetry Node SDK.
|
|
49
|
+
import "../services/server/ai/observability/instrumentation.js";
|
|
46
50
|
export { ChunkerFactory, chunkerFactory, createChunker, getAvailableStrategies as getFactoryStrategies, getDefaultConfig as getFactoryDefaultConfig, } from "./ChunkerFactory.js";
|
|
47
51
|
// ChunkerFactory and ChunkerRegistry patterns (from main worktree)
|
|
48
52
|
export { ChunkerRegistry as ChunkerRegistryV2, chunkerRegistry, getAvailableChunkers, getChunker, getChunkerMetadata, } from "./ChunkerRegistry.js";
|
|
@@ -478,6 +478,7 @@ async function _prepareRAGToolInner(ragConfig, fallbackProvider) {
|
|
|
478
478
|
return withSpan({
|
|
479
479
|
name: "neurolink.rag.search",
|
|
480
480
|
tracer: tracers.rag,
|
|
481
|
+
observationType: "span",
|
|
481
482
|
attributes: {
|
|
482
483
|
"rag.query_length": query ? String(query).length : 0,
|
|
483
484
|
"rag.top_k": topK ?? 5,
|
|
@@ -33,6 +33,7 @@ export async function rerank(results, query, model, options) {
|
|
|
33
33
|
return withSpan({
|
|
34
34
|
name: "neurolink.rag.rerank",
|
|
35
35
|
tracer: tracers.rag,
|
|
36
|
+
observationType: "span",
|
|
36
37
|
attributes: {
|
|
37
38
|
"rag.reranker.input_count": results.length,
|
|
38
39
|
"rag.reranker.top_k": options?.topK ?? 3,
|
|
@@ -153,6 +154,7 @@ export async function batchRerank(results, query, model, options) {
|
|
|
153
154
|
return withSpan({
|
|
154
155
|
name: "neurolink.rag.batchRerank",
|
|
155
156
|
tracer: tracers.rag,
|
|
157
|
+
observationType: "span",
|
|
156
158
|
attributes: {
|
|
157
159
|
"rag.reranker.input_count": results.length,
|
|
158
160
|
"rag.reranker.top_k": options?.topK ?? 3,
|
|
@@ -51,6 +51,7 @@ export function createVectorQueryTool(config, vectorStore) {
|
|
|
51
51
|
return withSpan({
|
|
52
52
|
name: "neurolink.rag.vectorQuery",
|
|
53
53
|
tracer: tracers.rag,
|
|
54
|
+
observationType: "span",
|
|
54
55
|
attributes: {
|
|
55
56
|
"rag.vector.index": indexName,
|
|
56
57
|
"rag.vector.top_k": params.topK ?? topK,
|
|
@@ -1026,7 +1026,7 @@ function captureCodexResponse(ctx, response, phase, account, attempt) {
|
|
|
1026
1026
|
}
|
|
1027
1027
|
function startCodexTracer(ctx, model, body, isFallbackRequest) {
|
|
1028
1028
|
try {
|
|
1029
|
-
|
|
1029
|
+
const tracer = ProxyTracer.startRequest({
|
|
1030
1030
|
requestId: ctx.requestId,
|
|
1031
1031
|
method: ctx.method,
|
|
1032
1032
|
path: ctx.path,
|
|
@@ -1038,6 +1038,8 @@ function startCodexTracer(ctx, model, body, isFallbackRequest) {
|
|
|
1038
1038
|
recordRequestMetrics: !isFallbackRequest,
|
|
1039
1039
|
recordUsageMetrics: true,
|
|
1040
1040
|
}, ctx.headers);
|
|
1041
|
+
tracer.setRequestInput(body);
|
|
1042
|
+
return tracer;
|
|
1041
1043
|
}
|
|
1042
1044
|
catch {
|
|
1043
1045
|
return undefined;
|
|
@@ -192,8 +192,8 @@ export declare const TranscribeConfigSchema: z.ZodObject<{
|
|
|
192
192
|
correction: z.ZodOptional<z.ZodObject<{
|
|
193
193
|
enabled: z.ZodOptional<z.ZodBoolean>;
|
|
194
194
|
guard: z.ZodOptional<z.ZodEnum<{
|
|
195
|
-
decide: "decide";
|
|
196
195
|
none: "none";
|
|
196
|
+
decide: "decide";
|
|
197
197
|
}>>;
|
|
198
198
|
decideTimeoutMs: z.ZodOptional<z.ZodNumber>;
|
|
199
199
|
rewrite: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<false>, z.ZodObject<{
|
|
@@ -242,8 +242,8 @@ export declare const TranscribeRequestSchema: z.ZodObject<{
|
|
|
242
242
|
correction: z.ZodOptional<z.ZodObject<{
|
|
243
243
|
enabled: z.ZodOptional<z.ZodBoolean>;
|
|
244
244
|
guard: z.ZodOptional<z.ZodEnum<{
|
|
245
|
-
decide: "decide";
|
|
246
245
|
none: "none";
|
|
246
|
+
decide: "decide";
|
|
247
247
|
}>>;
|
|
248
248
|
decideTimeoutMs: z.ZodOptional<z.ZodNumber>;
|
|
249
249
|
rewrite: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<false>, z.ZodObject<{
|
|
@@ -308,8 +308,8 @@ export declare const TranscribeStreamConfigSchema: z.ZodObject<{
|
|
|
308
308
|
correction: z.ZodOptional<z.ZodObject<{
|
|
309
309
|
enabled: z.ZodOptional<z.ZodBoolean>;
|
|
310
310
|
guard: z.ZodOptional<z.ZodEnum<{
|
|
311
|
-
decide: "decide";
|
|
312
311
|
none: "none";
|
|
312
|
+
decide: "decide";
|
|
313
313
|
}>>;
|
|
314
314
|
decideTimeoutMs: z.ZodOptional<z.ZodNumber>;
|
|
315
315
|
rewrite: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<false>, z.ZodObject<{
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type Context } from "@opentelemetry/api";
|
|
2
2
|
import type { NeurolinkTraceContext, ObservabilityConfig, SpanContent } from "../../../../types/index.js";
|
|
3
3
|
/**
|
|
4
|
-
* @description Starts NeuroLink's tracing once per process, synchronously. Every span created through the shared tracers calls it first, so direct provider calls without a NeuroLink instance are traced too. Langfuse is enabled unless `config.langfuse` is `false`, when an options object
|
|
4
|
+
* @description Starts NeuroLink's tracing once per process, synchronously. Every span created through the shared tracers calls it first once this module is loaded, as the main, server and livekit entries do, so direct provider calls without a NeuroLink instance are traced too. Langfuse is enabled unless `config.langfuse` is `false` or has `enabled: false`, when `config.langfuse` is `true` or an options object, or when `LANGFUSE_PUBLIC_KEY` and `LANGFUSE_SECRET_KEY` are set and `LANGFUSE_ENABLED` is not `false`, `0`, `off` or `no`; an OTLP exporter is added when `OTEL_EXPORTER_OTLP_ENDPOINT` is set. When the host already registered a TracerProvider no provider is created and `OTEL_EXPORTER_OTLP_ENDPOINT` is ignored with a warning; the Langfuse exporter, if configured, is registered on the shared exporter registry and the host adds `createExporterSpanProcessor()` to its provider. The first call that finds tracing configured decides the setup and a later `config` is ignored, with a warning naming the options that differ from the first when it does differ; calls made before anything is configured start nothing, so a later config or environment can still start tracing, unless `langfuse: false` was passed. Langfuse enabled without both keys (in config or the environment) exports nothing to Langfuse, and a warning says so once. With `langfuse: true` or the environment alone that counts as not configured, so only an OTLP endpoint starts tracing; an options object starts tracing anyway, as it always did, so exporters registered on the shared registry receive the spans. Pending spans and metrics are flushed when the process runs out of work (`beforeExit`), within the same budget as `flushOpenTelemetry`, which an explicit `process.exit()` skips; `NEUROLINK_TELEMETRY_EXIT_FLUSH` set to `false`, `0`, `off` or `no` skips that flush too, and the process ends as soon as it has no work. After `shutdownOpenTelemetry` it does nothing.
|
|
5
5
|
* @param config {ObservabilityConfig | undefined} - Observability options, honoured only when this is the first call.
|
|
6
6
|
* @returns {void} Nothing; a failed setup is logged and recorded in `getLangfuseHealthStatus`.
|
|
7
7
|
*/
|
|
@@ -13,20 +13,21 @@ export declare function ensureTelemetry(config?: ObservabilityConfig): void;
|
|
|
13
13
|
*/
|
|
14
14
|
export declare function acquireTelemetry(config?: ObservabilityConfig): void;
|
|
15
15
|
/**
|
|
16
|
-
* @description Releases one holder registered by `acquireTelemetry` and flushes pending spans. The provider stays registered for the process so cached tracers keep working and later instances reuse it.
|
|
16
|
+
* @description Releases one holder registered by `acquireTelemetry` and flushes pending spans as `flushOpenTelemetry` does. The provider stays registered for the process so cached tracers keep working and later instances reuse it.
|
|
17
17
|
* @returns {Promise<void>} Resolves when pending spans are flushed.
|
|
18
|
-
* @throws {Error} When any provider fails to flush.
|
|
18
|
+
* @throws {Error} When any provider fails to flush or does not finish within the flush budget.
|
|
19
19
|
*/
|
|
20
20
|
export declare function releaseTelemetry(): Promise<void>;
|
|
21
21
|
/**
|
|
22
|
-
* @description Exports pending spans and metrics from the providers NeuroLink owns and flushes its Langfuse exporter. A host provider is never flushed.
|
|
22
|
+
* @description Exports pending spans and metrics from the providers NeuroLink owns and flushes its Langfuse exporter. A host provider is never flushed. The OTLP span and metric exports are waited for at most the flush budget (`observability.langfuse.flushBudgetMs`, default 3000 ms) and the Langfuse exporter within its own budget. A provider that fails to flush, or does not finish within the budget (a collector that cannot be reached, for one), is logged as a warning and makes the call reject once every provider has settled.
|
|
23
23
|
* @returns {Promise<void>} Resolves when every owned provider has flushed.
|
|
24
|
-
* @throws {Error} When any provider fails to flush.
|
|
24
|
+
* @throws {Error} When any provider fails to flush or does not finish within the flush budget.
|
|
25
25
|
*/
|
|
26
26
|
export declare function flushOpenTelemetry(): Promise<void>;
|
|
27
27
|
/**
|
|
28
|
-
* @description Terminally shuts down the providers NeuroLink owns once no `acquireTelemetry` holder remains; while holders remain it only flushes. Telemetry cannot be restarted in this process afterwards. A host provider is never shut down; with one, only NeuroLink's Langfuse exporter is shut down and unregistered.
|
|
29
|
-
* @returns {Promise<void>} Resolves when telemetry is flushed or shut down.
|
|
28
|
+
* @description Terminally shuts down the providers NeuroLink owns once no `acquireTelemetry` holder remains; while holders remain it only flushes, as `flushOpenTelemetry` does. Telemetry cannot be restarted in this process afterwards. A host provider is never shut down; with one, only NeuroLink's Langfuse exporter is shut down and unregistered. The OTLP exporters get the same budget as `flushOpenTelemetry`, and a failure of the shutdown itself is logged as a warning.
|
|
29
|
+
* @returns {Promise<void>} Resolves when telemetry is flushed or shut down, or the flush budget has run out.
|
|
30
|
+
* @throws {Error} When holders remain and the flush they get fails.
|
|
30
31
|
*/
|
|
31
32
|
export declare function shutdownOpenTelemetry(): Promise<void>;
|
|
32
33
|
/**
|