@juspay/neurolink 12.17.0 → 12.17.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.
Files changed (71) hide show
  1. package/CHANGELOG.md +2 -7
  2. package/dist/browser/neurolink.min.js +422 -413
  3. package/dist/cli/commands/proxy.js +455 -259
  4. package/dist/constants/contextWindows.d.ts +2 -0
  5. package/dist/constants/contextWindows.js +4 -0
  6. package/dist/providers/anthropic/client.js +15 -5
  7. package/dist/proxy/anthropicBudgetResponse.d.ts +3 -0
  8. package/dist/proxy/anthropicBudgetResponse.js +153 -0
  9. package/dist/proxy/bodyCaptureProcessing.d.ts +1 -0
  10. package/dist/proxy/bodyCaptureProcessing.js +23 -4
  11. package/dist/proxy/bodyCaptureWorker.js +20 -2
  12. package/dist/proxy/codexFallback.d.ts +10 -0
  13. package/dist/proxy/codexFallback.js +108 -24
  14. package/dist/proxy/codexUsage.js +2 -0
  15. package/dist/proxy/globalInstaller.d.ts +32 -1
  16. package/dist/proxy/globalInstaller.js +386 -3
  17. package/dist/proxy/otelLogSink.d.ts +3 -3
  18. package/dist/proxy/otelLogSink.js +137 -144
  19. package/dist/proxy/otlpLogTransport.d.ts +3 -0
  20. package/dist/proxy/otlpLogTransport.js +334 -0
  21. package/dist/proxy/proxyActivity.d.ts +22 -2
  22. package/dist/proxy/proxyActivity.js +114 -9
  23. package/dist/proxy/proxyAnalysis.js +82 -52
  24. package/dist/proxy/proxyClientCancellation.d.ts +7 -0
  25. package/dist/proxy/proxyClientCancellation.js +23 -0
  26. package/dist/proxy/proxyContextPreflight.d.ts +25 -0
  27. package/dist/proxy/proxyContextPreflight.js +337 -0
  28. package/dist/proxy/proxyFailureDetails.d.ts +19 -0
  29. package/dist/proxy/proxyFailureDetails.js +96 -0
  30. package/dist/proxy/proxyLifecycle.js +7 -0
  31. package/dist/proxy/proxyRouteBodyCapture.d.ts +13 -0
  32. package/dist/proxy/proxyRouteBodyCapture.js +112 -0
  33. package/dist/proxy/proxyTokenBudget.d.ts +40 -0
  34. package/dist/proxy/proxyTokenBudget.js +523 -0
  35. package/dist/proxy/proxyTracer.d.ts +3 -2
  36. package/dist/proxy/proxyTracer.js +46 -20
  37. package/dist/proxy/proxyTranslationEngine.d.ts +8 -10
  38. package/dist/proxy/proxyTranslationEngine.js +736 -376
  39. package/dist/proxy/requestLogger.d.ts +1 -0
  40. package/dist/proxy/requestLogger.js +107 -5
  41. package/dist/proxy/restartControl.js +9 -4
  42. package/dist/proxy/rollingWorkerProcess.js +65 -0
  43. package/dist/proxy/rollingWorkerSupervisor.js +7 -0
  44. package/dist/proxy/updateCoordinator.d.ts +14 -1
  45. package/dist/proxy/updateCoordinator.js +57 -2
  46. package/dist/server/routes/claudeProxyRoutes.d.ts +4 -5
  47. package/dist/server/routes/claudeProxyRoutes.js +809 -577
  48. package/dist/server/routes/codexProxyRoutes.js +191 -9
  49. package/dist/server/routes/openaiProxyRoutes.d.ts +1 -1
  50. package/dist/server/routes/openaiProxyRoutes.js +261 -69
  51. package/dist/types/cli.d.ts +3 -0
  52. package/dist/types/codex.d.ts +5 -1
  53. package/dist/types/index.d.ts +2 -0
  54. package/dist/types/index.js +2 -0
  55. package/dist/types/proxy.d.ts +148 -3
  56. package/dist/types/proxy.js +0 -15
  57. package/dist/types/proxyBudget.d.ts +74 -0
  58. package/dist/types/proxyBudget.js +1 -0
  59. package/dist/types/proxyContext.d.ts +35 -0
  60. package/dist/types/proxyContext.js +1 -0
  61. package/dist/utils/toolChoice.d.ts +19 -0
  62. package/dist/utils/toolChoice.js +39 -0
  63. package/docs/assets/dashboards/README.md +37 -0
  64. package/docs/assets/dashboards/neurolink-proxy-observability-dashboard.json +197 -89
  65. package/docs-site/static/search-index.json +1 -1
  66. package/package.json +4 -1
  67. package/scripts/observability/NATIVE-DURABLE.md +63 -0
  68. package/scripts/observability/otel-collector.proxy-native-durable.yaml +93 -0
  69. package/scripts/observability/proxy-telemetry-backend.mjs +32 -5
  70. package/scripts/observability/proxy-telemetry-check.mjs +396 -61
  71. package/scripts/observability/query-proxy-history.mjs +34 -5
