@juspay/neurolink 12.35.0 → 12.35.2
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 -3
- package/dist/auth/index.d.ts +4 -14
- package/dist/auth/index.js +5 -18
- package/dist/browser/neurolink.min.js +389 -389
- package/dist/server/routes/claudeProxyRoutes.js +46 -10
- package/dist/telemetry/telemetryService.d.ts +21 -0
- package/dist/telemetry/telemetryService.js +200 -14
- package/dist/utils/fileDetector.js +12 -3
- package/dist/utils/messageBuilder.js +5 -2
- package/dist/utils/redirectDispatcher.d.ts +6 -1
- package/dist/utils/redirectDispatcher.js +7 -2
- package/dist/utils/safeFetch.js +12 -3
- package/package.json +1 -1
|
@@ -13,7 +13,6 @@ import { claudeOutputLogFields, observeClaudeJsonOutput, observeClaudeStreamOutp
|
|
|
13
13
|
import { access, readFile } from "node:fs/promises";
|
|
14
14
|
import { homedir } from "node:os";
|
|
15
15
|
import { join } from "node:path";
|
|
16
|
-
import { Agent } from "undici";
|
|
17
16
|
import { buildStableClaudeCodeBillingHeader, CLAUDE_CLI_USER_AGENT, CLAUDE_CODE_OAUTH_BETAS, getOrCreateClaudeCodeIdentity, parseClaudeCodeUserId, } from "../../auth/anthropicOAuth.js";
|
|
18
17
|
import { clearAccountCooldown, loadAccountCooldowns, saveAccountCooldown, } from "../../proxy/accountCooldown.js";
|
|
19
18
|
import { anthropicAccountKeysEqual, ENV_ANTHROPIC_ACCOUNT_KEY, isAccountAllowed, LEGACY_ANTHROPIC_ACCOUNT_KEY, normalizeAnthropicAccountKey, shouldLoadFallbackCredential, } from "../../proxy/accountSelection.js";
|
|
@@ -184,6 +183,50 @@ function prepareAnthropicWire(ctx, bodyStr) {
|
|
|
184
183
|
};
|
|
185
184
|
}
|
|
186
185
|
let anthropicUpstreamDispatcher;
|
|
186
|
+
let anthropicUpstreamDispatcherPromise;
|
|
187
|
+
/**
|
|
188
|
+
* Lazily construct (once) and cache the upstream Agent, reused across every
|
|
189
|
+
* request rather than rebuilt per call.
|
|
190
|
+
*
|
|
191
|
+
* `undici` is imported dynamically here — rather than as a static top-level
|
|
192
|
+
* import — so that requiring this module (and transitively, the package
|
|
193
|
+
* entry that re-exports `createClaudeProxyRoutes`) does not force-load undici
|
|
194
|
+
* until an upstream Anthropic request actually dispatches. The construction
|
|
195
|
+
* itself is guarded by a cached promise, not a bare `??=` on the Agent, so
|
|
196
|
+
* concurrent first callers await the same in-flight construction instead of
|
|
197
|
+
* each racing past the `undefined` check and building a duplicate Agent. A
|
|
198
|
+
* rejected construction (a bad undici install, an Agent constructor throwing)
|
|
199
|
+
* clears the cached promise rather than leaving it permanently rejected, so
|
|
200
|
+
* the next request gets a fresh attempt instead of every later request
|
|
201
|
+
* failing forever on a cached failure. See
|
|
202
|
+
* test/continuous-test-suite-import-cost.ts.
|
|
203
|
+
*/
|
|
204
|
+
async function getAnthropicUpstreamDispatcher() {
|
|
205
|
+
if (anthropicUpstreamDispatcher) {
|
|
206
|
+
return anthropicUpstreamDispatcher;
|
|
207
|
+
}
|
|
208
|
+
anthropicUpstreamDispatcherPromise ??= (async () => {
|
|
209
|
+
try {
|
|
210
|
+
const { Agent: UndiciAgent } = await import("undici");
|
|
211
|
+
// Node's global fetch applies Undici's 300s default headers timeout
|
|
212
|
+
// before the route's 15-minute abort signal. Keep both transport
|
|
213
|
+
// deadlines aligned with the proxy contract.
|
|
214
|
+
const agent = new UndiciAgent({
|
|
215
|
+
headersTimeout: UPSTREAM_FETCH_TIMEOUT_MS,
|
|
216
|
+
bodyTimeout: UPSTREAM_FETCH_TIMEOUT_MS,
|
|
217
|
+
});
|
|
218
|
+
anthropicUpstreamDispatcher = agent;
|
|
219
|
+
return agent;
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
// Let a later request try again instead of every subsequent request
|
|
223
|
+
// failing forever against a cached rejection.
|
|
224
|
+
anthropicUpstreamDispatcherPromise = undefined;
|
|
225
|
+
throw error;
|
|
226
|
+
}
|
|
227
|
+
})();
|
|
228
|
+
return anthropicUpstreamDispatcherPromise;
|
|
229
|
+
}
|
|
187
230
|
async function fetchAnthropicUpstream(url, init, ctx, accountKey, dispatch) {
|
|
188
231
|
clearClaudeFallbackTerminalEvidence(ctx);
|
|
189
232
|
const prepared = dispatch.preparedContext;
|
|
@@ -203,15 +246,8 @@ async function fetchAnthropicUpstream(url, init, ctx, accountKey, dispatch) {
|
|
|
203
246
|
ctx.metadata.tokenBudget = lease.snapshot;
|
|
204
247
|
throw init.signal?.reason ?? ctx.abortSignal?.reason;
|
|
205
248
|
}
|
|
206
|
-
// Node's global fetch applies Undici's 300s default headers timeout before
|
|
207
|
-
// the route's 15-minute abort signal. Keep both transport deadlines aligned
|
|
208
|
-
// with the proxy contract and instantiate lazily so importing routes has no
|
|
209
|
-
// open transport handles.
|
|
210
|
-
anthropicUpstreamDispatcher ??= new Agent({
|
|
211
|
-
headersTimeout: UPSTREAM_FETCH_TIMEOUT_MS,
|
|
212
|
-
bodyTimeout: UPSTREAM_FETCH_TIMEOUT_MS,
|
|
213
|
-
});
|
|
214
249
|
try {
|
|
250
|
+
const upstreamDispatcher = await getAnthropicUpstreamDispatcher();
|
|
215
251
|
const dispatchedBody = String(init.body);
|
|
216
252
|
recordAttempt(dispatch.accountLabel, dispatch.accountType);
|
|
217
253
|
dispatch.tracer?.logUpstreamRequestHeaders(init.headers);
|
|
@@ -230,7 +266,7 @@ async function fetchAnthropicUpstream(url, init, ctx, accountKey, dispatch) {
|
|
|
230
266
|
const response = await fetch(url, {
|
|
231
267
|
...init,
|
|
232
268
|
body: dispatchedBody,
|
|
233
|
-
dispatcher:
|
|
269
|
+
dispatcher: upstreamDispatcher,
|
|
234
270
|
});
|
|
235
271
|
return observeAnthropicBudgetResponse(response, lease, () => {
|
|
236
272
|
ctx.metadata.tokenBudget = lease.snapshot;
|
|
@@ -20,6 +20,14 @@ export declare class TelemetryService {
|
|
|
20
20
|
private requestCount;
|
|
21
21
|
private totalResponseTime;
|
|
22
22
|
private responseTimeCount;
|
|
23
|
+
private static readonly MAX_PENDING_CALLS;
|
|
24
|
+
private pendingCalls;
|
|
25
|
+
private isReady;
|
|
26
|
+
private droppedPendingCallWarned;
|
|
27
|
+
/** The in-flight (or already-settled) async init. Never rejects — see
|
|
28
|
+
* initializeTelemetry()'s catch block, which always resolves normally.
|
|
29
|
+
* `initialize()`/`shutdown()` await it so they don't race construction. */
|
|
30
|
+
private readyPromise?;
|
|
23
31
|
private constructor();
|
|
24
32
|
static getInstance(): TelemetryService;
|
|
25
33
|
private isTelemetryEnabled;
|
|
@@ -27,6 +35,11 @@ export declare class TelemetryService {
|
|
|
27
35
|
private adoptExternalTracerProvider;
|
|
28
36
|
private initializeTelemetry;
|
|
29
37
|
private initializeMetrics;
|
|
38
|
+
private bufferPendingCall;
|
|
39
|
+
/** Instruments now exist — drain anything buffered while we waited, in
|
|
40
|
+
* order, then flip fully sync for every call after this one. */
|
|
41
|
+
private markReady;
|
|
42
|
+
private discardPendingCalls;
|
|
30
43
|
initialize(): Promise<void>;
|
|
31
44
|
/**
|
|
32
45
|
* @deprecated Vercel AI SDK's experimental_telemetry creates ai.generateText/ai.streamText
|
|
@@ -36,13 +49,21 @@ export declare class TelemetryService {
|
|
|
36
49
|
*/
|
|
37
50
|
traceAIRequest<T>(provider: string, operation: () => Promise<T>, operationType?: string): Promise<T>;
|
|
38
51
|
recordAIRequest(provider: string, model: string, tokens: number, duration: number, cost?: number): void;
|
|
52
|
+
private emitAIRequest;
|
|
39
53
|
recordAIError(provider: string, error: Error): void;
|
|
54
|
+
private emitAIError;
|
|
40
55
|
recordMCPToolCall(toolName: string, duration: number, success: boolean): void;
|
|
56
|
+
private emitMCPToolCall;
|
|
41
57
|
recordConnection(type: "websocket" | "sse" | "http"): void;
|
|
58
|
+
private emitConnection;
|
|
42
59
|
recordConnectionClosed(type: "websocket" | "sse" | "http"): void;
|
|
60
|
+
private emitConnectionClosed;
|
|
43
61
|
recordResponseTime(endpoint: string, method: string, duration: number): void;
|
|
62
|
+
private emitResponseTime;
|
|
44
63
|
recordCustomMetric(name: string, value: number, labels?: Record<string, string>): void;
|
|
64
|
+
private emitCustomMetric;
|
|
45
65
|
recordCustomHistogram(name: string, value: number, labels?: Record<string, string>): void;
|
|
66
|
+
private emitCustomHistogram;
|
|
46
67
|
getHealthMetrics(): Promise<HealthMetrics>;
|
|
47
68
|
isEnabled(): boolean;
|
|
48
69
|
getStatus(): {
|
|
@@ -1,8 +1,4 @@
|
|
|
1
1
|
import { context, metrics, trace, } from "@opentelemetry/api";
|
|
2
|
-
import { BasicTracerProvider, BatchSpanProcessor, } from "@opentelemetry/sdk-trace-base";
|
|
3
|
-
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
|
|
4
|
-
import { resourceFromAttributes } from "@opentelemetry/resources";
|
|
5
|
-
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION, } from "@opentelemetry/semantic-conventions";
|
|
6
2
|
import { logger } from "../utils/logger.js";
|
|
7
3
|
export class TelemetryService {
|
|
8
4
|
static instance;
|
|
@@ -27,17 +23,52 @@ export class TelemetryService {
|
|
|
27
23
|
requestCount = 0;
|
|
28
24
|
totalResponseTime = 0;
|
|
29
25
|
responseTimeCount = 0;
|
|
26
|
+
// Async-init readiness + buffering. Instruments above are constructed by
|
|
27
|
+
// an async initializeTelemetry() that the constructor fires but does not
|
|
28
|
+
// await (see getInstance()'s comment). A recording call can therefore
|
|
29
|
+
// legitimately arrive while telemetry is enabled but the instruments
|
|
30
|
+
// don't exist yet — that is NOT the same as telemetry being disabled, so
|
|
31
|
+
// it must not be dropped silently. `pendingCalls` buffers the OTEL side
|
|
32
|
+
// effect of such a call (never the whole method — see each recordX
|
|
33
|
+
// method's emitX split) until `markReady()` drains it. The cap and
|
|
34
|
+
// rate-limited warning below exist only for a pathological case (init
|
|
35
|
+
// that never resolves); the common case drains within milliseconds.
|
|
36
|
+
static MAX_PENDING_CALLS = 200;
|
|
37
|
+
pendingCalls = [];
|
|
38
|
+
isReady = false;
|
|
39
|
+
droppedPendingCallWarned = false;
|
|
40
|
+
/** The in-flight (or already-settled) async init. Never rejects — see
|
|
41
|
+
* initializeTelemetry()'s catch block, which always resolves normally.
|
|
42
|
+
* `initialize()`/`shutdown()` await it so they don't race construction. */
|
|
43
|
+
readyPromise;
|
|
30
44
|
constructor() {
|
|
31
45
|
// Check if telemetry is enabled
|
|
32
46
|
this.enabled = this.isTelemetryEnabled();
|
|
33
47
|
if (this.enabled) {
|
|
34
|
-
|
|
48
|
+
// initializeTelemetry() is async — it dynamically imports the OTel
|
|
49
|
+
// SDK packages and constructs the tracer/meter/instruments — but it
|
|
50
|
+
// is fired here WITHOUT an await. The constructor itself must stay
|
|
51
|
+
// fully synchronous and non-yielding, because getInstance() below is
|
|
52
|
+
// a plain, synchronous check-then-assign with no await in between.
|
|
53
|
+
// That is only race-free as long as nothing here yields control back
|
|
54
|
+
// to the event loop before `TelemetryService.instance` is assigned.
|
|
55
|
+
// Awaiting anything in the constructor (or making getInstance()
|
|
56
|
+
// async) would let two concurrent getInstance() callers both observe
|
|
57
|
+
// "not yet assigned" and each construct their own instance — the
|
|
58
|
+
// exact double-construction race that
|
|
59
|
+
// observability/instrumentation.ts's initializeOpenTelemetry() already
|
|
60
|
+
// hit and had to fix with a shared in-flight promise (see the
|
|
61
|
+
// "Initialize OpenTelemetry once and let concurrent callers share the
|
|
62
|
+
// work" comment there). Do not reintroduce it here.
|
|
63
|
+
this.readyPromise = this.initializeTelemetry();
|
|
35
64
|
}
|
|
36
65
|
else {
|
|
37
66
|
logger.debug("[Telemetry] Disabled - set NEUROLINK_TELEMETRY_ENABLED=true or configure OTEL_EXPORTER_OTLP_ENDPOINT to enable");
|
|
38
67
|
}
|
|
39
68
|
}
|
|
40
69
|
static getInstance() {
|
|
70
|
+
// Synchronous check-then-assign — see the constructor's comment above
|
|
71
|
+
// for why this must never gain an await before the assignment.
|
|
41
72
|
if (!TelemetryService.instance) {
|
|
42
73
|
TelemetryService.instance = new TelemetryService();
|
|
43
74
|
}
|
|
@@ -84,12 +115,19 @@ export class TelemetryService {
|
|
|
84
115
|
endpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
|
|
85
116
|
});
|
|
86
117
|
}
|
|
87
|
-
initializeTelemetry() {
|
|
118
|
+
async initializeTelemetry() {
|
|
88
119
|
try {
|
|
89
120
|
if (this.hasExternalTracerProvider()) {
|
|
90
121
|
this.adoptExternalTracerProvider("global tracer provider already registered");
|
|
122
|
+
this.markReady();
|
|
91
123
|
return;
|
|
92
124
|
}
|
|
125
|
+
const [{ BasicTracerProvider, BatchSpanProcessor }, { OTLPTraceExporter }, { resourceFromAttributes }, { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION },] = await Promise.all([
|
|
126
|
+
import("@opentelemetry/sdk-trace-base"),
|
|
127
|
+
import("@opentelemetry/exporter-trace-otlp-http"),
|
|
128
|
+
import("@opentelemetry/resources"),
|
|
129
|
+
import("@opentelemetry/semantic-conventions"),
|
|
130
|
+
]);
|
|
93
131
|
const resource = resourceFromAttributes({
|
|
94
132
|
[ATTR_SERVICE_NAME]: process.env.OTEL_SERVICE_NAME || "neurolink-ai",
|
|
95
133
|
[ATTR_SERVICE_VERSION]: process.env.OTEL_SERVICE_VERSION || "3.0.1",
|
|
@@ -106,6 +144,7 @@ export class TelemetryService {
|
|
|
106
144
|
this.meter = metrics.getMeter("neurolink-ai");
|
|
107
145
|
this.tracer = this.tracerProvider.getTracer("neurolink-ai");
|
|
108
146
|
this.initializeMetrics();
|
|
147
|
+
this.markReady();
|
|
109
148
|
logger.debug("[Telemetry] Initialized local telemetry exporter", {
|
|
110
149
|
endpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
|
|
111
150
|
globalTracerProviderOwnedBy: "observability/instrumentation",
|
|
@@ -118,10 +157,16 @@ export class TelemetryService {
|
|
|
118
157
|
errorMessage.includes("already set");
|
|
119
158
|
if (isDuplicateRegistration && this.hasExternalTracerProvider()) {
|
|
120
159
|
this.adoptExternalTracerProvider("duplicate global tracer registration detected");
|
|
160
|
+
this.markReady();
|
|
121
161
|
return;
|
|
122
162
|
}
|
|
123
163
|
logger.error("[Telemetry] Failed to initialize:", error);
|
|
124
164
|
this.enabled = false;
|
|
165
|
+
// No instruments exist and none ever will for this instance — there
|
|
166
|
+
// is nothing left to drain a buffered call against, so the buffer is
|
|
167
|
+
// cleared rather than drained. Every recording method's `!this.enabled`
|
|
168
|
+
// guard now makes it a no-op anyway.
|
|
169
|
+
this.discardPendingCalls();
|
|
125
170
|
}
|
|
126
171
|
}
|
|
127
172
|
initializeMetrics() {
|
|
@@ -153,10 +198,57 @@ export class TelemetryService {
|
|
|
153
198
|
description: "Response time in milliseconds",
|
|
154
199
|
});
|
|
155
200
|
}
|
|
201
|
+
// ============================================================
|
|
202
|
+
// Pending-call buffer (see the field comment above pendingCalls)
|
|
203
|
+
// ============================================================
|
|
204
|
+
bufferPendingCall(call) {
|
|
205
|
+
if (this.pendingCalls.length >= TelemetryService.MAX_PENDING_CALLS) {
|
|
206
|
+
// Drop the oldest, not the newest — recent calls are more likely to
|
|
207
|
+
// still matter to whoever is about to read a dashboard.
|
|
208
|
+
this.pendingCalls.shift();
|
|
209
|
+
if (!this.droppedPendingCallWarned) {
|
|
210
|
+
this.droppedPendingCallWarned = true;
|
|
211
|
+
logger.warn(`[Telemetry] Pending metric buffer exceeded ${TelemetryService.MAX_PENDING_CALLS} entries — dropping the oldest buffered call(s). Logged once, not per drop: telemetry initialization may be unusually slow or stalled.`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
this.pendingCalls.push(call);
|
|
215
|
+
}
|
|
216
|
+
/** Instruments now exist — drain anything buffered while we waited, in
|
|
217
|
+
* order, then flip fully sync for every call after this one. */
|
|
218
|
+
markReady() {
|
|
219
|
+
this.isReady = true;
|
|
220
|
+
if (this.pendingCalls.length === 0) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
const buffered = this.pendingCalls;
|
|
224
|
+
this.pendingCalls = [];
|
|
225
|
+
for (const call of buffered) {
|
|
226
|
+
try {
|
|
227
|
+
call();
|
|
228
|
+
}
|
|
229
|
+
catch (error) {
|
|
230
|
+
logger.warn("[Telemetry] A buffered metric call failed during drain:", error);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
discardPendingCalls() {
|
|
235
|
+
this.pendingCalls = [];
|
|
236
|
+
}
|
|
156
237
|
async initialize() {
|
|
157
238
|
if (!this.enabled) {
|
|
158
239
|
return;
|
|
159
240
|
}
|
|
241
|
+
// The tracer/meter/instruments are constructed asynchronously (see the
|
|
242
|
+
// constructor's comment). Wait for that to settle before inspecting
|
|
243
|
+
// usingExternalTracerProvider/tracerProvider below, so this method's
|
|
244
|
+
// behavior doesn't depend on how many event-loop ticks have passed
|
|
245
|
+
// since getInstance() was called — without this, a caller invoking
|
|
246
|
+
// initialize() right after getInstance() would almost always observe
|
|
247
|
+
// `!this.tracerProvider` and return early, silently skipping the
|
|
248
|
+
// AsyncLocalStorageContextManager registration below.
|
|
249
|
+
if (this.readyPromise) {
|
|
250
|
+
await this.readyPromise;
|
|
251
|
+
}
|
|
160
252
|
if (this.usingExternalTracerProvider) {
|
|
161
253
|
this.initialized = true;
|
|
162
254
|
logger.debug("[Telemetry] External TracerProvider already initialized by host");
|
|
@@ -227,7 +319,21 @@ export class TelemetryService {
|
|
|
227
319
|
this.requestCount++;
|
|
228
320
|
this.totalResponseTime += duration;
|
|
229
321
|
this.responseTimeCount++;
|
|
230
|
-
if (!this.enabled
|
|
322
|
+
if (!this.enabled) {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
if (!this.isReady) {
|
|
326
|
+
// Instruments aren't constructed yet — buffer the OTEL side effect
|
|
327
|
+
// only (emitAIRequest), never this whole method: the runtime
|
|
328
|
+
// counters above must fire exactly once per call, not again when the
|
|
329
|
+
// buffer drains.
|
|
330
|
+
this.bufferPendingCall(() => this.emitAIRequest(provider, model, tokens, duration, cost));
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
this.emitAIRequest(provider, model, tokens, duration, cost);
|
|
334
|
+
}
|
|
335
|
+
emitAIRequest(provider, model, tokens, duration, cost) {
|
|
336
|
+
if (!this.aiRequestCounter) {
|
|
231
337
|
return;
|
|
232
338
|
}
|
|
233
339
|
const labels = { provider, model };
|
|
@@ -241,7 +347,17 @@ export class TelemetryService {
|
|
|
241
347
|
recordAIError(provider, error) {
|
|
242
348
|
// Track runtime metrics
|
|
243
349
|
this.errorCount++;
|
|
244
|
-
if (!this.enabled
|
|
350
|
+
if (!this.enabled) {
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
if (!this.isReady) {
|
|
354
|
+
this.bufferPendingCall(() => this.emitAIError(provider, error));
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
this.emitAIError(provider, error);
|
|
358
|
+
}
|
|
359
|
+
emitAIError(provider, error) {
|
|
360
|
+
if (!this.aiProviderErrors) {
|
|
245
361
|
return;
|
|
246
362
|
}
|
|
247
363
|
this.aiProviderErrors.add(1, {
|
|
@@ -251,7 +367,17 @@ export class TelemetryService {
|
|
|
251
367
|
});
|
|
252
368
|
}
|
|
253
369
|
recordMCPToolCall(toolName, duration, success) {
|
|
254
|
-
if (!this.enabled
|
|
370
|
+
if (!this.enabled) {
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
if (!this.isReady) {
|
|
374
|
+
this.bufferPendingCall(() => this.emitMCPToolCall(toolName, duration, success));
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
this.emitMCPToolCall(toolName, duration, success);
|
|
378
|
+
}
|
|
379
|
+
emitMCPToolCall(toolName, duration, success) {
|
|
380
|
+
if (!this.mcpToolCalls) {
|
|
255
381
|
return;
|
|
256
382
|
}
|
|
257
383
|
this.mcpToolCalls.add(1, {
|
|
@@ -263,7 +389,17 @@ export class TelemetryService {
|
|
|
263
389
|
recordConnection(type) {
|
|
264
390
|
// Track runtime metrics
|
|
265
391
|
this.activeConnectionCount++;
|
|
266
|
-
if (!this.enabled
|
|
392
|
+
if (!this.enabled) {
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
if (!this.isReady) {
|
|
396
|
+
this.bufferPendingCall(() => this.emitConnection(type));
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
this.emitConnection(type);
|
|
400
|
+
}
|
|
401
|
+
emitConnection(type) {
|
|
402
|
+
if (!this.connectionCounter) {
|
|
267
403
|
return;
|
|
268
404
|
}
|
|
269
405
|
this.connectionCounter.add(1, { connection_type: type });
|
|
@@ -271,7 +407,17 @@ export class TelemetryService {
|
|
|
271
407
|
recordConnectionClosed(type) {
|
|
272
408
|
// Track runtime metrics
|
|
273
409
|
this.activeConnectionCount = Math.max(0, this.activeConnectionCount - 1);
|
|
274
|
-
if (!this.enabled
|
|
410
|
+
if (!this.enabled) {
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
if (!this.isReady) {
|
|
414
|
+
this.bufferPendingCall(() => this.emitConnectionClosed(type));
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
this.emitConnectionClosed(type);
|
|
418
|
+
}
|
|
419
|
+
emitConnectionClosed(type) {
|
|
420
|
+
if (!this.connectionCounter) {
|
|
275
421
|
return;
|
|
276
422
|
}
|
|
277
423
|
// Optionally record disconnection metrics if needed
|
|
@@ -284,7 +430,17 @@ export class TelemetryService {
|
|
|
284
430
|
// Track runtime metrics
|
|
285
431
|
this.totalResponseTime += duration;
|
|
286
432
|
this.responseTimeCount++;
|
|
287
|
-
if (!this.enabled
|
|
433
|
+
if (!this.enabled) {
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
if (!this.isReady) {
|
|
437
|
+
this.bufferPendingCall(() => this.emitResponseTime(endpoint, method, duration));
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
this.emitResponseTime(endpoint, method, duration);
|
|
441
|
+
}
|
|
442
|
+
emitResponseTime(endpoint, method, duration) {
|
|
443
|
+
if (!this.responseTimeHistogram) {
|
|
288
444
|
return;
|
|
289
445
|
}
|
|
290
446
|
this.responseTimeHistogram.record(duration, {
|
|
@@ -295,7 +451,17 @@ export class TelemetryService {
|
|
|
295
451
|
}
|
|
296
452
|
// Custom Metrics
|
|
297
453
|
recordCustomMetric(name, value, labels) {
|
|
298
|
-
if (!this.enabled
|
|
454
|
+
if (!this.enabled) {
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
if (!this.isReady) {
|
|
458
|
+
this.bufferPendingCall(() => this.emitCustomMetric(name, value, labels));
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
this.emitCustomMetric(name, value, labels);
|
|
462
|
+
}
|
|
463
|
+
emitCustomMetric(name, value, labels) {
|
|
464
|
+
if (!this.meter) {
|
|
299
465
|
return;
|
|
300
466
|
}
|
|
301
467
|
const counter = this.meter.createCounter(`custom_${name}`, {
|
|
@@ -304,7 +470,17 @@ export class TelemetryService {
|
|
|
304
470
|
counter.add(value, labels || {});
|
|
305
471
|
}
|
|
306
472
|
recordCustomHistogram(name, value, labels) {
|
|
307
|
-
if (!this.enabled
|
|
473
|
+
if (!this.enabled) {
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
if (!this.isReady) {
|
|
477
|
+
this.bufferPendingCall(() => this.emitCustomHistogram(name, value, labels));
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
this.emitCustomHistogram(name, value, labels);
|
|
481
|
+
}
|
|
482
|
+
emitCustomHistogram(name, value, labels) {
|
|
483
|
+
if (!this.meter) {
|
|
308
484
|
return;
|
|
309
485
|
}
|
|
310
486
|
const histogram = this.meter.createHistogram(`custom_${name}_histogram`, {
|
|
@@ -370,6 +546,16 @@ export class TelemetryService {
|
|
|
370
546
|
}
|
|
371
547
|
// Cleanup
|
|
372
548
|
async shutdown() {
|
|
549
|
+
// See initialize()'s comment: wait for the async instrument
|
|
550
|
+
// construction to settle before deciding whether there is a
|
|
551
|
+
// tracerProvider to shut down. Without this, a shutdown() called
|
|
552
|
+
// immediately after getInstance() would see no tracerProvider yet and
|
|
553
|
+
// no-op, while construction — which is already in flight — finishes
|
|
554
|
+
// moments later and leaves a live exporter (with its background flush
|
|
555
|
+
// timers) behind with nothing left to ever shut it down.
|
|
556
|
+
if (this.readyPromise) {
|
|
557
|
+
await this.readyPromise;
|
|
558
|
+
}
|
|
373
559
|
if (this.enabled &&
|
|
374
560
|
this.tracerProvider &&
|
|
375
561
|
!this.usingExternalTracerProvider) {
|
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
import { open, readFile, realpath } from "fs/promises";
|
|
7
7
|
import { STATUS_CODES } from "node:http";
|
|
8
8
|
import { basename, isAbsolute as isAbsolutePath, relative as relativePath, resolve as resolvePath, sep, } from "path";
|
|
9
|
-
import { request } from "undici";
|
|
10
9
|
import { redirectFollowingDispatcher } from "./redirectDispatcher.js";
|
|
11
10
|
// Lazy-loaded processor singletons — avoids loading heavy media deps
|
|
12
11
|
// (mediabunny, fluent-ffmpeg, music-metadata, adm-zip) on every generate() call.
|
|
@@ -1609,8 +1608,13 @@ export class FileDetector {
|
|
|
1609
1608
|
const timeout = options?.timeout || FileDetector.DEFAULT_NETWORK_TIMEOUT;
|
|
1610
1609
|
return withRetry(async () => {
|
|
1611
1610
|
try {
|
|
1611
|
+
// Dynamic import keeps undici off the package's static import
|
|
1612
|
+
// graph — it is only needed once a URL-referenced attachment
|
|
1613
|
+
// actually has to be fetched. See
|
|
1614
|
+
// test/continuous-test-suite-import-cost.ts.
|
|
1615
|
+
const { request } = await import("undici");
|
|
1612
1616
|
const response = await request(url, {
|
|
1613
|
-
dispatcher: redirectFollowingDispatcher(5),
|
|
1617
|
+
dispatcher: await redirectFollowingDispatcher(5),
|
|
1614
1618
|
method: "GET",
|
|
1615
1619
|
headersTimeout: timeout,
|
|
1616
1620
|
bodyTimeout: timeout,
|
|
@@ -2375,8 +2379,13 @@ class MimeTypeStrategy {
|
|
|
2375
2379
|
// Wrap the whole HEAD (request + body drain) in withTimeout so a stalled
|
|
2376
2380
|
// dump() can't hang detection, per the project's async-timeout guideline.
|
|
2377
2381
|
contentType = await withTimeout((async () => {
|
|
2382
|
+
// Dynamic import keeps undici off the package's static import
|
|
2383
|
+
// graph — it is only needed once a URL-referenced attachment
|
|
2384
|
+
// actually has to be fetched. See
|
|
2385
|
+
// test/continuous-test-suite-import-cost.ts.
|
|
2386
|
+
const { request } = await import("undici");
|
|
2378
2387
|
const response = await request(input, {
|
|
2379
|
-
dispatcher: redirectFollowingDispatcher(5),
|
|
2388
|
+
dispatcher: await redirectFollowingDispatcher(5),
|
|
2380
2389
|
method: "HEAD",
|
|
2381
2390
|
headersTimeout: FileDetector.DEFAULT_HEAD_TIMEOUT,
|
|
2382
2391
|
bodyTimeout: FileDetector.DEFAULT_HEAD_TIMEOUT,
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync, statSync } from "fs";
|
|
2
2
|
import { open as openAsync, readFile as readFileAsync, stat as statAsync, } from "fs/promises";
|
|
3
3
|
import pLimit from "p-limit";
|
|
4
|
-
import { request } from "undici";
|
|
5
4
|
import { redirectFollowingDispatcher } from "./redirectDispatcher.js";
|
|
6
5
|
import { MultimodalLogger, ProviderImageAdapter, } from "../adapters/providerImageAdapter.js";
|
|
7
6
|
import { CONVERSATION_INSTRUCTIONS, STRUCTURED_OUTPUT_INSTRUCTIONS, } from "../config/conversationMemory.js";
|
|
@@ -1780,8 +1779,12 @@ async function downloadImageFromUrl(url) {
|
|
|
1780
1779
|
// Apply rate limiting only if cache missed
|
|
1781
1780
|
await urlDownloadRateLimiter.acquire();
|
|
1782
1781
|
try {
|
|
1782
|
+
// Dynamic import keeps undici off the package's static import graph —
|
|
1783
|
+
// it is only needed once a URL-referenced image actually has to be
|
|
1784
|
+
// fetched. See test/continuous-test-suite-import-cost.ts.
|
|
1785
|
+
const { request } = await import("undici");
|
|
1783
1786
|
const response = await request(url, {
|
|
1784
|
-
dispatcher: redirectFollowingDispatcher(5),
|
|
1787
|
+
dispatcher: await redirectFollowingDispatcher(5),
|
|
1785
1788
|
method: "GET",
|
|
1786
1789
|
headersTimeout: 10000, // 10 second timeout for headers
|
|
1787
1790
|
bodyTimeout: 30000, // 30 second timeout for body,
|
|
@@ -34,6 +34,11 @@ import type { Dispatcher } from "undici";
|
|
|
34
34
|
* requests. Losing a proxy on one Node version is recoverable; silently failing
|
|
35
35
|
* every redirecting download is not.
|
|
36
36
|
*
|
|
37
|
+
* `undici` itself is imported dynamically here (rather than at module scope)
|
|
38
|
+
* so that requiring this module — and transitively, messageBuilder.ts /
|
|
39
|
+
* fileDetector.ts — does not force-load undici until a URL attachment is
|
|
40
|
+
* actually fetched. See test/continuous-test-suite-import-cost.ts.
|
|
41
|
+
*
|
|
37
42
|
* @param maxRedirections How many redirects to follow before giving up.
|
|
38
43
|
*/
|
|
39
|
-
export declare function redirectFollowingDispatcher(maxRedirections: number): Dispatcher
|
|
44
|
+
export declare function redirectFollowingDispatcher(maxRedirections: number): Promise<Dispatcher>;
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { Agent, getGlobalDispatcher, interceptors } from "undici";
|
|
2
1
|
/**
|
|
3
2
|
* The major of the `undici` this package depends on.
|
|
4
3
|
*
|
|
@@ -42,9 +41,15 @@ const NPM_UNDICI_MAJOR = 7;
|
|
|
42
41
|
* requests. Losing a proxy on one Node version is recoverable; silently failing
|
|
43
42
|
* every redirecting download is not.
|
|
44
43
|
*
|
|
44
|
+
* `undici` itself is imported dynamically here (rather than at module scope)
|
|
45
|
+
* so that requiring this module — and transitively, messageBuilder.ts /
|
|
46
|
+
* fileDetector.ts — does not force-load undici until a URL attachment is
|
|
47
|
+
* actually fetched. See test/continuous-test-suite-import-cost.ts.
|
|
48
|
+
*
|
|
45
49
|
* @param maxRedirections How many redirects to follow before giving up.
|
|
46
50
|
*/
|
|
47
|
-
export function redirectFollowingDispatcher(maxRedirections) {
|
|
51
|
+
export async function redirectFollowingDispatcher(maxRedirections) {
|
|
52
|
+
const { Agent, getGlobalDispatcher, interceptors } = await import("undici");
|
|
48
53
|
const builtinMajor = Number.parseInt(process.versions.undici?.split(".")[0] ?? "", 10);
|
|
49
54
|
const base = Number.isFinite(builtinMajor) && builtinMajor === NPM_UNDICI_MAJOR
|
|
50
55
|
? getGlobalDispatcher()
|
package/dist/utils/safeFetch.js
CHANGED
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
*
|
|
17
17
|
* @module utils/safeFetch
|
|
18
18
|
*/
|
|
19
|
-
import { Agent, fetch as undiciFetch } from "undici";
|
|
20
19
|
import { readBoundedBuffer } from "./sizeGuard.js";
|
|
21
20
|
import { validateAndResolveUrl } from "./ssrfGuard.js";
|
|
22
21
|
const DEFAULT_TIMEOUT_MS = 60_000;
|
|
@@ -30,8 +29,14 @@ const DEFAULT_TIMEOUT_MS = 60_000;
|
|
|
30
29
|
* the OS resolver prefers AAAA, that broke every safeDownload (Replicate /
|
|
31
30
|
* Runway / Kling asset fetches) while plain curl of the same URL succeeded —
|
|
32
31
|
* curl races both families (Happy Eyeballs); a one-address pin cannot.
|
|
32
|
+
*
|
|
33
|
+
* `undici` is imported dynamically (this function is only ever called from
|
|
34
|
+
* the already-async `safeDownload`) so that requiring this module does not
|
|
35
|
+
* force-load undici until a download actually happens. See
|
|
36
|
+
* test/continuous-test-suite-import-cost.ts.
|
|
33
37
|
*/
|
|
34
|
-
function buildPinnedAgent(hostname, addresses) {
|
|
38
|
+
async function buildPinnedAgent(hostname, addresses) {
|
|
39
|
+
const { Agent } = await import("undici");
|
|
35
40
|
const primary = addresses[0];
|
|
36
41
|
if (!primary) {
|
|
37
42
|
throw new Error(`safeFetch: no validated addresses to pin for "${hostname}"`);
|
|
@@ -85,7 +90,11 @@ export async function safeDownload(url, options) {
|
|
|
85
90
|
const { url: validatedUrl, addresses } = await validateAndResolveUrl(url);
|
|
86
91
|
const parsed = new URL(validatedUrl);
|
|
87
92
|
const hostname = parsed.hostname.replace(/^\[|\]$/g, "");
|
|
88
|
-
|
|
93
|
+
// Dynamic import keeps undici off the package's static import graph — it
|
|
94
|
+
// is only needed once a download actually happens. See
|
|
95
|
+
// test/continuous-test-suite-import-cost.ts.
|
|
96
|
+
const { fetch: undiciFetch } = await import("undici");
|
|
97
|
+
const agent = await buildPinnedAgent(hostname, addresses);
|
|
89
98
|
const timeoutCtrl = new AbortController();
|
|
90
99
|
const timeoutId = setTimeout(() => timeoutCtrl.abort(), options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
91
100
|
const composedSignal = options.signal
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juspay/neurolink",
|
|
3
|
-
"version": "12.35.
|
|
3
|
+
"version": "12.35.2",
|
|
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": {
|