@juspay/neurolink 9.55.9 → 9.55.11
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 +8 -0
- package/dist/browser/neurolink.min.js +507 -378
- package/dist/core/modules/StreamHandler.js +12 -0
- package/dist/core/modules/ToolsManager.js +4 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +4 -1
- package/dist/lib/core/modules/StreamHandler.js +12 -0
- package/dist/lib/core/modules/ToolsManager.js +4 -0
- package/dist/lib/index.d.ts +2 -2
- package/dist/lib/index.js +4 -1
- package/dist/lib/mcp/toolDiscoveryService.js +99 -3
- package/dist/lib/mcp/toolRegistry.js +3 -0
- package/dist/lib/neurolink.js +8 -23
- package/dist/lib/processors/media/AudioProcessor.js +22 -3
- package/dist/lib/processors/media/VideoProcessor.js +48 -11
- package/dist/lib/services/server/ai/observability/instrumentation.d.ts +26 -0
- package/dist/lib/services/server/ai/observability/instrumentation.js +98 -15
- package/dist/lib/types/processor.d.ts +27 -0
- package/dist/lib/utils/mcpErrorText.d.ts +10 -0
- package/dist/lib/utils/mcpErrorText.js +36 -0
- package/dist/lib/utils/timeout.js +6 -0
- package/dist/mcp/toolDiscoveryService.js +99 -3
- package/dist/mcp/toolRegistry.js +3 -0
- package/dist/neurolink.js +8 -23
- package/dist/processors/media/AudioProcessor.js +22 -3
- package/dist/processors/media/VideoProcessor.js +48 -11
- package/dist/services/server/ai/observability/instrumentation.d.ts +26 -0
- package/dist/services/server/ai/observability/instrumentation.js +98 -15
- package/dist/types/processor.d.ts +27 -0
- package/dist/utils/mcpErrorText.d.ts +10 -0
- package/dist/utils/mcpErrorText.js +35 -0
- package/dist/utils/timeout.js +6 -0
- package/package.json +4 -4
|
@@ -113,6 +113,18 @@ export class StreamHandler {
|
|
|
113
113
|
// rather than crashing the process with an unhandled rejection.
|
|
114
114
|
if (NoOutputGeneratedError.isInstance(error)) {
|
|
115
115
|
logger.warn(`${providerName}: Stream produced no output (NoOutputGeneratedError), returning empty stream`);
|
|
116
|
+
// Curator P2-5: stamp the active OTel span so ContextEnricher.onEnd()
|
|
117
|
+
// surfaces a WARNING-level Langfuse observation instead of defaulting
|
|
118
|
+
// to DEFAULT with no status message.
|
|
119
|
+
try {
|
|
120
|
+
const activeSpan = trace.getSpan(otelContext.active());
|
|
121
|
+
if (activeSpan) {
|
|
122
|
+
activeSpan.setAttribute("neurolink.no_output", true);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
// Tracing not initialized — ignore.
|
|
127
|
+
}
|
|
116
128
|
// S4 fix: yield a sentinel chunk so Pipeline B can detect the empty stream
|
|
117
129
|
// and set the span to WARNING status instead of OK
|
|
118
130
|
yield {
|
|
@@ -399,6 +399,10 @@ export class ToolsManager {
|
|
|
399
399
|
attributes: {
|
|
400
400
|
"tool.name": toolName,
|
|
401
401
|
"tool.type": "custom",
|
|
402
|
+
// Curator P1-3: pure wrapper — duplicates the AI SDK's
|
|
403
|
+
// ai.toolCall observation in Langfuse. Keep the OTel span
|
|
404
|
+
// for internal metrics; filter from Langfuse export.
|
|
405
|
+
"langfuse.internal": true,
|
|
402
406
|
},
|
|
403
407
|
});
|
|
404
408
|
const startTime = Date.now();
|
package/dist/index.d.ts
CHANGED
|
@@ -48,8 +48,8 @@ export { TTSProcessor } from "./utils/ttsProcessor.js";
|
|
|
48
48
|
import { NeuroLink } from "./neurolink.js";
|
|
49
49
|
export { NeuroLink };
|
|
50
50
|
export { buildObservabilityConfigFromEnv } from "./utils/observabilityHelpers.js";
|
|
51
|
-
import { createContextEnricher, flushOpenTelemetry, getLangfuseContext, getLangfuseHealthStatus, getLangfuseSpanProcessor, getSpanProcessors, getTracer, getTracerProvider, initializeOpenTelemetry, isOpenTelemetryInitialized, isUsingExternalTracerProvider, runWithCurrentLangfuseContext, setLangfuseContext, shutdownOpenTelemetry } from "./services/server/ai/observability/instrumentation.js";
|
|
52
|
-
export { initializeOpenTelemetry, shutdownOpenTelemetry, flushOpenTelemetry, getLangfuseHealthStatus, setLangfuseContext, getLangfuseSpanProcessor, getTracerProvider, isOpenTelemetryInitialized, getSpanProcessors, createContextEnricher, isUsingExternalTracerProvider, getLangfuseContext, getTracer, runWithCurrentLangfuseContext, };
|
|
51
|
+
import { createContextEnricher, flushOpenTelemetry, getLangfuseContext, getLangfuseHealthStatus, getLangfuseSpanProcessor, getSpanProcessors, getTracer, getTracerProvider, initializeOpenTelemetry, isLangfuseInternalSpan, isOpenTelemetryInitialized, isUsingExternalTracerProvider, langfuseShouldExportSpan, runWithCurrentLangfuseContext, setLangfuseContext, shutdownOpenTelemetry } from "./services/server/ai/observability/instrumentation.js";
|
|
52
|
+
export { initializeOpenTelemetry, shutdownOpenTelemetry, flushOpenTelemetry, getLangfuseHealthStatus, setLangfuseContext, getLangfuseSpanProcessor, getTracerProvider, isOpenTelemetryInitialized, getSpanProcessors, createContextEnricher, isUsingExternalTracerProvider, isLangfuseInternalSpan, langfuseShouldExportSpan, getLangfuseContext, getTracer, runWithCurrentLangfuseContext, };
|
|
53
53
|
export { clearAnalyticsMetrics, createAnalyticsMiddleware, getAnalyticsMetrics, } from "./middleware/builtin/analytics.js";
|
|
54
54
|
export { createLifecycleMiddleware } from "./middleware/builtin/lifecycle.js";
|
|
55
55
|
export { MiddlewareFactory } from "./middleware/factory.js";
|
package/dist/index.js
CHANGED
|
@@ -80,11 +80,14 @@ import { createContextEnricher, flushOpenTelemetry,
|
|
|
80
80
|
// Enhanced context and tracing
|
|
81
81
|
getLangfuseContext, getLangfuseHealthStatus, getLangfuseSpanProcessor,
|
|
82
82
|
// NEW EXPORTS - External TracerProvider Support
|
|
83
|
-
getSpanProcessors, getTracer, getTracerProvider, initializeOpenTelemetry, isOpenTelemetryInitialized, isUsingExternalTracerProvider, runWithCurrentLangfuseContext, setLangfuseContext, shutdownOpenTelemetry, } from "./services/server/ai/observability/instrumentation.js";
|
|
83
|
+
getSpanProcessors, getTracer, getTracerProvider, initializeOpenTelemetry, isLangfuseInternalSpan, isOpenTelemetryInitialized, isUsingExternalTracerProvider, langfuseShouldExportSpan, runWithCurrentLangfuseContext, setLangfuseContext, shutdownOpenTelemetry, } from "./services/server/ai/observability/instrumentation.js";
|
|
84
84
|
import { getTelemetryStatus as getStatus, initializeTelemetry as init, } from "./telemetry/index.js";
|
|
85
85
|
export { initializeOpenTelemetry, shutdownOpenTelemetry, flushOpenTelemetry, getLangfuseHealthStatus, setLangfuseContext, getLangfuseSpanProcessor, getTracerProvider, isOpenTelemetryInitialized,
|
|
86
86
|
// NEW EXPORTS - External TracerProvider Support
|
|
87
87
|
getSpanProcessors, createContextEnricher, isUsingExternalTracerProvider,
|
|
88
|
+
// Host-processor filter helpers — reuse NeuroLink's internal-span filtering
|
|
89
|
+
// when the host app registers its own LangfuseSpanProcessor.
|
|
90
|
+
isLangfuseInternalSpan, langfuseShouldExportSpan,
|
|
88
91
|
// Enhanced context and tracing
|
|
89
92
|
getLangfuseContext, getTracer,
|
|
90
93
|
// ALS context propagation helper
|
|
@@ -113,6 +113,18 @@ export class StreamHandler {
|
|
|
113
113
|
// rather than crashing the process with an unhandled rejection.
|
|
114
114
|
if (NoOutputGeneratedError.isInstance(error)) {
|
|
115
115
|
logger.warn(`${providerName}: Stream produced no output (NoOutputGeneratedError), returning empty stream`);
|
|
116
|
+
// Curator P2-5: stamp the active OTel span so ContextEnricher.onEnd()
|
|
117
|
+
// surfaces a WARNING-level Langfuse observation instead of defaulting
|
|
118
|
+
// to DEFAULT with no status message.
|
|
119
|
+
try {
|
|
120
|
+
const activeSpan = trace.getSpan(otelContext.active());
|
|
121
|
+
if (activeSpan) {
|
|
122
|
+
activeSpan.setAttribute("neurolink.no_output", true);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
// Tracing not initialized — ignore.
|
|
127
|
+
}
|
|
116
128
|
// S4 fix: yield a sentinel chunk so Pipeline B can detect the empty stream
|
|
117
129
|
// and set the span to WARNING status instead of OK
|
|
118
130
|
yield {
|
|
@@ -399,6 +399,10 @@ export class ToolsManager {
|
|
|
399
399
|
attributes: {
|
|
400
400
|
"tool.name": toolName,
|
|
401
401
|
"tool.type": "custom",
|
|
402
|
+
// Curator P1-3: pure wrapper — duplicates the AI SDK's
|
|
403
|
+
// ai.toolCall observation in Langfuse. Keep the OTel span
|
|
404
|
+
// for internal metrics; filter from Langfuse export.
|
|
405
|
+
"langfuse.internal": true,
|
|
402
406
|
},
|
|
403
407
|
});
|
|
404
408
|
const startTime = Date.now();
|
package/dist/lib/index.d.ts
CHANGED
|
@@ -48,8 +48,8 @@ export { TTSProcessor } from "./utils/ttsProcessor.js";
|
|
|
48
48
|
import { NeuroLink } from "./neurolink.js";
|
|
49
49
|
export { NeuroLink };
|
|
50
50
|
export { buildObservabilityConfigFromEnv } from "./utils/observabilityHelpers.js";
|
|
51
|
-
import { createContextEnricher, flushOpenTelemetry, getLangfuseContext, getLangfuseHealthStatus, getLangfuseSpanProcessor, getSpanProcessors, getTracer, getTracerProvider, initializeOpenTelemetry, isOpenTelemetryInitialized, isUsingExternalTracerProvider, runWithCurrentLangfuseContext, setLangfuseContext, shutdownOpenTelemetry } from "./services/server/ai/observability/instrumentation.js";
|
|
52
|
-
export { initializeOpenTelemetry, shutdownOpenTelemetry, flushOpenTelemetry, getLangfuseHealthStatus, setLangfuseContext, getLangfuseSpanProcessor, getTracerProvider, isOpenTelemetryInitialized, getSpanProcessors, createContextEnricher, isUsingExternalTracerProvider, getLangfuseContext, getTracer, runWithCurrentLangfuseContext, };
|
|
51
|
+
import { createContextEnricher, flushOpenTelemetry, getLangfuseContext, getLangfuseHealthStatus, getLangfuseSpanProcessor, getSpanProcessors, getTracer, getTracerProvider, initializeOpenTelemetry, isLangfuseInternalSpan, isOpenTelemetryInitialized, isUsingExternalTracerProvider, langfuseShouldExportSpan, runWithCurrentLangfuseContext, setLangfuseContext, shutdownOpenTelemetry } from "./services/server/ai/observability/instrumentation.js";
|
|
52
|
+
export { initializeOpenTelemetry, shutdownOpenTelemetry, flushOpenTelemetry, getLangfuseHealthStatus, setLangfuseContext, getLangfuseSpanProcessor, getTracerProvider, isOpenTelemetryInitialized, getSpanProcessors, createContextEnricher, isUsingExternalTracerProvider, isLangfuseInternalSpan, langfuseShouldExportSpan, getLangfuseContext, getTracer, runWithCurrentLangfuseContext, };
|
|
53
53
|
export { clearAnalyticsMetrics, createAnalyticsMiddleware, getAnalyticsMetrics, } from "./middleware/builtin/analytics.js";
|
|
54
54
|
export { createLifecycleMiddleware } from "./middleware/builtin/lifecycle.js";
|
|
55
55
|
export { MiddlewareFactory } from "./middleware/factory.js";
|
package/dist/lib/index.js
CHANGED
|
@@ -80,11 +80,14 @@ import { createContextEnricher, flushOpenTelemetry,
|
|
|
80
80
|
// Enhanced context and tracing
|
|
81
81
|
getLangfuseContext, getLangfuseHealthStatus, getLangfuseSpanProcessor,
|
|
82
82
|
// NEW EXPORTS - External TracerProvider Support
|
|
83
|
-
getSpanProcessors, getTracer, getTracerProvider, initializeOpenTelemetry, isOpenTelemetryInitialized, isUsingExternalTracerProvider, runWithCurrentLangfuseContext, setLangfuseContext, shutdownOpenTelemetry, } from "./services/server/ai/observability/instrumentation.js";
|
|
83
|
+
getSpanProcessors, getTracer, getTracerProvider, initializeOpenTelemetry, isLangfuseInternalSpan, isOpenTelemetryInitialized, isUsingExternalTracerProvider, langfuseShouldExportSpan, runWithCurrentLangfuseContext, setLangfuseContext, shutdownOpenTelemetry, } from "./services/server/ai/observability/instrumentation.js";
|
|
84
84
|
import { getTelemetryStatus as getStatus, initializeTelemetry as init, } from "./telemetry/index.js";
|
|
85
85
|
export { initializeOpenTelemetry, shutdownOpenTelemetry, flushOpenTelemetry, getLangfuseHealthStatus, setLangfuseContext, getLangfuseSpanProcessor, getTracerProvider, isOpenTelemetryInitialized,
|
|
86
86
|
// NEW EXPORTS - External TracerProvider Support
|
|
87
87
|
getSpanProcessors, createContextEnricher, isUsingExternalTracerProvider,
|
|
88
|
+
// Host-processor filter helpers — reuse NeuroLink's internal-span filtering
|
|
89
|
+
// when the host app registers its own LangfuseSpanProcessor.
|
|
90
|
+
isLangfuseInternalSpan, langfuseShouldExportSpan,
|
|
88
91
|
// Enhanced context and tracing
|
|
89
92
|
getLangfuseContext, getTracer,
|
|
90
93
|
// ALS context propagation helper
|
|
@@ -9,10 +9,72 @@ import { globalCircuitBreakerManager, CircuitBreakerOpenError, } from "./mcpCirc
|
|
|
9
9
|
import { isObject, isNullish } from "../utils/typeUtils.js";
|
|
10
10
|
import { validateToolName, validateToolDescription, } from "../utils/parameterValidation.js";
|
|
11
11
|
import { withTimeout } from "../utils/errorHandling.js";
|
|
12
|
+
import { extractMcpErrorText } from "../utils/mcpErrorText.js";
|
|
12
13
|
import { SpanKind, SpanStatusCode } from "@opentelemetry/api";
|
|
13
14
|
import { tracers } from "../telemetry/tracers.js";
|
|
14
15
|
import { withSpan } from "../telemetry/withSpan.js";
|
|
15
16
|
const mcpTracer = tracers.mcp;
|
|
17
|
+
/**
|
|
18
|
+
* JSON-stringify a value for a Langfuse input/output preview attribute,
|
|
19
|
+
* truncated to a hard cap to stay under span attribute size limits. The
|
|
20
|
+
* returned string is guaranteed to be ≤ maxLen characters; when truncated,
|
|
21
|
+
* the last character is replaced with an ellipsis.
|
|
22
|
+
*/
|
|
23
|
+
function safeJsonStringify(value, maxLen) {
|
|
24
|
+
if (maxLen <= 0) {
|
|
25
|
+
return "";
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
const str = JSON.stringify(value);
|
|
29
|
+
if (typeof str !== "string") {
|
|
30
|
+
return "";
|
|
31
|
+
}
|
|
32
|
+
if (str.length <= maxLen) {
|
|
33
|
+
return str;
|
|
34
|
+
}
|
|
35
|
+
return str.slice(0, Math.max(0, maxLen - 1)) + "…";
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return "";
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Match property names that commonly hold secrets. Values under these keys
|
|
43
|
+
* are replaced with `[REDACTED]` before serialization. Case-insensitive.
|
|
44
|
+
* Conservative list — anything matching *here* is masked; the rest of the
|
|
45
|
+
* structure is preserved so Langfuse still gets a meaningful preview.
|
|
46
|
+
*/
|
|
47
|
+
const SENSITIVE_KEY_PATTERN = /^(password|passwd|secret|token|api[_-]?key|apikey|access[_-]?key|authorization|auth|bearer|credential|cookie|session[_-]?id|private[_-]?key|client[_-]?secret|refresh[_-]?token|x-api-key)$/i;
|
|
48
|
+
/**
|
|
49
|
+
* Walk a value, producing a structurally-equivalent copy with sensitive-key
|
|
50
|
+
* values masked. Unlike `transformParamsForLogging` (which collapses objects
|
|
51
|
+
* to a "N params" string), this preserves non-sensitive content so Langfuse
|
|
52
|
+
* input/output previews stay useful. Bounded depth guards against cycles.
|
|
53
|
+
*/
|
|
54
|
+
function redactForPreview(value, depth = 0) {
|
|
55
|
+
if (depth > 10) {
|
|
56
|
+
return "[...]";
|
|
57
|
+
}
|
|
58
|
+
if (value === null || value === undefined) {
|
|
59
|
+
return value;
|
|
60
|
+
}
|
|
61
|
+
if (typeof value !== "object") {
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
64
|
+
if (Array.isArray(value)) {
|
|
65
|
+
return value.map((v) => redactForPreview(v, depth + 1));
|
|
66
|
+
}
|
|
67
|
+
const out = {};
|
|
68
|
+
for (const [k, v] of Object.entries(value)) {
|
|
69
|
+
if (SENSITIVE_KEY_PATTERN.test(k)) {
|
|
70
|
+
out[k] = "[REDACTED]";
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
out[k] = redactForPreview(v, depth + 1);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
16
78
|
/**
|
|
17
79
|
* Default timeout for MCP tool execution operations in milliseconds.
|
|
18
80
|
* Configurable via MCP_TOOL_TIMEOUT env var.
|
|
@@ -376,6 +438,18 @@ export class ToolDiscoveryService extends EventEmitter {
|
|
|
376
438
|
"mcp.server_id": serverId,
|
|
377
439
|
"mcp.tool_name": toolName,
|
|
378
440
|
"mcp.timeout_ms": effectiveTimeout,
|
|
441
|
+
// Curator P1-4: Langfuse observations rely on ai.*/gen_ai.*
|
|
442
|
+
// attributes for tool name and I/O previews. Provide them so
|
|
443
|
+
// the SPAN observation in Langfuse is legible without
|
|
444
|
+
// timestamp-joining against the parent ai.toolCall. Redact
|
|
445
|
+
// parameters via the existing secret-stripping helper so
|
|
446
|
+
// tokens/credentials/paths don't leave the process.
|
|
447
|
+
"ai.tool.name": toolName,
|
|
448
|
+
"gen_ai.tool.name": toolName,
|
|
449
|
+
"gen_ai.request": safeJsonStringify({
|
|
450
|
+
name: toolName,
|
|
451
|
+
arguments: redactForPreview(parameters),
|
|
452
|
+
}, 2048),
|
|
379
453
|
},
|
|
380
454
|
}, async (callSpan) => {
|
|
381
455
|
try {
|
|
@@ -384,11 +458,26 @@ export class ToolDiscoveryService extends EventEmitter {
|
|
|
384
458
|
name: toolName,
|
|
385
459
|
arguments: parameters,
|
|
386
460
|
}), timeout, new Error(`Tool execution timeout: ${toolName}`));
|
|
387
|
-
|
|
461
|
+
// Curator P0-1/P0-2: the MCP client does NOT throw on protocol
|
|
462
|
+
// errors — it returns { isError: true, content: [...] }. Detect
|
|
463
|
+
// that pattern so the span status reflects reality.
|
|
464
|
+
const resultObj = callResult;
|
|
465
|
+
if (resultObj && resultObj.isError === true) {
|
|
466
|
+
const errorText = extractMcpErrorText(resultObj);
|
|
467
|
+
callSpan.setStatus({
|
|
468
|
+
code: SpanStatusCode.ERROR,
|
|
469
|
+
message: errorText || `Tool ${toolName} returned isError`,
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
else {
|
|
473
|
+
callSpan.setStatus({ code: SpanStatusCode.OK });
|
|
474
|
+
}
|
|
388
475
|
// ── MCP output normalization ──────────────────────────────────
|
|
389
476
|
// Intercept here — after receive, before cache, before memory,
|
|
390
477
|
// before LLM context injection. Returns a compact surrogate when
|
|
391
478
|
// the payload exceeds mcp.outputLimits.maxBytes.
|
|
479
|
+
let resultForPreview = callResult;
|
|
480
|
+
let resultForReturn = callResult;
|
|
392
481
|
if (this.outputNormalizer) {
|
|
393
482
|
try {
|
|
394
483
|
const normalized = await this.outputNormalizer.normalize(callResult, { toolName, serverId });
|
|
@@ -396,7 +485,8 @@ export class ToolDiscoveryService extends EventEmitter {
|
|
|
396
485
|
if (normalized.isExternalized) {
|
|
397
486
|
callSpan.setAttribute("mcp.output.original_bytes", normalized.originalBytes);
|
|
398
487
|
}
|
|
399
|
-
|
|
488
|
+
resultForPreview = normalized.result;
|
|
489
|
+
resultForReturn = normalized.result;
|
|
400
490
|
}
|
|
401
491
|
catch (normErr) {
|
|
402
492
|
mcpLogger.warn(`[ToolDiscoveryService] McpOutputNormalizer failed for ` +
|
|
@@ -405,7 +495,13 @@ export class ToolDiscoveryService extends EventEmitter {
|
|
|
405
495
|
}
|
|
406
496
|
}
|
|
407
497
|
// ── end normalization ─────────────────────────────────────────
|
|
408
|
-
|
|
498
|
+
// Curator P1-4: build gen_ai.response AFTER normalization so
|
|
499
|
+
// large payloads use the compact surrogate instead of the raw
|
|
500
|
+
// result (avoids redundant stringify + memory hit on payloads
|
|
501
|
+
// that were specifically externalized to Redis). Redact via the
|
|
502
|
+
// same secret-stripping path used for request parameters.
|
|
503
|
+
callSpan.setAttribute("gen_ai.response", safeJsonStringify(redactForPreview(resultForPreview), 2048));
|
|
504
|
+
return resultForReturn;
|
|
409
505
|
}
|
|
410
506
|
catch (err) {
|
|
411
507
|
callSpan.setStatus({
|
|
@@ -257,6 +257,9 @@ export class MCPToolRegistry extends MCPRegistry {
|
|
|
257
257
|
attributes: {
|
|
258
258
|
[ATTR.GEN_AI_TOOL_NAME]: toolName,
|
|
259
259
|
[ATTR.MCP_SERVER_ID]: preResolvedServerId || "builtin",
|
|
260
|
+
// Curator P1-3: registry-level wrapper — duplicates ai.toolCall in
|
|
261
|
+
// Langfuse. Retained for OTel/metrics; skipped for Langfuse export.
|
|
262
|
+
"langfuse.internal": true,
|
|
260
263
|
},
|
|
261
264
|
}, async (span) => {
|
|
262
265
|
try {
|
package/dist/lib/neurolink.js
CHANGED
|
@@ -64,6 +64,7 @@ import { CircuitBreaker, ERROR_CODES, ErrorFactory, isAbortError, isRetriableErr
|
|
|
64
64
|
// Factory processing imports
|
|
65
65
|
import { createCleanStreamOptions, enhanceTextGenerationOptions, processFactoryOptions, processStreamingFactoryOptions, validateFactoryConfig, } from "./utils/factoryProcessing.js";
|
|
66
66
|
import { logger, mcpLogger } from "./utils/logger.js";
|
|
67
|
+
import { extractMcpErrorText } from "./utils/mcpErrorText.js";
|
|
67
68
|
import { createCustomToolServerInfo, detectCategory, } from "./utils/mcpDefaults.js";
|
|
68
69
|
import { resolveModel } from "./utils/modelAliasResolver.js";
|
|
69
70
|
// Import orchestration components
|
|
@@ -133,29 +134,6 @@ function mcpCategoryToErrorCategory(mcpCategory) {
|
|
|
133
134
|
return ErrorCategory.EXECUTION;
|
|
134
135
|
}
|
|
135
136
|
}
|
|
136
|
-
/**
|
|
137
|
-
* Extract a human-readable error string from an MCP isError result object.
|
|
138
|
-
* Returns an empty string if nothing useful can be extracted.
|
|
139
|
-
*/
|
|
140
|
-
function extractMcpErrorText(raw) {
|
|
141
|
-
try {
|
|
142
|
-
const resultObj = typeof raw === "string" ? JSON.parse(raw) : raw;
|
|
143
|
-
if (!resultObj || typeof resultObj !== "object") {
|
|
144
|
-
return "";
|
|
145
|
-
}
|
|
146
|
-
const content = resultObj.content;
|
|
147
|
-
if (!Array.isArray(content)) {
|
|
148
|
-
return "";
|
|
149
|
-
}
|
|
150
|
-
const texts = content
|
|
151
|
-
.filter((c) => c.type === "text" && c.text)
|
|
152
|
-
.map((c) => c.text);
|
|
153
|
-
return texts.join(" ").substring(0, 500);
|
|
154
|
-
}
|
|
155
|
-
catch {
|
|
156
|
-
return "";
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
137
|
/**
|
|
160
138
|
* Check if an error is a non-retryable provider error that should immediately
|
|
161
139
|
* stop the retry/fallback chain. These errors represent permanent failures
|
|
@@ -6267,6 +6245,13 @@ Current user's request: ${currentInput}`;
|
|
|
6267
6245
|
"tool.type": executionContext.toolType,
|
|
6268
6246
|
"tool.input_size": executionContext.inputSize,
|
|
6269
6247
|
"tool.input_preview": executionContext.truncatedInput,
|
|
6248
|
+
// NOT marked langfuse.internal: this is the public entrypoint for
|
|
6249
|
+
// `NeuroLink.executeTool()`. Direct API callers (not going through
|
|
6250
|
+
// the AI SDK) would otherwise produce zero Langfuse observations —
|
|
6251
|
+
// the lower-level registry/discovery spans are internal wrappers.
|
|
6252
|
+
// AI-SDK-initiated custom tools will produce both ai.toolCall and
|
|
6253
|
+
// this span, which is the accepted tradeoff for keeping direct
|
|
6254
|
+
// invocations observable.
|
|
6270
6255
|
},
|
|
6271
6256
|
}, (toolSpan) => this.executeToolWithSpan(toolName, params, options, executionContext, toolSpan));
|
|
6272
6257
|
}
|
|
@@ -36,10 +36,27 @@
|
|
|
36
36
|
* }
|
|
37
37
|
* ```
|
|
38
38
|
*/
|
|
39
|
-
import { parseBuffer, selectCover } from "music-metadata";
|
|
40
39
|
import { BaseFileProcessor } from "../base/BaseFileProcessor.js";
|
|
41
40
|
import { SIZE_LIMITS_MB } from "../config/index.js";
|
|
42
41
|
import { FileErrorCode } from "../errors/index.js";
|
|
42
|
+
let _musicMetadata = null;
|
|
43
|
+
async function loadMusicMetadata() {
|
|
44
|
+
if (_musicMetadata) {
|
|
45
|
+
return _musicMetadata;
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
_musicMetadata = await import(/* @vite-ignore */ "music-metadata");
|
|
49
|
+
return _musicMetadata;
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
const e = err instanceof Error ? err : null;
|
|
53
|
+
if (e?.code === "ERR_MODULE_NOT_FOUND" &&
|
|
54
|
+
e.message.includes("music-metadata")) {
|
|
55
|
+
throw new Error('Audio processing requires the "music-metadata" package. Install it with:\n pnpm add music-metadata', { cause: err });
|
|
56
|
+
}
|
|
57
|
+
throw err;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
43
60
|
// =============================================================================
|
|
44
61
|
// TYPES
|
|
45
62
|
// =============================================================================
|
|
@@ -239,7 +256,7 @@ export class AudioProcessor extends BaseFileProcessor {
|
|
|
239
256
|
// Step 5: Extract tags from common metadata
|
|
240
257
|
const tags = this.extractTags(audioMetadata);
|
|
241
258
|
// Step 6: Extract embedded cover art if present
|
|
242
|
-
const coverArt = this.extractCoverArt(audioMetadata);
|
|
259
|
+
const coverArt = await this.extractCoverArt(audioMetadata);
|
|
243
260
|
// Step 7: Attempt transcription if API key is available
|
|
244
261
|
const filename = this.getFilename(fileInfo);
|
|
245
262
|
const transcriptionResult = await this.attemptTranscription(buffer, filename, fileInfo.mimetype);
|
|
@@ -404,6 +421,7 @@ export class AudioProcessor extends BaseFileProcessor {
|
|
|
404
421
|
// parseBuffer accepts (Uint8Array, fileInfo?: IFileInfo | string, options?)
|
|
405
422
|
// where string is interpreted as MIME type.
|
|
406
423
|
const mimeType = fileInfo.mimetype || undefined;
|
|
424
|
+
const { parseBuffer } = await loadMusicMetadata();
|
|
407
425
|
return parseBuffer(buffer, mimeType);
|
|
408
426
|
}
|
|
409
427
|
/**
|
|
@@ -467,11 +485,12 @@ export class AudioProcessor extends BaseFileProcessor {
|
|
|
467
485
|
* @param audioMetadata - Parsed audio metadata from music-metadata
|
|
468
486
|
* @returns Cover art as Buffer, or null if no cover art is embedded
|
|
469
487
|
*/
|
|
470
|
-
extractCoverArt(audioMetadata) {
|
|
488
|
+
async extractCoverArt(audioMetadata) {
|
|
471
489
|
const pictures = audioMetadata.common.picture;
|
|
472
490
|
if (!pictures || pictures.length === 0) {
|
|
473
491
|
return null;
|
|
474
492
|
}
|
|
493
|
+
const { selectCover } = await loadMusicMetadata();
|
|
475
494
|
const cover = selectCover(pictures);
|
|
476
495
|
if (!cover) {
|
|
477
496
|
return null;
|
|
@@ -44,9 +44,7 @@
|
|
|
44
44
|
* ```
|
|
45
45
|
*/
|
|
46
46
|
import { randomUUID } from "crypto";
|
|
47
|
-
import ffmpegCommand from "fluent-ffmpeg";
|
|
48
47
|
import { createWriteStream, existsSync, promises as fs } from "fs";
|
|
49
|
-
import { Input, FilePathSource, ALL_FORMATS } from "mediabunny";
|
|
50
48
|
import { tmpdir } from "os";
|
|
51
49
|
import { join } from "path";
|
|
52
50
|
import { Readable } from "stream";
|
|
@@ -56,6 +54,40 @@ import { SIZE_LIMITS_MB } from "../config/index.js";
|
|
|
56
54
|
import { FileErrorCode } from "../errors/index.js";
|
|
57
55
|
import { tracers, ATTR, withSpan } from "../../telemetry/index.js";
|
|
58
56
|
import { logger } from "../../utils/logger.js";
|
|
57
|
+
// fluent-ffmpeg's default export is callable + has static methods — avoid caching
|
|
58
|
+
// the module type (it confuses TS); Node's module cache handles dedup.
|
|
59
|
+
async function loadFluentFfmpeg() {
|
|
60
|
+
try {
|
|
61
|
+
const mod = await import(/* @vite-ignore */ "fluent-ffmpeg");
|
|
62
|
+
return mod.default;
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
const e = err instanceof Error ? err : null;
|
|
66
|
+
if (e?.code === "ERR_MODULE_NOT_FOUND" &&
|
|
67
|
+
e.message.includes("fluent-ffmpeg")) {
|
|
68
|
+
throw new Error('Video processing requires the "fluent-ffmpeg" package. Install it with:\n pnpm add fluent-ffmpeg', { cause: err });
|
|
69
|
+
}
|
|
70
|
+
throw err;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
let _mediabunny = null;
|
|
74
|
+
async function loadMediaBunny() {
|
|
75
|
+
if (_mediabunny) {
|
|
76
|
+
return _mediabunny;
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
_mediabunny = await import(/* @vite-ignore */ "mediabunny");
|
|
80
|
+
return _mediabunny;
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
const e = err instanceof Error ? err : null;
|
|
84
|
+
if (e?.code === "ERR_MODULE_NOT_FOUND" &&
|
|
85
|
+
e.message.includes("mediabunny")) {
|
|
86
|
+
throw new Error('Video processing requires the "mediabunny" package. Install it with:\n pnpm add mediabunny', { cause: err });
|
|
87
|
+
}
|
|
88
|
+
throw err;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
59
91
|
// =============================================================================
|
|
60
92
|
// FFMPEG PATH INITIALIZATION
|
|
61
93
|
// =============================================================================
|
|
@@ -90,7 +122,8 @@ async function initFfmpegPaths() {
|
|
|
90
122
|
const ffmpegStatic = await import("ffmpeg-static");
|
|
91
123
|
const ffmpegPath = ffmpegStatic.default;
|
|
92
124
|
if (typeof ffmpegPath === "string" && existsSync(ffmpegPath)) {
|
|
93
|
-
|
|
125
|
+
const ff = await loadFluentFfmpeg();
|
|
126
|
+
ff.setFfmpegPath(ffmpegPath);
|
|
94
127
|
}
|
|
95
128
|
}
|
|
96
129
|
catch {
|
|
@@ -469,7 +502,8 @@ export class VideoProcessor extends BaseFileProcessor {
|
|
|
469
502
|
* @param filePath - Path to the video file
|
|
470
503
|
* @returns Success result with probe data or error message
|
|
471
504
|
*/
|
|
472
|
-
probeVideo(filePath) {
|
|
505
|
+
async probeVideo(filePath) {
|
|
506
|
+
const ffmpeg = await loadFluentFfmpeg();
|
|
473
507
|
return new Promise((resolve) => {
|
|
474
508
|
const timeoutId = setTimeout(() => {
|
|
475
509
|
resolve({
|
|
@@ -477,7 +511,7 @@ export class VideoProcessor extends BaseFileProcessor {
|
|
|
477
511
|
error: `ffprobe timed out after ${VIDEO_CONFIG.FFPROBE_TIMEOUT_MS}ms`,
|
|
478
512
|
});
|
|
479
513
|
}, VIDEO_CONFIG.FFPROBE_TIMEOUT_MS);
|
|
480
|
-
|
|
514
|
+
ffmpeg.ffprobe(filePath, (err, data) => {
|
|
481
515
|
clearTimeout(timeoutId);
|
|
482
516
|
if (err) {
|
|
483
517
|
resolve({
|
|
@@ -496,11 +530,12 @@ export class VideoProcessor extends BaseFileProcessor {
|
|
|
496
530
|
* Falls back to ffprobe if mediabunny fails or doesn't support the format.
|
|
497
531
|
*/
|
|
498
532
|
async probeVideoWithMediabunny(filePath) {
|
|
533
|
+
const mb = await loadMediaBunny();
|
|
499
534
|
let input;
|
|
500
535
|
try {
|
|
501
|
-
input = new Input({
|
|
502
|
-
source: new FilePathSource(filePath),
|
|
503
|
-
formats: [...ALL_FORMATS],
|
|
536
|
+
input = new mb.Input({
|
|
537
|
+
source: new mb.FilePathSource(filePath),
|
|
538
|
+
formats: [...mb.ALL_FORMATS],
|
|
504
539
|
});
|
|
505
540
|
const duration = await input.computeDuration();
|
|
506
541
|
const videoTrack = await input.getPrimaryVideoTrack();
|
|
@@ -671,7 +706,8 @@ export class VideoProcessor extends BaseFileProcessor {
|
|
|
671
706
|
* @param outputDir - Directory to write frame files
|
|
672
707
|
* @param timestamps - Array of timestamps in seconds
|
|
673
708
|
*/
|
|
674
|
-
runFfmpegFrameExtraction(videoPath, outputDir, timestamps, intervalSec) {
|
|
709
|
+
async runFfmpegFrameExtraction(videoPath, outputDir, timestamps, intervalSec) {
|
|
710
|
+
const ff = await loadFluentFfmpeg();
|
|
675
711
|
return new Promise((resolve, reject) => {
|
|
676
712
|
// Improved select expression to pick exactly one frame per interval
|
|
677
713
|
// instead of multiple frames within a 0.5s window.
|
|
@@ -679,7 +715,7 @@ export class VideoProcessor extends BaseFileProcessor {
|
|
|
679
715
|
const timeoutId = setTimeout(() => {
|
|
680
716
|
reject(new Error(`ffmpeg frame extraction timed out after ${VIDEO_CONFIG.FFMPEG_TIMEOUT_MS}ms`));
|
|
681
717
|
}, VIDEO_CONFIG.FFMPEG_TIMEOUT_MS);
|
|
682
|
-
|
|
718
|
+
ff(videoPath)
|
|
683
719
|
.outputOptions([
|
|
684
720
|
"-vf",
|
|
685
721
|
`select='${selectExpr}',scale='min(${VIDEO_CONFIG.FRAME_MAX_DIMENSION}\\,iw):-2'`,
|
|
@@ -740,11 +776,12 @@ export class VideoProcessor extends BaseFileProcessor {
|
|
|
740
776
|
*/
|
|
741
777
|
async extractSubtitles(videoPath, tempDir) {
|
|
742
778
|
const subtitlePath = join(tempDir, "subtitles.srt");
|
|
779
|
+
const ffSub = await loadFluentFfmpeg();
|
|
743
780
|
await new Promise((resolve, reject) => {
|
|
744
781
|
const timeoutId = setTimeout(() => {
|
|
745
782
|
reject(new Error(`ffmpeg subtitle extraction timed out after ${VIDEO_CONFIG.FFMPEG_TIMEOUT_MS}ms`));
|
|
746
783
|
}, VIDEO_CONFIG.FFMPEG_TIMEOUT_MS);
|
|
747
|
-
|
|
784
|
+
ffSub(videoPath)
|
|
748
785
|
.outputOptions(["-map", "0:s:0", "-c:s", "srt"])
|
|
749
786
|
.output(subtitlePath)
|
|
750
787
|
.on("end", () => {
|
|
@@ -11,6 +11,32 @@ import { LoggerProvider } from "@opentelemetry/sdk-logs";
|
|
|
11
11
|
import { type SpanProcessor } from "@opentelemetry/sdk-trace-base";
|
|
12
12
|
import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
|
|
13
13
|
import type { LangfuseConfig, LangfuseContext } from "../../../../types/index.js";
|
|
14
|
+
/**
|
|
15
|
+
* True when a span is an internal NeuroLink wrapper that should NOT be sent to
|
|
16
|
+
* Langfuse. Internal wrappers carry the `langfuse.internal: true` attribute.
|
|
17
|
+
*
|
|
18
|
+
* Exposed so host apps that bring their own `LangfuseSpanProcessor` (e.g.
|
|
19
|
+
* `skipLangfuseSpanProcessor: true`, or manual registration on an existing
|
|
20
|
+
* TracerProvider) can apply the same filter and avoid duplicate observations.
|
|
21
|
+
*/
|
|
22
|
+
export declare function isLangfuseInternalSpan(span: {
|
|
23
|
+
attributes?: Record<string, unknown>;
|
|
24
|
+
}): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Drop-in `shouldExportSpan` predicate for a `LangfuseSpanProcessor` that
|
|
27
|
+
* filters out NeuroLink internal wrapper spans.
|
|
28
|
+
*
|
|
29
|
+
* Usage in host apps:
|
|
30
|
+
* ```ts
|
|
31
|
+
* import { langfuseShouldExportSpan } from "@juspay/neurolink";
|
|
32
|
+
* new LangfuseSpanProcessor({ ..., shouldExportSpan: langfuseShouldExportSpan });
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export declare function langfuseShouldExportSpan({ otelSpan, }: {
|
|
36
|
+
otelSpan: {
|
|
37
|
+
attributes?: Record<string, unknown>;
|
|
38
|
+
};
|
|
39
|
+
}): boolean;
|
|
14
40
|
/**
|
|
15
41
|
* Initialize OpenTelemetry with Langfuse span processor
|
|
16
42
|
*
|