@@ -5,7 +5,7 @@ import { getProxyRequestTraceContext, proxyLogContext, } from "./proxyTraceConte
5
5
  import { setImmediate as yieldToRequests } from "node:timers/promises";
6
6
  import { SeverityNumber } from "@opentelemetry/api-logs";
7
7
  import { ExportResultCode } from "@opentelemetry/core";
8
- import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-http";
8
+ import { createProxyOtlpLogTransport } from "./otlpLogTransport.js";
9
9
  import { resourceFromAttributes } from "@opentelemetry/resources";
10
10
  import { BatchLogRecordProcessor, LoggerProvider, } from "@opentelemetry/sdk-logs";
11
11
  import { sanitizeForLog } from "../utils/logSanitize.js";
@@ -15,7 +15,7 @@ let restoreConsole;
15
15
  const queues = [];
16
16
  const bodyPublications = new Map();
17
17
  const bodyPublicationDeadlines = new WeakMap();
18
- let bodyPublicationChain = Promise.resolve();
18
+ const bodyPublicationOperations = new Set();
19
19
  let shuttingDown = false;
20
20
  const BODY_OTLP_CHUNK_SIZE = 128 * 1024;
21
21
  const OTLP_EXPORT_TIMEOUT_MS = 30_000;
@@ -41,6 +41,8 @@ export function isProxyOtelOnly() {
41
41
  /** Reserve capacity including exports in flight, independently for metadata and bodies. */
42
42
  function createTrackedProcessor(url, capacity, kind) {
43
43
  const unsettled = new Set();
44
+ const enqueuedAt = new WeakMap();
45
+ const capacityWaiters = new Set();
44
46
  const flushWaiters = new Set();
45
47
  const state = {
46
48
  attempted: 0,
@@ -99,12 +101,17 @@ function createTrackedProcessor(url, capacity, kind) {
99
101
  }
100
102
  });
101
103
  };
