@juspay/neurolink 12.34.3 → 12.35.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.
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Exhaustiveness enforcement for the translation IR.
3
+ *
4
+ * A translation layer fails quietly: an unhandled content part or event variant
5
+ * becomes a missing tool call, and the only symptom is the next hop rejecting a
6
+ * request nobody can explain. `assertProxyIRExhaustive` makes that a compile
7
+ * error instead. Passing a value whose type is not `never` does not type-check,
8
+ * so a new IR variant breaks every `switch` that has not handled it.
9
+ *
10
+ * It throws at runtime as well, because the compile-time guarantee only holds for
11
+ * values that really are typed. A wire payload narrowed from `unknown` can reach a
12
+ * default branch at runtime, and failing loudly there beats translating a request
13
+ * with a silently dropped element.
14
+ */
15
+ import type { ProxyIRContentPart, ProxyIRResponseEvent, ProxyIRTerminalOutcome, ProxyIRUnmappableValue } from "../types/index.js";
16
+ /**
17
+ * Refuse an unhandled IR variant.
18
+ *
19
+ * `context` names the switch that failed, since the value alone rarely says which
20
+ * of several exhaustive switches over the same union let it through.
21
+ */
22
+ export declare function assertProxyIRExhaustive(value: never, context: string): never;
23
+ /** Mark a wire value no codec could represent, preserving it for the caller. */
24
+ export declare function proxyIRUnmappable(sourceKind: string, reason: string, raw: unknown): ProxyIRUnmappableValue;
25
+ /** Narrow a collected codec result to the unmappable marker. */
26
+ export declare function isProxyIRUnmappable(value: unknown): value is ProxyIRUnmappableValue;
27
+ /**
28
+ * Name a content part for a diagnostic.
29
+ *
30
+ * This lives in `src` rather than in the suite that asserts on it because
31
+ * `tsconfig.json` excludes `test`: an exhaustive switch written only in a test
32
+ * gives no compile-time guarantee at all. These describers are the shipped
33
+ * consumers that pin every union, so adding an IR variant fails `tsc --noEmit`
34
+ * here before it can reach a codec that silently drops it.
35
+ *
36
+ * Deliberately excludes the payload of text, thinking and tool arguments: a
37
+ * describer's output reaches logs and span attributes, and prompt content does
38
+ * not belong there.
39
+ */
40
+ export declare function describeProxyIRPart(part: ProxyIRContentPart): string;
41
+ /** Name a stream event for a diagnostic, without its payload. */
42
+ export declare function describeProxyIRResponseEvent(event: ProxyIRResponseEvent): string;
43
+ /** Name a terminal outcome, keeping an unmodelled upstream stop reason visible. */
44
+ export declare function describeProxyIRTerminalOutcome(outcome: ProxyIRTerminalOutcome): string;
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Exhaustiveness enforcement for the translation IR.
3
+ *
4
+ * A translation layer fails quietly: an unhandled content part or event variant
5
+ * becomes a missing tool call, and the only symptom is the next hop rejecting a
6
+ * request nobody can explain. `assertProxyIRExhaustive` makes that a compile
7
+ * error instead. Passing a value whose type is not `never` does not type-check,
8
+ * so a new IR variant breaks every `switch` that has not handled it.
9
+ *
10
+ * It throws at runtime as well, because the compile-time guarantee only holds for
11
+ * values that really are typed. A wire payload narrowed from `unknown` can reach a
12
+ * default branch at runtime, and failing loudly there beats translating a request
13
+ * with a silently dropped element.
14
+ */
15
+ /**
16
+ * Refuse an unhandled IR variant.
17
+ *
18
+ * `context` names the switch that failed, since the value alone rarely says which
19
+ * of several exhaustive switches over the same union let it through.
20
+ */
21
+ export function assertProxyIRExhaustive(value, context) {
22
+ throw new Error(`[proxy-ir] ${context} did not handle variant: ${describeProxyIRVariant(value)}`);
23
+ }
24
+ /**
25
+ * Describe a value for the exhaustiveness error without assuming its shape.
26
+ *
27
+ * The parameter is `never` at the call site, so nothing about it can be trusted
28
+ * here: it may be a primitive, or an object with no discriminant at all — and an
29
+ * untagged object is exactly the shape a malformed wire payload takes. Reviewed
30
+ * finding (PR #1826): serializing that object's fields defeated the point of a
31
+ * log/span-safe diagnostic, since the object can carry the same text or tool
32
+ * arguments the typed describers deliberately withhold. The fallback is now a
33
+ * fixed, shape-only marker — never the object's own content — for every
34
+ * untagged case, not only the ones this file happens to call today.
35
+ */
36
+ function describeProxyIRVariant(value) {
37
+ if (value === null || typeof value !== "object") {
38
+ return String(value);
39
+ }
40
+ const record = value;
41
+ for (const discriminant of ["kind", "status", "source", "format"]) {
42
+ const tag = record[discriminant];
43
+ if (typeof tag === "string") {
44
+ return `${discriminant}=${tag}`;
45
+ }
46
+ }
47
+ return "[object without recognized discriminant]";
48
+ }
49
+ /** Mark a wire value no codec could represent, preserving it for the caller. */
50
+ export function proxyIRUnmappable(sourceKind, reason, raw) {
51
+ return { proxyIRUnmappable: true, sourceKind, reason, raw };
52
+ }
53
+ /** Narrow a collected codec result to the unmappable marker. */
54
+ export function isProxyIRUnmappable(value) {
55
+ return (value !== null &&
56
+ typeof value === "object" &&
57
+ value.proxyIRUnmappable === true);
58
+ }
59
+ /**
60
+ * Name a content part for a diagnostic.
61
+ *
62
+ * This lives in `src` rather than in the suite that asserts on it because
63
+ * `tsconfig.json` excludes `test`: an exhaustive switch written only in a test
64
+ * gives no compile-time guarantee at all. These describers are the shipped
65
+ * consumers that pin every union, so adding an IR variant fails `tsc --noEmit`
66
+ * here before it can reach a codec that silently drops it.
67
+ *
68
+ * Deliberately excludes the payload of text, thinking and tool arguments: a
69
+ * describer's output reaches logs and span attributes, and prompt content does
70
+ * not belong there.
71
+ */
72
+ export function describeProxyIRPart(part) {
73
+ switch (part.kind) {
74
+ case "text":
75
+ return `text(${part.text.length}b)`;
76
+ case "thinking":
77
+ return `thinking(${part.text.length}b)`;
78
+ case "image":
79
+ return `image(${part.encoding})`;
80
+ case "tool_call":
81
+ return `tool_call(${part.toolName})`;
82
+ case "tool_result":
83
+ return `tool_result(${part.content.length} parts${part.isError ? ", error" : ""})`;
84
+ case "unmapped":
85
+ return `unmapped(${part.sourceKind}: ${part.reason})`;
86
+ default:
87
+ return assertProxyIRExhaustive(part, "describeProxyIRPart");
88
+ }
89
+ }
90
+ /** Name a stream event for a diagnostic, without its payload. */
91
+ export function describeProxyIRResponseEvent(event) {
92
+ switch (event.kind) {
93
+ case "text_delta":
94
+ return `text_delta(${event.text.length}b)`;
95
+ case "thinking_delta":
96
+ return `thinking_delta(${event.text.length}b)`;
97
+ case "tool_call_start":
98
+ return `tool_call_start(${event.toolName})`;
99
+ case "tool_call_arguments_delta":
100
+ return `tool_call_arguments_delta(${event.partialArguments.length}b)`;
101
+ case "tool_call_done":
102
+ return "tool_call_done";
103
+ case "item_done":
104
+ return "item_done";
105
+ case "usage":
106
+ return "usage";
107
+ case "terminal":
108
+ return `terminal(${describeProxyIRTerminalOutcome(event.outcome)})`;
109
+ default:
110
+ return assertProxyIRExhaustive(event, "describeProxyIRResponseEvent");
111
+ }
112
+ }
113
+ /** Name a terminal outcome, keeping an unmodelled upstream stop reason visible. */
114
+ export function describeProxyIRTerminalOutcome(outcome) {
115
+ switch (outcome.status) {
116
+ case "completed":
117
+ return outcome.finishReason === "other"
118
+ ? `completed:${outcome.rawFinishReason}`
119
+ : `completed:${outcome.finishReason}`;
120
+ case "failed":
121
+ return `failed:${outcome.error.code}`;
122
+ case "incomplete":
123
+ return `incomplete:${outcome.reason}`;
124
+ default:
125
+ return assertProxyIRExhaustive(outcome, "describeProxyIRTerminalOutcome");
126
+ }
127
+ }
@@ -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
- this.initializeTelemetry();
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 || !this.aiRequestCounter) {
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 || !this.aiProviderErrors) {
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 || !this.mcpToolCalls) {
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 || !this.connectionCounter) {
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 || !this.connectionCounter) {
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 || !this.responseTimeHistogram) {
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 || !this.meter) {
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 || !this.meter) {
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) {
@@ -55,6 +55,7 @@ export * from "./providerCatalog.js";
55
55
  export * from "./providerCatalog.generated.js";
56
56
  export * from "./providers.js";
57
57
  export * from "./proxy.js";
58
+ export * from "./proxyIR.js";
58
59
  export * from "./proxyClient.js";
59
60
  export * from "./proxyContext.js";
60
61
  export * from "./rag.js";
@@ -59,6 +59,7 @@ export * from "./providerCatalog.js";
59
59
  export * from "./providerCatalog.generated.js";
60
60
  export * from "./providers.js";
61
61
  export * from "./proxy.js";
62
+ export * from "./proxyIR.js";
62
63
  export * from "./proxyClient.js";
63
64
  export * from "./proxyContext.js";
64
65
  export * from "./rag.js";