@juspay/neurolink 12.34.0 → 12.34.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +3 -7
- package/dist/browser/neurolink.min.js +418 -418
- package/dist/mcp/externalServerManager.js +1 -1
- package/dist/mcp/mcpClientFactory.d.ts +1 -1
- package/dist/mcp/mcpClientFactory.js +5 -5
- package/dist/neurolink.js +6 -2
- package/dist/proxy/vertexAnthropicFallback.d.ts +8 -0
- package/dist/proxy/vertexAnthropicFallback.js +30 -6
- package/dist/services/server/ai/observability/instrumentation.d.ts +7 -14
- package/dist/services/server/ai/observability/instrumentation.js +80 -32
- package/dist/utils/redis.js +3 -1
- package/package.json +2 -1
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
* - Tool discovery and registration
|
|
8
8
|
*/
|
|
9
9
|
import { EventEmitter } from "events";
|
|
10
|
-
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
11
10
|
import { mcpLogger } from "../utils/logger.js";
|
|
12
11
|
import { MCPClientFactory } from "./mcpClientFactory.js";
|
|
13
12
|
import { ToolDiscoveryService } from "./toolDiscoveryService.js";
|
|
@@ -1063,6 +1062,7 @@ export class ExternalServerManager extends EventEmitter {
|
|
|
1063
1062
|
instance.client = clientResult.client;
|
|
1064
1063
|
instance.transportInstance = clientResult.transport;
|
|
1065
1064
|
instance.process = clientResult.process || null;
|
|
1065
|
+
const { StdioClientTransport } = await import("@modelcontextprotocol/sdk/client/stdio.js");
|
|
1066
1066
|
instance.pid =
|
|
1067
1067
|
clientResult.transport instanceof StdioClientTransport
|
|
1068
1068
|
? (clientResult.transport.pid ?? undefined)
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Supports stdio, SSE, WebSocket, and HTTP transports
|
|
5
5
|
* Enhanced with retry, rate limiting, and OAuth 2.1 support
|
|
6
6
|
*/
|
|
7
|
-
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
7
|
+
import type { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
8
8
|
import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
|
|
9
9
|
import type { ClientCapabilities } from "@modelcontextprotocol/sdk/types.js";
|
|
10
10
|
import type { ChildProcess } from "child_process";
|
|
@@ -4,11 +4,6 @@
|
|
|
4
4
|
* Supports stdio, SSE, WebSocket, and HTTP transports
|
|
5
5
|
* Enhanced with retry, rate limiting, and OAuth 2.1 support
|
|
6
6
|
*/
|
|
7
|
-
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
8
|
-
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
9
|
-
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
10
|
-
import { WebSocketClientTransport } from "@modelcontextprotocol/sdk/client/websocket.js";
|
|
11
|
-
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
12
7
|
import { mcpLogger } from "../utils/logger.js";
|
|
13
8
|
import { globalCircuitBreakerManager } from "./mcpCircuitBreaker.js";
|
|
14
9
|
import { CircuitBreakerOpenError } from "../types/index.js";
|
|
@@ -209,6 +204,7 @@ export class MCPClientFactory {
|
|
|
209
204
|
const transport = transportResult.transport;
|
|
210
205
|
try {
|
|
211
206
|
// Create client
|
|
207
|
+
const { Client } = await import("@modelcontextprotocol/sdk/client/index.js");
|
|
212
208
|
const client = new Client(this.NEUROLINK_IMPLEMENTATION, {
|
|
213
209
|
capabilities: this.DEFAULT_CAPABILITIES,
|
|
214
210
|
});
|
|
@@ -296,6 +292,7 @@ export class MCPClientFactory {
|
|
|
296
292
|
if (!config.command) {
|
|
297
293
|
throw new Error(`Command is required for stdio transport`);
|
|
298
294
|
}
|
|
295
|
+
const { StdioClientTransport } = await import("@modelcontextprotocol/sdk/client/stdio.js");
|
|
299
296
|
const transport = new StdioClientTransport({
|
|
300
297
|
command: config.command,
|
|
301
298
|
args: config.args || [],
|
|
@@ -333,6 +330,7 @@ export class MCPClientFactory {
|
|
|
333
330
|
});
|
|
334
331
|
try {
|
|
335
332
|
const url = new URL(config.url);
|
|
333
|
+
const { SSEClientTransport } = await import("@modelcontextprotocol/sdk/client/sse.js");
|
|
336
334
|
const transport = new SSEClientTransport(url);
|
|
337
335
|
return { transport };
|
|
338
336
|
}
|
|
@@ -352,6 +350,7 @@ export class MCPClientFactory {
|
|
|
352
350
|
});
|
|
353
351
|
try {
|
|
354
352
|
const url = new URL(config.url);
|
|
353
|
+
const { WebSocketClientTransport } = await import("@modelcontextprotocol/sdk/client/websocket.js");
|
|
355
354
|
const transport = new WebSocketClientTransport(url);
|
|
356
355
|
return { transport };
|
|
357
356
|
}
|
|
@@ -402,6 +401,7 @@ export class MCPClientFactory {
|
|
|
402
401
|
const requestInit = {
|
|
403
402
|
headers: Object.keys(headers).length > 0 ? headers : undefined,
|
|
404
403
|
};
|
|
404
|
+
const { StreamableHTTPClientTransport } = await import("@modelcontextprotocol/sdk/client/streamableHttp.js");
|
|
405
405
|
const transport = new StreamableHTTPClientTransport(url, {
|
|
406
406
|
requestInit,
|
|
407
407
|
fetch: fetchWithEnhancements,
|
package/dist/neurolink.js
CHANGED
|
@@ -2804,11 +2804,15 @@ Current user's request: ${currentInput}`;
|
|
|
2804
2804
|
try {
|
|
2805
2805
|
const langfuseConfig = this.observabilityConfig?.langfuse;
|
|
2806
2806
|
if (langfuseConfig?.enabled) {
|
|
2807
|
-
|
|
2807
|
+
try {
|
|
2808
|
+
await initializeOpenTelemetry(langfuseConfig);
|
|
2809
|
+
}
|
|
2810
|
+
catch (err) {
|
|
2808
2811
|
logger.error("[NeuroLink] OpenTelemetry initialization failed", {
|
|
2809
2812
|
error: err instanceof Error ? err.message : String(err),
|
|
2810
2813
|
});
|
|
2811
|
-
|
|
2814
|
+
return;
|
|
2815
|
+
}
|
|
2812
2816
|
logger.debug("[NeuroLink] Langfuse observability initialized via public method");
|
|
2813
2817
|
}
|
|
2814
2818
|
else {
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* `tool_use`, `tool_result`, `cache_control` and thinking blocks all survive
|
|
13
13
|
* here because nothing reshapes them.
|
|
14
14
|
*/
|
|
15
|
+
import type { GoogleAuth } from "google-auth-library";
|
|
15
16
|
import type { VertexAccessTokenProvider, UsageContext, VertexPassthroughTerminal, VertexAnthropicPassthroughRequest, VertexAnthropicPassthroughTarget } from "../types/index.js";
|
|
16
17
|
/**
|
|
17
18
|
* Translate `output_config` for Vertex.
|
|
@@ -40,6 +41,13 @@ export declare function buildVertexAnthropicPayload(body: Readonly<Record<string
|
|
|
40
41
|
* caller's `stream` flag selects the verb rather than changing the body.
|
|
41
42
|
*/
|
|
42
43
|
export declare function buildVertexAnthropicUrl(target: VertexAnthropicPassthroughTarget): string;
|
|
44
|
+
/** Exported only for continuous-test-suite-vertex-anthropic-fallback.ts,
|
|
45
|
+
* which proves concurrent callers share one instance. Not part of the
|
|
46
|
+
* package's public surface — this module isn't re-exported from index.ts. */
|
|
47
|
+
export declare function auth(): Promise<GoogleAuth>;
|
|
48
|
+
/** Test-only: isolates the concurrency proof from cache state any earlier
|
|
49
|
+
* test in the same process may have left behind. */
|
|
50
|
+
export declare function resetAuthCacheForTests(): void;
|
|
43
51
|
/**
|
|
44
52
|
* Replace the Google credential lookup, for tests only.
|
|
45
53
|
*
|
|
@@ -12,7 +12,6 @@
|
|
|
12
12
|
* `tool_use`, `tool_result`, `cache_control` and thinking blocks all survive
|
|
13
13
|
* here because nothing reshapes them.
|
|
14
14
|
*/
|
|
15
|
-
import { GoogleAuth } from "google-auth-library";
|
|
16
15
|
import { logger } from "../utils/logger.js";
|
|
17
16
|
import { isTransientNetworkError } from "./proxyFetch.js";
|
|
18
17
|
/** Vertex pins the Anthropic API version in the body, not a header. */
|
|
@@ -379,16 +378,41 @@ export function buildVertexAnthropicUrl(target) {
|
|
|
379
378
|
return (`https://${host}/v1/projects/${target.projectId}/locations/` +
|
|
380
379
|
`${target.location}/publishers/anthropic/models/${target.model}:${verb}`);
|
|
381
380
|
}
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
381
|
+
// google-auth-library is only needed once a Vertex fallback request actually
|
|
382
|
+
// needs a Google access token, so it's loaded with `await import(...)` instead
|
|
383
|
+
// of a static import. The module promise is cached at module scope, and the
|
|
384
|
+
// resulting client instance is cached separately, exactly as before.
|
|
385
|
+
let googleAuthModulePromise;
|
|
386
|
+
function loadGoogleAuth() {
|
|
387
|
+
if (!googleAuthModulePromise) {
|
|
388
|
+
googleAuthModulePromise = import("google-auth-library");
|
|
389
|
+
}
|
|
390
|
+
return googleAuthModulePromise;
|
|
391
|
+
}
|
|
392
|
+
// Cache the PROMISE, not the resolved client: the guard below runs to
|
|
393
|
+
// completion before any await, so two calls entering before the first
|
|
394
|
+
// resolves both see the same in-flight promise instead of each racing to
|
|
395
|
+
// construct — and return — its own GoogleAuth instance.
|
|
396
|
+
let cachedAuthPromise;
|
|
397
|
+
/** Exported only for continuous-test-suite-vertex-anthropic-fallback.ts,
|
|
398
|
+
* which proves concurrent callers share one instance. Not part of the
|
|
399
|
+
* package's public surface — this module isn't re-exported from index.ts. */
|
|
400
|
+
export function auth() {
|
|
401
|
+
if (!cachedAuthPromise) {
|
|
402
|
+
cachedAuthPromise = loadGoogleAuth().then(({ GoogleAuth }) => new GoogleAuth({ scopes: [CLOUD_PLATFORM_SCOPE] }));
|
|
403
|
+
}
|
|
404
|
+
return cachedAuthPromise;
|
|
405
|
+
}
|
|
406
|
+
/** Test-only: isolates the concurrency proof from cache state any earlier
|
|
407
|
+
* test in the same process may have left behind. */
|
|
408
|
+
export function resetAuthCacheForTests() {
|
|
409
|
+
cachedAuthPromise = undefined;
|
|
386
410
|
}
|
|
387
411
|
let accessTokenProviderForTests;
|
|
388
412
|
async function vertexAccessToken() {
|
|
389
413
|
return accessTokenProviderForTests
|
|
390
414
|
? accessTokenProviderForTests()
|
|
391
|
-
: auth().getAccessToken();
|
|
415
|
+
: (await auth()).getAccessToken();
|
|
392
416
|
}
|
|
393
417
|
/**
|
|
394
418
|
* Replace the Google credential lookup, for tests only.
|
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
* Flow: Vercel AI SDK → OpenTelemetry Spans → LangfuseSpanProcessor → Langfuse Platform
|
|
8
8
|
*/
|
|
9
9
|
import { trace, type Span as ApiSpan } from "@opentelemetry/api";
|
|
10
|
-
import { LoggerProvider } from "@opentelemetry/sdk-logs";
|
|
11
|
-
import {
|
|
12
|
-
import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
|
|
10
|
+
import type { LoggerProvider } from "@opentelemetry/sdk-logs";
|
|
11
|
+
import type { SpanProcessor } from "@opentelemetry/sdk-trace-base";
|
|
12
|
+
import type { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
|
|
13
13
|
import type { LangfuseConfig, LangfuseContext } from "../../../../types/index.js";
|
|
14
14
|
/**
|
|
15
15
|
* True when a span is an internal NeuroLink wrapper that should NOT be sent to
|
|
@@ -38,18 +38,11 @@ export declare function langfuseShouldExportSpan({ otelSpan, }: {
|
|
|
38
38
|
};
|
|
39
39
|
}): boolean;
|
|
40
40
|
/**
|
|
41
|
-
* Initialize OpenTelemetry
|
|
41
|
+
* Initialize OpenTelemetry once and let concurrent callers share the work.
|
|
42
42
|
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
* 3. Registering the provider globally for AI SDK to use
|
|
47
|
-
*
|
|
48
|
-
* NEW: If useExternalTracerProvider is true or autoDetectExternalProvider detects
|
|
49
|
-
* an existing provider, steps 2 and 3 are skipped. The span processors are still
|
|
50
|
-
* created and can be retrieved via getSpanProcessors().
|
|
51
|
-
*
|
|
52
|
-
* @param config - Langfuse configuration passed from parent application
|
|
43
|
+
* NeuroLink constructors intentionally start this asynchronously. Keeping the
|
|
44
|
+
* in-flight promise here lets another constructor join it and lets an immediate
|
|
45
|
+
* flush or shutdown wait for the same initialization before touching providers.
|
|
53
46
|
*/
|
|
54
47
|
export declare function initializeOpenTelemetry(config: LangfuseConfig): Promise<void>;
|
|
55
48
|
/**
|
|
@@ -7,23 +7,17 @@
|
|
|
7
7
|
* Flow: Vercel AI SDK → OpenTelemetry Spans → LangfuseSpanProcessor → Langfuse Platform
|
|
8
8
|
*/
|
|
9
9
|
import { metrics, SpanStatusCode, trace, } from "@opentelemetry/api";
|
|
10
|
-
import { W3CTraceContextPropagator } from "@opentelemetry/core";
|
|
11
|
-
import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-http";
|
|
12
|
-
import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http";
|
|
13
|
-
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
|
|
14
|
-
import { resourceFromAttributes } from "@opentelemetry/resources";
|
|
15
|
-
import { MeterProvider, PeriodicExportingMetricReader, } from "@opentelemetry/sdk-metrics";
|
|
16
|
-
import { BatchLogRecordProcessor, LoggerProvider, } from "@opentelemetry/sdk-logs";
|
|
17
|
-
import { BatchSpanProcessor, } from "@opentelemetry/sdk-trace-base";
|
|
18
|
-
import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
|
|
19
|
-
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION, } from "@opentelemetry/semantic-conventions";
|
|
20
10
|
import { AsyncLocalStorage } from "async_hooks";
|
|
21
11
|
import { extractMcpErrorText } from "../../../../utils/mcpErrorText.js";
|
|
22
12
|
import { logger } from "../../../../utils/logger.js";
|
|
23
13
|
import { tryImport } from "../../../../utils/tryImport.js";
|
|
24
14
|
import { LANGFUSE_ATTR } from "../../../../telemetry/attributes.js";
|
|
25
15
|
const LOG_PREFIX = "[OpenTelemetry]";
|
|
26
|
-
function createOtelResource(config, serviceName) {
|
|
16
|
+
async function createOtelResource(config, serviceName) {
|
|
17
|
+
const [{ resourceFromAttributes }, { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION },] = await Promise.all([
|
|
18
|
+
import("@opentelemetry/resources"),
|
|
19
|
+
import("@opentelemetry/semantic-conventions"),
|
|
20
|
+
]);
|
|
27
21
|
return resourceFromAttributes({
|
|
28
22
|
[ATTR_SERVICE_NAME]: serviceName,
|
|
29
23
|
[ATTR_SERVICE_VERSION]: config.release || "v1.0.0",
|
|
@@ -32,13 +26,19 @@ function createOtelResource(config, serviceName) {
|
|
|
32
26
|
}
|
|
33
27
|
/**
|
|
34
28
|
* Configure OTLP metrics and logs with bounded serialization batches for
|
|
35
|
-
* request-serving processes.
|
|
29
|
+
* request-serving processes. Initialization awaits this so a caller may flush
|
|
30
|
+
* or shut down immediately after it returns without racing providers that have
|
|
31
|
+
* not been constructed yet. Signal-specific failures stay non-fatal here.
|
|
36
32
|
*/
|
|
37
|
-
function initializeOtlpMetricsAndLogs(resource, otlpEndpoint, serviceName) {
|
|
33
|
+
async function initializeOtlpMetricsAndLogs(resource, otlpEndpoint, serviceName) {
|
|
38
34
|
if (!otlpEndpoint) {
|
|
39
35
|
return;
|
|
40
36
|
}
|
|
41
37
|
try {
|
|
38
|
+
const [{ OTLPMetricExporter }, { MeterProvider, PeriodicExportingMetricReader },] = await Promise.all([
|
|
39
|
+
import("@opentelemetry/exporter-metrics-otlp-http"),
|
|
40
|
+
import("@opentelemetry/sdk-metrics"),
|
|
41
|
+
]);
|
|
42
42
|
const metricExporter = new OTLPMetricExporter({
|
|
43
43
|
url: `${otlpEndpoint}/v1/metrics`,
|
|
44
44
|
});
|
|
@@ -68,6 +68,10 @@ function initializeOtlpMetricsAndLogs(resource, otlpEndpoint, serviceName) {
|
|
|
68
68
|
});
|
|
69
69
|
}
|
|
70
70
|
try {
|
|
71
|
+
const [{ OTLPLogExporter }, { BatchLogRecordProcessor, LoggerProvider }] = await Promise.all([
|
|
72
|
+
import("@opentelemetry/exporter-logs-otlp-http"),
|
|
73
|
+
import("@opentelemetry/sdk-logs"),
|
|
74
|
+
]);
|
|
71
75
|
const logExporter = new OTLPLogExporter({
|
|
72
76
|
url: `${otlpEndpoint}/v1/logs`,
|
|
73
77
|
});
|
|
@@ -101,6 +105,7 @@ let meterProvider = null;
|
|
|
101
105
|
let loggerProvider = null;
|
|
102
106
|
let langfuseProcessor = null;
|
|
103
107
|
let isInitialized = false;
|
|
108
|
+
let initializationPromise = null;
|
|
104
109
|
let isCredentialsValid = false;
|
|
105
110
|
let currentConfig = null;
|
|
106
111
|
let usingExternalProvider = false;
|
|
@@ -637,8 +642,8 @@ async function initializeExternalOpenTelemetryMode(config, resource, otlpEndpoin
|
|
|
637
642
|
? await createLangfuseProcessor(config)
|
|
638
643
|
: null;
|
|
639
644
|
usingExternalProvider = true;
|
|
645
|
+
await initializeOtlpMetricsAndLogs(resource, otlpEndpoint, serviceName);
|
|
640
646
|
isInitialized = true;
|
|
641
|
-
initializeOtlpMetricsAndLogs(resource, otlpEndpoint, serviceName);
|
|
642
647
|
try {
|
|
643
648
|
const globalProvider = trace.getTracerProvider();
|
|
644
649
|
const provider = globalProvider;
|
|
@@ -759,6 +764,10 @@ async function initializeStandaloneOpenTelemetryMode(config, resource, otlpEndpo
|
|
|
759
764
|
}
|
|
760
765
|
if (otlpEndpoint) {
|
|
761
766
|
try {
|
|
767
|
+
const [{ OTLPTraceExporter }, { BatchSpanProcessor }] = await Promise.all([
|
|
768
|
+
import("@opentelemetry/exporter-trace-otlp-http"),
|
|
769
|
+
import("@opentelemetry/sdk-trace-base"),
|
|
770
|
+
]);
|
|
762
771
|
const otlpExporter = new OTLPTraceExporter({
|
|
763
772
|
url: `${otlpEndpoint}/v1/traces`,
|
|
764
773
|
});
|
|
@@ -782,13 +791,17 @@ async function initializeStandaloneOpenTelemetryMode(config, resource, otlpEndpo
|
|
|
782
791
|
});
|
|
783
792
|
}
|
|
784
793
|
}
|
|
794
|
+
const [{ NodeTracerProvider }, { W3CTraceContextPropagator }] = await Promise.all([
|
|
795
|
+
import("@opentelemetry/sdk-trace-node"),
|
|
796
|
+
import("@opentelemetry/core"),
|
|
797
|
+
]);
|
|
785
798
|
tracerProvider = new NodeTracerProvider({ resource, spanProcessors });
|
|
786
799
|
tracerProvider.register({
|
|
787
800
|
propagator: new W3CTraceContextPropagator(),
|
|
788
801
|
});
|
|
789
802
|
usingExternalProvider = false;
|
|
803
|
+
await initializeOtlpMetricsAndLogs(resource, otlpEndpoint, serviceName);
|
|
790
804
|
isInitialized = true;
|
|
791
|
-
initializeOtlpMetricsAndLogs(resource, otlpEndpoint, serviceName);
|
|
792
805
|
logger.info(`${LOG_PREFIX} Observability initialized`, {
|
|
793
806
|
baseUrl: config.baseUrl || "https://cloud.langfuse.com",
|
|
794
807
|
environment: config.environment || "dev",
|
|
@@ -820,6 +833,39 @@ async function initializeStandaloneOpenTelemetryMode(config, resource, otlpEndpo
|
|
|
820
833
|
throw error;
|
|
821
834
|
}
|
|
822
835
|
}
|
|
836
|
+
/**
|
|
837
|
+
* Initialize OpenTelemetry once and let concurrent callers share the work.
|
|
838
|
+
*
|
|
839
|
+
* NeuroLink constructors intentionally start this asynchronously. Keeping the
|
|
840
|
+
* in-flight promise here lets another constructor join it and lets an immediate
|
|
841
|
+
* flush or shutdown wait for the same initialization before touching providers.
|
|
842
|
+
*/
|
|
843
|
+
export async function initializeOpenTelemetry(config) {
|
|
844
|
+
if (isInitialized) {
|
|
845
|
+
currentConfig = config;
|
|
846
|
+
logger.debug(`${LOG_PREFIX} Already initialized, config updated`, {
|
|
847
|
+
usingExternalProvider,
|
|
848
|
+
hasLangfuseProcessor: !!langfuseProcessor,
|
|
849
|
+
hasTraceNameFormat: typeof config.traceNameFormat === "function",
|
|
850
|
+
});
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
if (initializationPromise) {
|
|
854
|
+
await initializationPromise;
|
|
855
|
+
currentConfig = config;
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
const pending = initializeOpenTelemetryOnce(config);
|
|
859
|
+
initializationPromise = pending;
|
|
860
|
+
try {
|
|
861
|
+
await pending;
|
|
862
|
+
}
|
|
863
|
+
finally {
|
|
864
|
+
if (initializationPromise === pending) {
|
|
865
|
+
initializationPromise = null;
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
}
|
|
823
869
|
/**
|
|
824
870
|
* Initialize OpenTelemetry with Langfuse span processor
|
|
825
871
|
*
|
|
@@ -834,20 +880,7 @@ async function initializeStandaloneOpenTelemetryMode(config, resource, otlpEndpo
|
|
|
834
880
|
*
|
|
835
881
|
* @param config - Langfuse configuration passed from parent application
|
|
836
882
|
*/
|
|
837
|
-
|
|
838
|
-
// Guard against multiple initializations — but always update config
|
|
839
|
-
// so that later NeuroLink instances can change traceNameFormat,
|
|
840
|
-
// autoDetectOperationName, and other configuration preferences
|
|
841
|
-
// without re-initializing the OTEL infrastructure.
|
|
842
|
-
if (isInitialized) {
|
|
843
|
-
currentConfig = config;
|
|
844
|
-
logger.debug(`${LOG_PREFIX} Already initialized, config updated`, {
|
|
845
|
-
usingExternalProvider,
|
|
846
|
-
hasLangfuseProcessor: !!langfuseProcessor,
|
|
847
|
-
hasTraceNameFormat: typeof config.traceNameFormat === "function",
|
|
848
|
-
});
|
|
849
|
-
return;
|
|
850
|
-
}
|
|
883
|
+
async function initializeOpenTelemetryOnce(config) {
|
|
851
884
|
// FIRST: Check for external provider mode - bypasses enabled check
|
|
852
885
|
// NOTE: When autoDetectExternalProvider is true, we trust the flag directly rather than
|
|
853
886
|
// calling hasExternalTracerProvider(). This is because Neurolink may bundle its own copy
|
|
@@ -862,7 +895,7 @@ export async function initializeOpenTelemetry(config) {
|
|
|
862
895
|
const langfuseRequested = config?.enabled === true;
|
|
863
896
|
const hasLangfuseCreds = !!config.publicKey && !!config.secretKey;
|
|
864
897
|
const serviceName = process.env.OTEL_SERVICE_NAME || "neurolink";
|
|
865
|
-
const resource = createOtelResource(config, serviceName);
|
|
898
|
+
const resource = await createOtelResource(config, serviceName);
|
|
866
899
|
if (shouldUseExternal) {
|
|
867
900
|
await initializeExternalOpenTelemetryMode(config, resource, otlpEndpoint, serviceName, langfuseRequested, hasLangfuseCreds);
|
|
868
901
|
return;
|
|
@@ -873,6 +906,15 @@ export async function initializeOpenTelemetry(config) {
|
|
|
873
906
|
* Flush all pending spans to Langfuse
|
|
874
907
|
*/
|
|
875
908
|
export async function flushOpenTelemetry() {
|
|
909
|
+
if (initializationPromise) {
|
|
910
|
+
try {
|
|
911
|
+
await initializationPromise;
|
|
912
|
+
}
|
|
913
|
+
catch {
|
|
914
|
+
// Initialization owns its error logging; a failed init leaves nothing
|
|
915
|
+
// initialized, so the check below skips the flush as it always has.
|
|
916
|
+
}
|
|
917
|
+
}
|
|
876
918
|
if (!isInitialized) {
|
|
877
919
|
logger.debug(`${LOG_PREFIX} Not initialized, skipping flush`);
|
|
878
920
|
return;
|
|
@@ -951,8 +993,14 @@ export async function flushOpenTelemetry() {
|
|
|
951
993
|
* Shutdown OpenTelemetry and Langfuse span processor
|
|
952
994
|
*/
|
|
953
995
|
export async function shutdownOpenTelemetry() {
|
|
954
|
-
if (
|
|
955
|
-
|
|
996
|
+
if (initializationPromise) {
|
|
997
|
+
try {
|
|
998
|
+
await initializationPromise;
|
|
999
|
+
}
|
|
1000
|
+
catch {
|
|
1001
|
+
// Initialization owns its error logging. Continue so any provider it
|
|
1002
|
+
// created before failing is still shut down below.
|
|
1003
|
+
}
|
|
956
1004
|
}
|
|
957
1005
|
try {
|
|
958
1006
|
// Only shutdown tracerProvider if we created it
|
package/dist/utils/redis.js
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
* Helper functions for Redis storage operations
|
|
4
4
|
*/
|
|
5
5
|
import { randomUUID } from "crypto";
|
|
6
|
-
import { createClient } from "redis";
|
|
7
6
|
import { logger } from "./logger.js";
|
|
8
7
|
const SESSION_ONLY_PREFIX = "session-only:";
|
|
9
8
|
/** Characters that end the authority (`user:pass@host:port`) of a URL in text. */
|
|
@@ -195,6 +194,9 @@ export async function createRedisClient(config) {
|
|
|
195
194
|
if (config.password && !config.url) {
|
|
196
195
|
clientOptions.password = config.password;
|
|
197
196
|
}
|
|
197
|
+
// Imported here, not at the top: the redis client is ~590 modules, a quarter
|
|
198
|
+
// of everything importing the SDK loaded, and most callers never connect.
|
|
199
|
+
const { createClient } = await import("redis");
|
|
198
200
|
// Create client with secured options
|
|
199
201
|
const client = createClient(clientOptions);
|
|
200
202
|
client.on("error", (err) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juspay/neurolink",
|
|
3
|
-
"version": "12.34.
|
|
3
|
+
"version": "12.34.1",
|
|
4
4
|
"packageManager": "pnpm@10.15.1",
|
|
5
5
|
"description": "The pipe layer of an AI nervous system: one interface connecting provider neurons to your application, across three inference types — generate, stream and decide. `decide` returns typed, calibrated judgements from a non-generative model (~400ms, ~$0.00002/call) for routing, tool selection and context budgeting. MCP-native (4 transports), voice TTS/STT/realtime, RAG, agents, memory, compaction, 9 observability exporters. OpenAI · Anthropic · Gemini · Bedrock · Azure · Ollama · TypeSafe Jev and more.",
|
|
6
6
|
"author": {
|
|
@@ -274,6 +274,7 @@
|
|
|
274
274
|
"test:aistudio-loop-characterization": "tsx test/continuous-test-suite-aistudio-loop-characterization.ts",
|
|
275
275
|
"test:docs-mcp": "pnpm exec tsx test/continuous-test-suite-docs-mcp.ts",
|
|
276
276
|
"test:docs-search-index": "pnpm exec tsx test/continuous-test-suite-docs-search-index.ts",
|
|
277
|
+
"test:import-cost": "pnpm exec tsx test/continuous-test-suite-import-cost.ts",
|
|
277
278
|
"test:vertex-claude-characterization": "tsx test/continuous-test-suite-vertex-claude-characterization.ts",
|
|
278
279
|
"test:toolfree-guard-overhead": "pnpm exec tsx test/continuous-test-suite-toolfree-guard-overhead.ts",
|
|
279
280
|
"test:file-tool-roots": "pnpm exec tsx test/continuous-test-suite-file-tool-roots.ts",
|