102
- const transport = new OTLPLogExporter({
103
- url,
104
- timeoutMillis: OTLP_EXPORT_TIMEOUT_MS,
105
- });
104
+ const transport = createProxyOtlpLogTransport(url, OTLP_EXPORT_TIMEOUT_MS);
106
105
  const exporter = {
107
106
  export(records, callback) {
107
+ const exportStartedAt = performance.now();
108
+ for (const record of records) {
109
+ const id = record.attributes["body.capture_id"];
110
+ const publication = typeof id === "string" ? bodyPublications.get(id) : undefined;
111
+ if (publication) {
112
+ publication.maxChunkQueueWaitMs = Math.max(publication.maxChunkQueueWaitMs ?? 0, exportStartedAt - (enqueuedAt.get(record) ?? exportStartedAt));
113
+ }
114
+ }
108
115
  let settled = false;
109
116
  const deadline = setTimeout(() => settle({
110
117
  code: ExportResultCode.FAILED,
@@ -133,6 +140,7 @@ function createTrackedProcessor(url, capacity, kind) {
133
140
  const id = record.attributes?.["body.capture_id"];
134
141
  const publication = typeof id === "string" ? bodyPublications.get(id) : undefined;
135
142
  if (publication) {
143
+ publication.maxChunkExportMs = Math.max(publication.maxChunkExportMs ?? 0, performance.now() - exportStartedAt);
136
144
  const activeDeadline = bodyPublicationDeadlines.get(publication);
137
145
  if (result.code === ExportResultCode.SUCCESS &&
138
146
  (activeDeadline === undefined || Date.now() < activeDeadline)) {
@@ -144,6 +152,9 @@ function createTrackedProcessor(url, capacity, kind) {
144
152
  publication.notify?.();
145
153
  }
146
154
  }
155
+ for (const notify of capacityWaiters) {
156
+ notify();
157
+ }
147
158
  for (const notify of flushWaiters) {
148
159
  notify();
149
160
  }
@@ -164,7 +175,7 @@ function createTrackedProcessor(url, capacity, kind) {
164
175
  const batch = new BatchLogRecordProcessor(exporter, {
165
176
  maxQueueSize: capacity,
166
177
  maxExportBatchSize: 64,
167
- scheduledDelayMillis: 1000,
178
+ scheduledDelayMillis: kind === "bodies" ? 25 : 1000,
168
179
  exportTimeoutMillis: OTLP_EXPORT_CALLBACK_DEADLINE_MS,
169
180
  });
170
181
  const processor = {
@@ -188,6 +199,7 @@ function createTrackedProcessor(url, capacity, kind) {
188
199
  state.submitted++;
189
200
  state.outstanding++;
190
201
  unsettled.add(record);
202
+ enqueuedAt.set(record, performance.now());
191
203
  state.highWaterOutstanding = Math.max(state.highWaterOutstanding, state.outstanding);
192
204
  batch.onEmit(record);
193
205
  },
@@ -218,12 +230,32 @@ function createTrackedProcessor(url, capacity, kind) {
218
230
  }
219
231
  },
220
232
  };
221
- return { state, processor, capacity, kind };
233
+ /** Wait for a shared queue slot without force-flushing or owning another capture. */
234
+ const waitForCapacity = (deadline) => new Promise((resolve) => {
235
+ const timer = setTimeout(() => finish(false), Math.max(0, deadline - Date.now()));
236
+ timer.unref?.();
237
+ const finish = (available) => {
238
+ clearTimeout(timer);
239
+ capacityWaiters.delete(check);
240
+ resolve(available);
241
+ };
242
+ const check = () => {
243
+ if (Date.now() >= deadline) {
244
+ finish(false);
245
+ }
246
+ else if (state.outstanding < capacity) {
247
+ finish(true);
248
+ }
249
+ };
250
+ capacityWaiters.add(check);
251
+ check();
252
+ });
253
+ return { state, processor, capacity, kind, waitForCapacity };
222
254
  }
223
255
  /**
224
- * Own a whole capture within byte/count bounds, then pace its chunks by actual
225
- * export callbacks. SDK forceFlush alone does not await an automatic export
226
- * already in flight. Serial publication prevents bursts from dropping tails.
256
+ * Publish bounded captures concurrently into a shared batch queue. Transport
257
+ * callbacks settle each chunk once; a flush is a process fence, not a per-body
258
+ * network round trip. Metadata has a separate queue and transport.
227
259
  */
228
260
  export async function publishProxyOtelBody(captureId, body, emit) {
229
261
  bodyDelivery.attempted++;
@@ -258,7 +290,7 @@ export async function publishProxyOtelBody(captureId, body, emit) {
258
290
  };
259
291
  bodyPublications.set(captureId, progress);
260
292
  let deferredRelease;
261
- const operation = bodyPublicationChain.then(async () => {
293
+ const operation = (async () => {
262
294
  const queue = queues.find((candidate) => candidate.kind === "bodies");
263
295
  if (!queue) {
264
296
  return {
@@ -269,14 +301,39 @@ export async function publishProxyOtelBody(captureId, body, emit) {
269
301
  reason: "body_exporter_unavailable",
270
302
  };
271
303
  }
272
- // A capture owns bounded publication memory while waiting behind earlier
273
- // captures. Start its active deadline only when it reaches the head of
274
- // the chain; healthy queued captures must not expire solely because an
275
- // earlier export used its transport timeout.
304
+ const startedAt = performance.now();
276
305
  const deadline = Date.now() + BODY_PUBLICATION_ACTIVE_DEADLINE_MS;
277
306
  bodyPublicationDeadlines.set(progress, deadline);
278
307
  const chunks = splitUtf8StringByBytes(body, BODY_OTLP_CHUNK_SIZE);
279
- const awaitSettlement = () => new Promise((resolve) => {
308
+ let capacityWaitMs = 0;
309
+ let reason;
310
+ try {
311
+ for (let index = 0; index < chunks.length; index++) {
312
+ while (queue.state.outstanding >= queue.capacity) {
313
+ const waitingAt = performance.now();
314
+ const available = await queue.waitForCapacity(deadline);
315
+ capacityWaitMs += performance.now() - waitingAt;
316
+ if (!available) {
317
+ reason = "body_publication_deadline";
318
+ break;
319
+ }
320
+ }
321
+ if (Date.now() >= deadline) {
322
+ reason = "body_publication_deadline";
323
+ break;
324
+ }
325
+ emit(chunks[index], index, chunks.length);
326
+ // Multiple waiters can wake together; the next iteration checks the
327
+ // shared capacity synchronously before emitting its next chunk.
328
+ if (index % 4 === 3) {
329
+ await yieldToRequests();
330
+ }
331
+ }
332
+ }
333
+ catch {
334
+ reason = "body_publication_failed";
335
+ }
336
+ const settlement = new Promise((resolve) => {
280
337
  const check = () => {
281
338
  if (progress.acknowledged + progress.unconfirmed + progress.dropped >=
282
339
  progress.emitted) {
@@ -287,123 +344,55 @@ export async function publishProxyOtelBody(captureId, body, emit) {
287
344
  progress.notify = check;
288
345
  check();
289
346
  });
290
- const resultFromProgress = (snapshot, reason) => {
291
- const pending = Math.max(0, snapshot.emitted -
292
- snapshot.acknowledged -
293
- snapshot.unconfirmed -
294
- snapshot.dropped);
295
- const unconfirmedChunks = snapshot.unconfirmed + pending;
296
- const status = snapshot.emitted === 0 && chunks.length > 0
297
- ? "rejected"
298
- : snapshot.dropped || snapshot.emitted !== chunks.length
299
- ? "partial"
300
- : unconfirmedChunks
301
- ? "export_unconfirmed"
302
- : "transport_acknowledged";
303
- return {
304
- status,
305
- expectedChunks: chunks.length,
306
- acknowledgedChunks: snapshot.acknowledged,
307
- unconfirmedChunks,
308
- droppedChunks: snapshot.dropped,
309
- notSubmittedChunks: chunks.length - snapshot.emitted,
310
- ...(reason ? { reason } : {}),
311
- };
312
- };
313
- const settleCurrentBatch = async () => {
314
- const currentProgress = () => ({
315
- acknowledged: progress.acknowledged,
316
- unconfirmed: progress.unconfirmed,
317
- dropped: progress.dropped,
318
- emitted: progress.emitted,
319
- });
320
- const settlement = queue.processor.forceFlush().then(awaitSettlement);
321
- const remainingMs = deadline - Date.now();
322
- if (remainingMs <= 0) {
323
- deferredRelease = settlement.catch(() => undefined);
324
- return resultFromProgress(currentProgress(), "body_publication_deadline");
325
- }
326
- let timer;
327
- const completed = await Promise.race([
328
- settlement.then(() => true),
329
- new Promise((resolve) => {
330
- timer = setTimeout(() => resolve(false), remainingMs);
331
- timer.unref?.();
332
- }),
333
- ]).finally(() => clearTimeout(timer));
334
- if (!completed || Date.now() >= deadline) {
335
- if (!completed) {
336
- // The exporter callback owns the submitted records until it settles.
337
- // Keep the capture identity and memory reservation alive meanwhile.
338
- deferredRelease = settlement.catch(() => undefined);
339
- }
340
- return resultFromProgress(currentProgress(), "body_publication_deadline");
341
- }
342
- return undefined;
343
- };
344
- let reason;
345
- try {
346
- for (let offset = 0; offset < chunks.length; offset += 64) {
347
- if (Date.now() >= deadline) {
348
- reason = "body_publication_deadline";
349
- break;
350
- }
351
- // Ordinary body records from an external logger may share this queue.
352
- // Wait for them before admitting any part of this batch.
353
- while (queue.state.outstanding > queue.capacity - 64) {
354
- const deadlineResult = await settleCurrentBatch();
355
- if (deadlineResult) {
356
- return deadlineResult;
357
- }
358
- }
359
- for (let i = offset; i < Math.min(offset + 64, chunks.length); i++) {
360
- emit(chunks[i], i, chunks.length);
361
- }
362
- const deadlineResult = await settleCurrentBatch();
363
- if (deadlineResult) {
364
- return deadlineResult;
365
- }
366
- if (progress.unconfirmed || progress.dropped) {
367
- reason = "body_export_unconfirmed";
368
- break;
369
- }
370
- await yieldToRequests();
371
- }
347
+ let timer;
348
+ const completed = await Promise.race([
349
+ settlement.then(() => true),
350
+ new Promise((resolve) => {
351
+ timer = setTimeout(() => resolve(false), Math.max(0, deadline - Date.now()));
352
+ timer.unref?.();
353
+ }),
354
+ ]).finally(() => clearTimeout(timer));
355
+ if (!completed) {
356
+ // Keep identity/bytes until the real callback or callback deadline settles
357
+ // submitted chunks. Late success cannot rewrite an uncertainty result.
358
+ deferredRelease = settlement;
372
359
  }
373
- catch (error) {
374
- reason =
375
- error instanceof Error &&
376
- error.message === "body_publication_deadline"
377
- ? "body_publication_deadline"
378
- : "body_publication_failed";
379
- // Do not let error cleanup hold the serial publication chain past this
380
- // capture's deadline. Exporter callbacks still own submitted chunks, so
381
- // retain the capture identity and memory reservation until they settle.
382
- const settlement = queue.processor
383
- .forceFlush()
384
- .catch(() => undefined)
385
- .then(awaitSettlement);
386
- const remainingMs = deadline - Date.now();
387
- if (remainingMs <= 0) {
388
- deferredRelease = settlement;
389
- }
390
- else {
391
- let timer;
392
- const completed = await Promise.race([
393
- settlement.then(() => true),
394
- new Promise((resolve) => {
395
- timer = setTimeout(() => resolve(false), remainingMs);
396
- timer.unref?.();
397
- }),
398
- ]).finally(() => clearTimeout(timer));
399
- if (!completed) {
400
- deferredRelease = settlement;
401
- }
402
- }
360
+ if (!completed || Date.now() >= deadline) {
361
+ reason ??= "body_publication_deadline";
403
362
  }
404
- return resultFromProgress(progress, reason);
405
- });
406
- bodyPublicationChain = operation.then(() => undefined, () => undefined);
363
+ if (!reason && progress.unconfirmed) {
364
+ reason = "body_export_unconfirmed";
365
+ }
366
+ else if (!reason && progress.dropped) {
367
+ reason = "body_export_queue_full";
368
+ }
369
+ const pending = Math.max(0, progress.emitted -
370
+ progress.acknowledged -
371
+ progress.unconfirmed -
372
+ progress.dropped);
373
+ const unconfirmedChunks = progress.unconfirmed + pending;
374
+ const status = progress.emitted === 0
375
+ ? "rejected"
376
+ : progress.dropped || progress.emitted !== chunks.length
377
+ ? "partial"
378
+ : unconfirmedChunks
379
+ ? "export_unconfirmed"
380
+ : "transport_acknowledged";
381
+ return {
382
+ status,
383
+ expectedChunks: chunks.length,
384
+ acknowledgedChunks: progress.acknowledged,
385
+ unconfirmedChunks,
386
+ droppedChunks: progress.dropped,
387
+ notSubmittedChunks: chunks.length - progress.emitted,
388
+ publicationMs: performance.now() - startedAt,
389
+ capacityWaitMs,
390
+ maxChunkQueueWaitMs: progress.maxChunkQueueWaitMs ?? 0,
391
+ maxChunkExportMs: progress.maxChunkExportMs ?? 0,
392
+ ...(reason ? { reason } : {}),
393
+ };
394
+ })();
395
+ bodyPublicationOperations.add(operation);
407
396
  try {
408
397
  const result = await operation;
409
398
  if (result.status === "transport_acknowledged") {
@@ -421,13 +410,14 @@ export async function publishProxyOtelBody(captureId, body, emit) {
421
410
  return result;
422
411
  }
423
412
  finally {
413
+ bodyPublicationOperations.delete(operation);
424
414
  const release = () => {
425
415
  bodyPublications.delete(captureId);
426
416
  bodyDelivery.pending--;
427
417
  bodyDelivery.pendingBytes -= bytes;
428
418
  };
429
419
  if (deferredRelease) {
430
- void deferredRelease.finally(release);
420
+ void deferredRelease.then(release, release);
431
421
  }
432
422
  else {
433
423
  release();
@@ -449,16 +439,19 @@ export function initializeProxyOtelLogs(role = "worker") {
449
439
  if (!endpoint) {
450
440
  throw new Error("OTel-only proxy logging requires an OTLP endpoint");
451
441
  }
452
- const url = new URL(endpoint);
453
- if (!["http:", "https:"].includes(url.protocol)) {
454
- throw new Error("Proxy OTLP logs endpoint must use HTTP or HTTPS");
455
- }
456
- const loopback = ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
457
- if (url.protocol === "http:" && !loopback) {
458
- throw new Error("Proxy OTLP logs require HTTPS for non-loopback collectors");
442
+ const bodyEndpoint = process.env.NEUROLINK_PROXY_OTLP_BODIES_ENDPOINT ?? endpoint;
443
+ for (const target of [endpoint, bodyEndpoint]) {
444
+ const url = new URL(target);
445
+ if (!["http:", "https:"].includes(url.protocol)) {
446
+ throw new Error("Proxy OTLP logs endpoint must use HTTP or HTTPS");
447
+ }
448
+ const loopback = ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
449
+ if (url.protocol === "http:" && !loopback) {
450
+ throw new Error("Proxy OTLP logs require HTTPS for non-loopback collectors");
451
+ }
459
452
  }
460
453
  const metadata = createTrackedProcessor(endpoint, 2048, "metadata");
461
- const bodies = createTrackedProcessor(endpoint, 256, "bodies");
454
+ const bodies = createTrackedProcessor(bodyEndpoint, 256, "bodies");
462
455
  queues.push(metadata, bodies);
463
456
  provider = new LoggerProvider({
464
457
  resource: resourceFromAttributes({
@@ -594,13 +587,13 @@ export function getProxyOtelLogSnapshot() {
594
587
  }
595
588
  /** Bounded provider flush belongs after final request and lifecycle publication. */
596
589
  export async function flushProxyOtelLogs() {
597
- await bodyPublicationChain;
590
+ await Promise.allSettled([...bodyPublicationOperations]);
598
591
  await provider?.forceFlush();
599
592
  }
600
593
  /** Release this process's exporter and restore console ownership. */
601
594
  export async function shutdownProxyOtelLogs() {
602
595
  shuttingDown = true;
603
- await bodyPublicationChain;
596
+ await Promise.allSettled([...bodyPublicationOperations]);
604
597
  restoreConsole?.();
605
598
  restoreConsole = undefined;
606
599
  await provider?.shutdown();
@@ -608,7 +601,7 @@ export async function shutdownProxyOtelLogs() {
608
601
  queues.length = 0;
609
602
  invalidRecords = 0;
610
603
  bodyPublications.clear();
611
- bodyPublicationChain = Promise.resolve();
604
+ bodyPublicationOperations.clear();
612
605
  shuttingDown = false;
613
606
  for (const key of [
614
607
  "attempted",
@@ -0,0 +1,3 @@
1
+ import type { LogRecordExporter } from "@opentelemetry/sdk-logs";
2
+ /** Both proxy queues use this transport; success is not backend persistence. */
3
+ export declare function createProxyOtlpLogTransport(endpoint: string, timeoutMillis: number): LogRecordExporter;