@juspay/neurolink 9.42.0 → 9.42.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.
Files changed (84) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/dist/auth/anthropicOAuth.js +12 -0
  3. package/dist/browser/neurolink.min.js +337 -336
  4. package/dist/cli/commands/mcp.d.ts +6 -0
  5. package/dist/cli/commands/mcp.js +188 -184
  6. package/dist/cli/commands/proxy.js +537 -518
  7. package/dist/core/baseProvider.d.ts +6 -1
  8. package/dist/core/baseProvider.js +208 -230
  9. package/dist/core/factory.d.ts +3 -0
  10. package/dist/core/factory.js +138 -188
  11. package/dist/evaluation/pipeline/evaluationPipeline.js +5 -2
  12. package/dist/evaluation/scorers/scorerRegistry.d.ts +3 -0
  13. package/dist/evaluation/scorers/scorerRegistry.js +353 -282
  14. package/dist/lib/auth/anthropicOAuth.js +12 -0
  15. package/dist/lib/core/baseProvider.d.ts +6 -1
  16. package/dist/lib/core/baseProvider.js +208 -230
  17. package/dist/lib/core/factory.d.ts +3 -0
  18. package/dist/lib/core/factory.js +138 -188
  19. package/dist/lib/evaluation/pipeline/evaluationPipeline.js +5 -2
  20. package/dist/lib/evaluation/scorers/scorerRegistry.d.ts +3 -0
  21. package/dist/lib/evaluation/scorers/scorerRegistry.js +353 -282
  22. package/dist/lib/mcp/toolRegistry.d.ts +2 -0
  23. package/dist/lib/mcp/toolRegistry.js +32 -31
  24. package/dist/lib/neurolink.d.ts +38 -0
  25. package/dist/lib/neurolink.js +1858 -1689
  26. package/dist/lib/providers/googleAiStudio.js +0 -5
  27. package/dist/lib/providers/googleVertex.d.ts +10 -0
  28. package/dist/lib/providers/googleVertex.js +436 -444
  29. package/dist/lib/providers/litellm.d.ts +1 -0
  30. package/dist/lib/providers/litellm.js +73 -64
  31. package/dist/lib/providers/ollama.js +17 -4
  32. package/dist/lib/providers/openAI.d.ts +2 -0
  33. package/dist/lib/providers/openAI.js +139 -140
  34. package/dist/lib/proxy/claudeFormat.js +12 -4
  35. package/dist/lib/proxy/oauthFetch.js +298 -318
  36. package/dist/lib/proxy/proxyConfig.js +3 -1
  37. package/dist/lib/proxy/proxyFetch.js +250 -222
  38. package/dist/lib/proxy/requestLogger.js +132 -45
  39. package/dist/lib/proxy/sseInterceptor.js +36 -11
  40. package/dist/lib/server/routes/claudeProxyRoutes.d.ts +10 -1
  41. package/dist/lib/server/routes/claudeProxyRoutes.js +2726 -2272
  42. package/dist/lib/services/server/ai/observability/instrumentation.js +194 -218
  43. package/dist/lib/tasks/backends/bullmqBackend.js +24 -18
  44. package/dist/lib/tasks/store/redisTaskStore.js +23 -16
  45. package/dist/lib/tasks/taskManager.d.ts +2 -0
  46. package/dist/lib/tasks/taskManager.js +100 -5
  47. package/dist/lib/telemetry/telemetryService.js +9 -5
  48. package/dist/lib/types/proxyTypes.d.ts +124 -1
  49. package/dist/lib/utils/providerHealth.d.ts +1 -0
  50. package/dist/lib/utils/providerHealth.js +46 -31
  51. package/dist/lib/utils/providerUtils.js +11 -22
  52. package/dist/mcp/toolRegistry.d.ts +2 -0
  53. package/dist/mcp/toolRegistry.js +32 -31
  54. package/dist/neurolink.d.ts +38 -0
  55. package/dist/neurolink.js +1858 -1689
  56. package/dist/providers/googleAiStudio.js +0 -5
  57. package/dist/providers/googleVertex.d.ts +10 -0
  58. package/dist/providers/googleVertex.js +436 -444
  59. package/dist/providers/litellm.d.ts +1 -0
  60. package/dist/providers/litellm.js +73 -64
  61. package/dist/providers/ollama.js +17 -4
  62. package/dist/providers/openAI.d.ts +2 -0
  63. package/dist/providers/openAI.js +139 -140
  64. package/dist/proxy/claudeFormat.js +12 -4
  65. package/dist/proxy/oauthFetch.js +298 -318
  66. package/dist/proxy/proxyConfig.js +3 -1
  67. package/dist/proxy/proxyFetch.js +250 -222
  68. package/dist/proxy/requestLogger.js +132 -45
  69. package/dist/proxy/sseInterceptor.js +36 -11
  70. package/dist/server/routes/claudeProxyRoutes.d.ts +10 -1
  71. package/dist/server/routes/claudeProxyRoutes.js +2726 -2272
  72. package/dist/services/server/ai/observability/instrumentation.js +194 -218
  73. package/dist/tasks/backends/bullmqBackend.js +24 -18
  74. package/dist/tasks/store/redisTaskStore.js +23 -16
  75. package/dist/tasks/taskManager.d.ts +2 -0
  76. package/dist/tasks/taskManager.js +100 -5
  77. package/dist/telemetry/telemetryService.js +9 -5
  78. package/dist/types/proxyTypes.d.ts +124 -1
  79. package/dist/utils/providerHealth.d.ts +1 -0
  80. package/dist/utils/providerHealth.js +46 -31
  81. package/dist/utils/providerUtils.js +12 -22
  82. package/package.json +3 -2
  83. package/scripts/observability/check-proxy-telemetry.mjs +1 -1
  84. package/scripts/observability/manage-local-openobserve.sh +36 -5
@@ -28,6 +28,9 @@ let otelResolveAttempts = 0;
28
28
  const MAX_RESOLVE_ATTEMPTS = 10;
29
29
  /** Maximum body chunk size emitted to OTLP logs. */
30
30
  const BODY_OTLP_CHUNK_SIZE = 16_000;
31
+ /** Maximum redacted body bytes persisted per capture entry. */
32
+ const MAX_CAPTURED_BODY_BYTES = 1024 * 1024;
33
+ const BODY_TRUNCATION_MARKER = "\n...[TRUNCATED]";
31
34
  const gzip = promisify(gzipCallback);
32
35
  /** Headers whose values must always be redacted. */
33
36
  const SENSITIVE_HEADER_NAMES = new Set([
@@ -262,7 +265,117 @@ function sanitizePhase(phase) {
262
265
  function sha256(value) {
263
266
  return createHash("sha256").update(value).digest("hex");
264
267
  }
265
- async function writeBodyArtifact(entry, redactedHeaders, redactedBody) {
268
+ function utf8ByteLength(value) {
269
+ return Buffer.byteLength(value, "utf8");
270
+ }
271
+ function truncateUtf8String(input, maxBytes, marker = BODY_TRUNCATION_MARKER) {
272
+ const inputBytes = utf8ByteLength(input);
273
+ if (inputBytes <= maxBytes) {
274
+ return { value: input, bytes: inputBytes, truncated: false };
275
+ }
276
+ const markerBytes = utf8ByteLength(marker);
277
+ if (maxBytes <= markerBytes) {
278
+ return { value: marker, bytes: markerBytes, truncated: true };
279
+ }
280
+ let value = "";
281
+ let bytes = 0;
282
+ for (const char of input) {
283
+ const charBytes = utf8ByteLength(char);
284
+ if (bytes + charBytes + markerBytes > maxBytes) {
285
+ break;
286
+ }
287
+ value += char;
288
+ bytes += charBytes;
289
+ }
290
+ const truncatedValue = `${value}${marker}`;
291
+ return {
292
+ value: truncatedValue,
293
+ bytes: utf8ByteLength(truncatedValue),
294
+ truncated: true,
295
+ };
296
+ }
297
+ function splitUtf8StringByBytes(input, maxBytes) {
298
+ if (!input) {
299
+ return [""];
300
+ }
301
+ const chunks = [];
302
+ let currentChunk = "";
303
+ let currentBytes = 0;
304
+ for (const char of input) {
305
+ const charBytes = utf8ByteLength(char);
306
+ if (currentChunk && currentBytes + charBytes > maxBytes) {
307
+ chunks.push(currentChunk);
308
+ currentChunk = char;
309
+ currentBytes = charBytes;
310
+ continue;
311
+ }
312
+ currentChunk += char;
313
+ currentBytes += charBytes;
314
+ }
315
+ if (currentChunk) {
316
+ chunks.push(currentChunk);
317
+ }
318
+ return chunks;
319
+ }
320
+ function prepareRedactedBody(body) {
321
+ const redacted = redactBody(body);
322
+ if (redacted === undefined) {
323
+ return { truncated: false };
324
+ }
325
+ return truncateUtf8String(redacted, MAX_CAPTURED_BODY_BYTES);
326
+ }
327
+ function collectManagedLogFiles(rootDir) {
328
+ const managedFiles = [];
329
+ const walk = (directory) => {
330
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
331
+ const entryPath = join(directory, entry.name);
332
+ if (entry.isDirectory()) {
333
+ walk(entryPath);
334
+ continue;
335
+ }
336
+ const isTopLevelProxyLog = directory === rootDir &&
337
+ /^proxy(?:-attempts|-debug)?-.*\.jsonl$/.test(entry.name);
338
+ const isBodyArtifact = entry.name.endsWith(".json.gz") &&
339
+ entryPath.includes(`${join(rootDir, "bodies")}`);
340
+ if (!isTopLevelProxyLog && !isBodyArtifact) {
341
+ continue;
342
+ }
343
+ try {
344
+ const stat = statSync(entryPath);
345
+ managedFiles.push({
346
+ path: entryPath,
347
+ mtime: stat.mtimeMs,
348
+ size: stat.size,
349
+ });
350
+ }
351
+ catch {
352
+ // Non-fatal
353
+ }
354
+ }
355
+ };
356
+ walk(rootDir);
357
+ return managedFiles;
358
+ }
359
+ function pruneEmptyDirectories(directory, stopAt) {
360
+ if (!existsSync(directory)) {
361
+ return;
362
+ }
363
+ try {
364
+ const entries = readdirSync(directory, { withFileTypes: true });
365
+ for (const entry of entries) {
366
+ if (entry.isDirectory()) {
367
+ pruneEmptyDirectories(join(directory, entry.name), stopAt);
368
+ }
369
+ }
370
+ if (directory !== stopAt && readdirSync(directory).length === 0) {
371
+ rmSync(directory, { recursive: true, force: true });
372
+ }
373
+ }
374
+ catch {
375
+ // Non-fatal
376
+ }
377
+ }
378
+ async function writeBodyArtifact(entry, redactedHeaders, redactedBody, bodyTruncated) {
266
379
  if (!logDir || redactedBody === undefined) {
267
380
  return {};
268
381
  }
@@ -299,9 +412,10 @@ async function writeBodyArtifact(entry, redactedHeaders, redactedBody) {
299
412
  return {
300
413
  bodyPath,
301
414
  bodySha256: sha256(redactedBody),
302
- redactedBodyBytes: Buffer.byteLength(redactedBody, "utf8"),
415
+ redactedBodyBytes: utf8ByteLength(redactedBody),
303
416
  storedFileBytes: compressed.byteLength,
304
417
  redactedBody,
418
+ bodyTruncated,
305
419
  };
306
420
  }
307
421
  function emitOtlpBodyLogRecord(entry, stored) {
@@ -311,9 +425,10 @@ function emitOtlpBodyLogRecord(entry, stored) {
311
425
  return;
312
426
  }
313
427
  const otelLogger = provider.getLogger("neurolink-proxy-bodies", "1.0.0");
314
- const totalChunks = Math.max(1, Math.ceil(stored.redactedBody.length / BODY_OTLP_CHUNK_SIZE));
428
+ const chunks = splitUtf8StringByBytes(stored.redactedBody, BODY_OTLP_CHUNK_SIZE);
429
+ const totalChunks = Math.max(1, chunks.length);
315
430
  for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex++) {
316
- const chunk = stored.redactedBody.slice(chunkIndex * BODY_OTLP_CHUNK_SIZE, (chunkIndex + 1) * BODY_OTLP_CHUNK_SIZE);
431
+ const chunk = chunks[chunkIndex] ?? "";
317
432
  otelLogger.emit({
318
433
  severityNumber: (entry.responseStatus ?? 0) >= 400
319
434
  ? SeverityNumber.WARN
@@ -347,6 +462,9 @@ function emitOtlpBodyLogRecord(entry, stored) {
347
462
  ...(stored.redactedBodyBytes !== undefined && {
348
463
  "body.bytes": stored.redactedBodyBytes,
349
464
  }),
465
+ ...(stored.bodyTruncated !== undefined && {
466
+ "body.truncated": stored.bodyTruncated,
467
+ }),
350
468
  ...(entry.traceId && { "trace.id": entry.traceId }),
351
469
  ...(entry.spanId && { "span.id": entry.spanId }),
352
470
  ...(entry.metadata && {
@@ -370,9 +488,10 @@ export async function logBodyCapture(entry) {
370
488
  ? { traceId: entry.traceId, spanId: entry.spanId }
371
489
  : bridge.getCurrentTraceContext();
372
490
  const redactedHeaders = redactHeaders(entry.headers);
491
+ const preparedBody = prepareRedactedBody(entry.body);
373
492
  let stored = {};
374
493
  try {
375
- stored = await writeBodyArtifact(entry, redactedHeaders, redactBody(entry.body));
494
+ stored = await writeBodyArtifact(entry, redactedHeaders, preparedBody.value, preparedBody.truncated);
376
495
  }
377
496
  catch {
378
497
  // Best-effort artifact persistence; continue with in-memory metadata only.
@@ -396,8 +515,9 @@ export async function logBodyCapture(entry) {
396
515
  bodyPath: stored.bodyPath,
397
516
  bodySha256: stored.bodySha256,
398
517
  observedBodyBytes: entry.bodySize,
399
- redactedBodyBytes: stored.redactedBodyBytes,
518
+ redactedBodyBytes: stored.redactedBodyBytes ?? preparedBody.bytes,
400
519
  storedFileBytes: stored.storedFileBytes,
520
+ bodyTruncated: stored.bodyTruncated ?? preparedBody.truncated,
401
521
  metadata: entry.metadata,
402
522
  };
403
523
  if (traceCtx) {
@@ -497,20 +617,7 @@ export function cleanupLogs(maxAgeDays = 7, maxSizeMb = 500) {
497
617
  }
498
618
  try {
499
619
  const activeLogDir = logDir;
500
- const files = readdirSync(logDir)
501
- .filter((f) => (f.startsWith("proxy-") || f.startsWith("proxy-attempts-")) &&
502
- f.endsWith(".jsonl"))
503
- .map((f) => {
504
- const filePath = join(activeLogDir, f);
505
- const stat = statSync(filePath);
506
- return {
507
- name: f,
508
- path: filePath,
509
- mtime: stat.mtimeMs,
510
- size: stat.size,
511
- };
512
- })
513
- .sort((a, b) => a.mtime - b.mtime); // oldest first
620
+ const files = collectManagedLogFiles(activeLogDir).sort((a, b) => a.mtime - b.mtime); // oldest first
514
621
  const cutoff = Date.now() - maxAgeDays * 24 * 60 * 60 * 1000;
515
622
  let deletedCount = 0;
516
623
  let freedBytes = 0;
@@ -528,34 +635,11 @@ export function cleanupLogs(maxAgeDays = 7, maxSizeMb = 500) {
528
635
  }
529
636
  const bodiesDir = join(logDir, "bodies");
530
637
  if (existsSync(bodiesDir)) {
531
- for (const entry of readdirSync(bodiesDir)) {
532
- const bodyPath = join(bodiesDir, entry);
533
- try {
534
- if (statSync(bodyPath).mtimeMs < cutoff) {
535
- rmSync(bodyPath, { recursive: true, force: true });
536
- }
537
- }
538
- catch {
539
- // Non-fatal
540
- }
541
- }
542
- }
543
- // Include body artifacts in total size calculation
544
- const bodiesDirForSize = join(logDir, "bodies");
545
- let bodiesSize = 0;
546
- if (existsSync(bodiesDirForSize)) {
547
- for (const entry of readdirSync(bodiesDirForSize)) {
548
- try {
549
- bodiesSize += statSync(join(bodiesDirForSize, entry)).size;
550
- }
551
- catch {
552
- // Non-fatal
553
- }
554
- }
638
+ pruneEmptyDirectories(bodiesDir, bodiesDir);
555
639
  }
556
640
  // Pass 2: if total size exceeds maxSizeMb, delete oldest until under limit
557
641
  const maxBytes = maxSizeMb * 1024 * 1024;
558
- let totalSize = remaining.reduce((sum, f) => sum + f.size, 0) + bodiesSize;
642
+ let totalSize = remaining.reduce((sum, f) => sum + f.size, 0);
559
643
  while (totalSize > maxBytes && remaining.length > 0) {
560
644
  const oldest = remaining.shift();
561
645
  if (!oldest) {
@@ -566,6 +650,9 @@ export function cleanupLogs(maxAgeDays = 7, maxSizeMb = 500) {
566
650
  deletedCount++;
567
651
  freedBytes += oldest.size;
568
652
  }
653
+ if (existsSync(bodiesDir)) {
654
+ pruneEmptyDirectories(bodiesDir, bodiesDir);
655
+ }
569
656
  if (deletedCount > 0) {
570
657
  logger.info(`[proxy] log cleanup: deleted ${deletedCount} file(s), freed ${(freedBytes / 1024 / 1024).toFixed(1)} MB`);
571
658
  }
@@ -91,31 +91,52 @@ function createAccumulator(captureRawText) {
91
91
  eventLogTruncated: false,
92
92
  };
93
93
  }
94
- function truncateString(input, maxBytes) {
95
- if (input.length <= maxBytes) {
94
+ function utf8ByteLength(input) {
95
+ return Buffer.byteLength(input, "utf8");
96
+ }
97
+ function truncateUtf8String(input, maxBytes) {
98
+ if (utf8ByteLength(input) <= maxBytes) {
96
99
  return input;
97
100
  }
98
- return `${input.slice(0, maxBytes)}${TRUNCATION_MARKER}`;
101
+ const markerBytes = utf8ByteLength(TRUNCATION_MARKER);
102
+ if (maxBytes <= 0 || maxBytes < markerBytes) {
103
+ return "";
104
+ }
105
+ let output = "";
106
+ let usedBytes = 0;
107
+ for (const char of input) {
108
+ const charBytes = utf8ByteLength(char);
109
+ if (usedBytes + charBytes + markerBytes > maxBytes) {
110
+ break;
111
+ }
112
+ output += char;
113
+ usedBytes += charBytes;
114
+ }
115
+ return `${output}${TRUNCATION_MARKER}`;
116
+ }
117
+ function truncateString(input, maxBytes) {
118
+ return truncateUtf8String(input, maxBytes);
99
119
  }
100
120
  function appendCappedFragment(current, fragment, currentBytes, maxBytes) {
121
+ const fragmentBytes = utf8ByteLength(fragment);
101
122
  if (currentBytes >= maxBytes) {
102
123
  return {
103
124
  value: current && current.endsWith(TRUNCATION_MARKER)
104
125
  ? current
105
126
  : `${current ?? ""}${TRUNCATION_MARKER}`,
106
- nextBytes: currentBytes + fragment.length,
127
+ nextBytes: currentBytes + fragmentBytes,
107
128
  };
108
129
  }
109
130
  const remainingBytes = maxBytes - currentBytes;
110
- const nextBytes = currentBytes + fragment.length;
111
- if (fragment.length <= remainingBytes) {
131
+ const nextBytes = currentBytes + fragmentBytes;
132
+ if (fragmentBytes <= remainingBytes) {
112
133
  return {
113
134
  value: `${current ?? ""}${fragment}`,
114
135
  nextBytes,
115
136
  };
116
137
  }
117
138
  return {
118
- value: `${current ?? ""}${fragment.slice(0, remainingBytes)}${TRUNCATION_MARKER}`,
139
+ value: `${current ?? ""}${truncateUtf8String(fragment, remainingBytes)}`,
119
140
  nextBytes,
120
141
  };
121
142
  }
@@ -129,15 +150,19 @@ function appendRawTextChunk(acc, chunk) {
129
150
  acc.rawTextTruncated = true;
130
151
  return;
131
152
  }
132
- if (chunk.length <= remainingBytes) {
153
+ const chunkBytes = utf8ByteLength(chunk);
154
+ if (chunkBytes <= remainingBytes) {
133
155
  acc.rawTextChunks.push(chunk);
134
- acc.rawTextBytes += chunk.length;
156
+ acc.rawTextBytes += chunkBytes;
135
157
  return;
136
158
  }
137
- acc.rawTextChunks.push(chunk.slice(0, remainingBytes), TRUNCATION_MARKER);
159
+ acc.rawTextChunks.push(truncateUtf8String(chunk, remainingBytes));
138
160
  acc.rawTextBytes = MAX_RAW_TEXT_BYTES;
139
161
  acc.rawTextTruncated = true;
140
162
  }
163
+ function getBlockContentBytes(block) {
164
+ return utf8ByteLength(block.text ?? block.thinking ?? block.toolInput ?? "");
165
+ }
141
166
  function finalize(acc) {
142
167
  const totalTokens = acc.inputTokens + acc.outputTokens;
143
168
  return {
@@ -199,7 +224,7 @@ function processContentBlockStart(acc, parsed) {
199
224
  entry.toolInput = "";
200
225
  }
201
226
  acc.contentBlocks.push(entry);
202
- acc.blockByteCounts.set(index, 0);
227
+ acc.blockByteCounts.set(index, getBlockContentBytes(entry));
203
228
  }
204
229
  function processContentBlockDelta(acc, parsed) {
205
230
  const index = parsed.index ?? 0;
@@ -12,7 +12,11 @@
12
12
  import type { ModelRouter } from "../../proxy/modelRouter.js";
13
13
  import type { ParsedClaudeRequest } from "../../types/index.js";
14
14
  import type { RouteGroup } from "../types.js";
15
- export type { ClaudeProxyDeps } from "../../types/index.js";
15
+ type ProxyTranslationAttempt = {
16
+ provider?: string;
17
+ model?: string;
18
+ label: string;
19
+ };
16
20
  /**
17
21
  * Create Claude-compatible proxy routes.
18
22
  *
@@ -40,6 +44,10 @@ export declare function buildProxyFallbackOptions(parsed: ParsedClaudeRequest, o
40
44
  provider?: string;
41
45
  model?: string;
42
46
  }): Record<string, unknown>;
47
+ export declare function buildProxyTranslationAttempts(primary: {
48
+ provider: string;
49
+ model?: string;
50
+ }, modelRouter?: ModelRouter, parsed?: Pick<ParsedClaudeRequest, "images" | "thinkingConfig">): ProxyTranslationAttempt[];
43
51
  /**
44
52
  * Detect transient upstream failures that should trigger account/provider failover.
45
53
  *
@@ -47,3 +55,4 @@ export declare function buildProxyFallbackOptions(parsed: ParsedClaudeRequest, o
47
55
  * carry transient HTML responses (e.g. 520 pages) inside `error.message`.
48
56
  */
49
57
  export declare function isTransientHttpFailure(status: number, errBody: string): boolean;
58
+ export {};