@juspay/neurolink 12.20.0 → 12.20.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 (44) hide show
  1. package/CHANGELOG.md +2 -6
  2. package/dist/browser/neurolink.min.js +400 -400
  3. package/dist/cli/commands/proxy.js +171 -25
  4. package/dist/cli/commands/telemetry.d.ts +4 -1
  5. package/dist/cli/commands/telemetry.js +113 -0
  6. package/dist/cli/utils/nativeTelemetry.d.ts +19 -0
  7. package/dist/cli/utils/nativeTelemetry.js +322 -0
  8. package/dist/proxy/bodyCapturePolicy.d.ts +6 -0
  9. package/dist/proxy/bodyCapturePolicy.js +132 -0
  10. package/dist/proxy/codexFallback.d.ts +27 -9
  11. package/dist/proxy/codexFallback.js +84 -13
  12. package/dist/proxy/openaiFormat.js +8 -1
  13. package/dist/proxy/otelLogSink.d.ts +11 -1
  14. package/dist/proxy/otelLogSink.js +174 -27
  15. package/dist/proxy/otlpLogTransport.d.ts +4 -2
  16. package/dist/proxy/otlpLogTransport.js +22 -7
  17. package/dist/proxy/requestLogger.js +11 -3
  18. package/dist/proxy/restartControl.js +18 -3
  19. package/dist/proxy/rollingProxyServer.js +48 -1
  20. package/dist/proxy/rollingWorkerProcess.js +87 -10
  21. package/dist/proxy/rollingWorkerProtocol.js +4 -2
  22. package/dist/proxy/rollingWorkerSupervisor.js +47 -10
  23. package/dist/proxy/socketWorkerRuntime.js +90 -4
  24. package/dist/proxy/updateChecker.d.ts +3 -3
  25. package/dist/proxy/updateChecker.js +16 -9
  26. package/dist/proxy/updateCoordinator.d.ts +10 -1
  27. package/dist/proxy/updateCoordinator.js +68 -1
  28. package/dist/proxy/updateState.d.ts +2 -0
  29. package/dist/proxy/updateState.js +15 -0
  30. package/dist/proxy/vertexAnthropicFallback.d.ts +17 -1
  31. package/dist/proxy/vertexAnthropicFallback.js +25 -1
  32. package/dist/server/routes/claudeProxyRoutes.d.ts +17 -0
  33. package/dist/server/routes/claudeProxyRoutes.js +53 -5
  34. package/dist/types/cli.d.ts +18 -0
  35. package/dist/types/providers.d.ts +8 -0
  36. package/dist/types/proxy.d.ts +117 -7
  37. package/dist/utils/anthropicCacheBreakpoints.d.ts +22 -1
  38. package/dist/utils/anthropicCacheBreakpoints.js +113 -0
  39. package/dist/utils/pricing.js +69 -2
  40. package/docs-site/static/search-index.json +1 -1
  41. package/package.json +1 -1
  42. package/scripts/observability/NATIVE-DURABLE.md +68 -0
  43. package/scripts/observability/otel-collector.proxy-native-durable.yaml +2 -2
  44. package/scripts/observability/proxy-telemetry-check.mjs +135 -50
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/neurolink",
3
- "version": "12.20.0",
3
+ "version": "12.20.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": {
@@ -5,6 +5,74 @@
5
5
  Application logs remain OTLP-only. Collector retry storage writes a bounded
6
6
  queue to disk so records accepted into that queue survive a collector restart.
7
7
 
8
+ ## Prepare an existing native installation
9
+
10
+ The shipped CLI stages a migration from the existing collector. It preserves
11
+ the metadata stream (for example, `curator`), backend endpoint and authorization,
12
+ metadata listener, health listener and metrics port. Its new body stream defaults
13
+ to `<metadata stream>_bodies`. Other operator customizations need review against
14
+ the staged standard profile, particularly filters, TLS, memory limits and
15
+ exporter settings. The original collector YAML and all service settings stay
16
+ unchanged.
17
+
18
+ ```sh
19
+ neurolink telemetry native-prepare \
20
+ --collector-config "$HOME/.neurolink/telemetry-native/config/collector.yaml" \
21
+ --output "$HOME/neurolink-native-migration-review" \
22
+ --queue-directory "$HOME/.neurolink/telemetry-native/queue" \
23
+ --compaction-directory "$HOME/.neurolink/telemetry-native/compact" \
24
+ --disk-quota-mib 1024 \
25
+ --metadata-retention-days 14 \
26
+ --body-retention-days 3
27
+
28
+ neurolink telemetry native-validate \
29
+ --directory "$HOME/neurolink-native-migration-review" \
30
+ --collector-bin "$HOME/.neurolink/telemetry-native/bin/otelcol-contrib"
31
+ ```
32
+
33
+ The destination directory must not exist. It is created with mode `0700`, with
34
+ `0600` files: `collector.yaml`, `collector.env.json`, `proxy.env`, `doctor.env`
35
+ and `manifest.json`. The JSON environment contains the private backend
36
+ authorization; keep it private and do not paste it into an issue. Command output
37
+ and the profile do not contain that credential. The doctor snippet includes
38
+ **both stream names** and the actual collector metrics port; supply backend
39
+ authorization to the doctor's environment privately. Snippets are environment
40
+ configuration to merge, not shell scripts to execute. Never replace an existing
41
+ proxy environment wholesale with a snippet.
42
+
43
+ `native-validate` checks private file permissions, staged file hashes, the source
44
+ configuration hash, and stable collector version `>= 0.160.0`, then runs only
45
+ `otelcol-contrib validate`. It uses temporary queue paths inside the stage and
46
+ removes them afterward. It never starts a collector or connects to the backend.
47
+ Binary diagnostics are suppressed because they can contain credentials. A
48
+ successful validation proves profile acceptance, not delivery, backend retention,
49
+ queue recovery or runtime adoption. Re-prepare into a new directory after any
50
+ source or staged-file edits.
51
+
52
+ The manifest records requested payload limits, planned volume quota, separate
53
+ backend retention periods and remaining operator checks. `--metadata-queue-mib`
54
+ (default 32) applies to each of three metadata signal queues;
55
+ `--body-queue-mib` defaults to 256. These become collector environment byte
56
+ limits. The quota must allow at least twice the sum of payload limits, but that
57
+ is planning headroom, **not a guarantee that compaction fits**. No filesystem
58
+ quota or backend retention setting is applied. The operator must provision both
59
+ private storage directories on a quota-managed volume and apply/verify backend
60
+ retention before activation.
61
+
62
+ There is deliberately no activation command in this workflow. Service changes
63
+ require an explicit operator action: first back up collector config, service
64
+ definition and existing environments; then merge the reviewed settings using
65
+ the service manager. On failed health, export or backend checks, restore those
66
+ backups. Do not substitute `proxy install` for this review: it may regenerate
67
+ existing launch settings. Verify fresh metadata **and** body records, queue
68
+ failure counters and unchanged application-log files afterward. Package release
69
+ and static validation do not establish that the live service adopted the stage.
70
+
71
+ Host scheduling pressure is separate from queue durability. If the machine has
72
+ many concurrent builds, MCP launches or other CPU consumers, review that
73
+ concurrency independently. Preparation and validation do not stop processes,
74
+ disable scanners or change host scheduling policy.
75
+
8
76
  Configure these variables in the collector service environment:
9
77
 
10
78
  - `NEUROLINK_OPENOBSERVE_OTLP_ENDPOINT`: backend API endpoint, for example
@@ -31,7 +31,7 @@ exporters:
31
31
  enabled: true
32
32
  num_consumers: 2
33
33
  sizer: bytes
34
- queue_size: 33554432
34
+ queue_size: ${env:NEUROLINK_OTEL_METADATA_QUEUE_BYTES:-33554432}
35
35
  storage: file_storage
36
36
  otlphttp/openobserve-bodies:
37
37
  endpoint: ${env:NEUROLINK_OPENOBSERVE_OTLP_ENDPOINT}
@@ -48,7 +48,7 @@ exporters:
48
48
  enabled: true
49
49
  num_consumers: 2
50
50
  sizer: bytes
51
- queue_size: 268435456
51
+ queue_size: ${env:NEUROLINK_OTEL_BODY_QUEUE_BYTES:-268435456}
52
52
  storage: file_storage
53
53
  extensions:
54
54
  file_storage:
@@ -144,6 +144,89 @@ function normalizeMetadataRows(kind, rows) {
144
144
  };
145
145
  }
146
146
 
147
+ /**
148
+ * Resolve a single exported source, never a reference chain. References retain
149
+ * the current phase identity and prove only transport acceptance until queried.
150
+ * @param {Record<string, any>} index
151
+ */
152
+ export function resolveProxyBodyCaptureTarget(index) {
153
+ if (!/^[a-f0-9-]{36}$/i.test(index.captureId ?? "")) {
154
+ throw new Error("Invalid capture identifier");
155
+ }
156
+ if (index.bodyDelivery?.status !== "reference") {
157
+ if (index.bodyReference !== undefined) {
158
+ throw new Error("Unexpected body reference on a non-reference capture");
159
+ }
160
+ return { captureId: index.captureId, exportedAt: undefined };
161
+ }
162
+ const reference = index.bodyReference;
163
+ if (
164
+ !reference ||
165
+ !/^[a-f0-9-]{36}$/i.test(reference.captureId ?? "") ||
166
+ reference.captureId === index.captureId ||
167
+ typeof index.requestId !== "string" ||
168
+ reference.requestId !== index.requestId ||
169
+ !/^[a-f0-9]{64}$/i.test(index.bodySha256 ?? "") ||
170
+ reference.bodySha256 !== index.bodySha256 ||
171
+ reference.redactedBodyBytes !== index.redactedBodyBytes ||
172
+ !Number.isFinite(Date.parse(reference.exportedAt))
173
+ ) {
174
+ throw new Error("Invalid or conflicting body reference");
175
+ }
176
+ return {
177
+ captureId: reference.captureId,
178
+ exportedAt: Date.parse(reference.exportedAt),
179
+ };
180
+ }
181
+
182
+ /**
183
+ * Reconstruct exact retained bytes, including reference captures, using the
184
+ * same event identity and chunk integrity checks as the telemetry doctor.
185
+ * @param {Record<string, any>} index
186
+ * @param {Array<Record<string, any>>} rawChunks
187
+ */
188
+ export function reconstructProxyBodyCapture(index, rawChunks) {
189
+ const target = resolveProxyBodyCaptureTarget(index);
190
+ const normalized = normalizeBodyChunkRows(rawChunks);
191
+ const chunks = normalized.chunks;
192
+ const bytes = chunks.reduce(
193
+ (sum, row) =>
194
+ sum + (typeof row.body === "string" ? Buffer.byteLength(row.body) : Infinity),
195
+ 0,
196
+ );
197
+ if (bytes > 8 * 1024 * 1024) {
198
+ throw new Error("Stored capture exceeds the 8 MiB verification bound");
199
+ }
200
+ const body = chunks.map((row) => row.body).join("");
201
+ const expected = Number(chunks[0]?.body_chunk_count ?? 0);
202
+ const verified =
203
+ expected > 0 &&
204
+ expected === chunks.length &&
205
+ chunks.every(
206
+ (row, i) =>
207
+ Number(row.body_chunk_index) === i &&
208
+ Number(row.body_chunk_count) === expected &&
209
+ row.body_capture_id === target.captureId &&
210
+ row.request_id === index.requestId,
211
+ ) &&
212
+ normalized.diagnostics.missingEventIds === 0 &&
213
+ normalized.diagnostics.conflicts.length === 0 &&
214
+ bytes === index.redactedBodyBytes &&
215
+ createHash("sha256").update(body).digest("hex") === index.bodySha256;
216
+ return {
217
+ body,
218
+ captureId: index.captureId,
219
+ sourceCaptureId: target.captureId,
220
+ expectedChunks: expected,
221
+ storedChunks: chunks.length,
222
+ rawStoredChunks: rawChunks.length,
223
+ exactRetryDuplicates: normalized.diagnostics.exactRetryDuplicates,
224
+ missingEventIds: normalized.diagnostics.missingEventIds,
225
+ identityConflicts: normalized.diagnostics.conflicts,
226
+ verified,
227
+ };
228
+ }
229
+
147
230
  /** @param {Array<Record<string, unknown>>} rows */
148
231
  function normalizeBodyChunkRows(rows) {
149
232
  const byEvent = new Map();
@@ -159,6 +242,7 @@ function normalizeBodyChunkRows(rows) {
159
242
  : undefined;
160
243
  const physicalIdentity = JSON.stringify([
161
244
  row._timestamp,
245
+ row.request_id,
162
246
  row.body_capture_id,
163
247
  row.body_chunk_index,
164
248
  row.body_chunk_count,
@@ -576,16 +660,19 @@ export async function checkProxyTelemetry({
576
660
  reason: "supervisor_telemetry_missing",
577
661
  },
578
662
  );
579
- /** @type {Array<{kind: string, dropped: number, exportUnconfirmed: number, outstanding: number, capacity: number, recentFailures?: unknown, failureHistoryEvicted?: number}>} */
663
+ /** @type {Array<{kind: string, dropped: number, exportUnconfirmed: number, outstanding: number, capacity: number, byteCapacity?: number, outstandingBytes?: number, retryingRecords?: number, recentFailures?: unknown, failureHistoryEvicted?: number}>} */
580
664
  const failures = (logs?.otel?.queues ?? []).map(
581
665
  (
582
- /** @type {{kind: string, dropped: number, exportUnconfirmed: number, outstanding: number, capacity: number, recentFailures?: unknown, failureHistoryEvicted?: number}} */ q,
666
+ /** @type {{kind: string, dropped: number, exportUnconfirmed: number, outstanding: number, capacity: number, byteCapacity?: number, outstandingBytes?: number, retryingRecords?: number, recentFailures?: unknown, failureHistoryEvicted?: number}} */ q,
583
667
  ) => ({
584
668
  kind: q.kind,
585
669
  dropped: q.dropped,
586
670
  exportUnconfirmed: q.exportUnconfirmed,
587
671
  outstanding: q.outstanding,
588
672
  capacity: q.capacity,
673
+ byteCapacity: q.byteCapacity,
674
+ outstandingBytes: q.outstandingBytes,
675
+ retryingRecords: q.retryingRecords,
589
676
  recentFailures: q.recentFailures,
590
677
  failureHistoryEvicted: q.failureHistoryEvicted,
591
678
  }),
@@ -702,7 +789,13 @@ export async function checkProxyTelemetry({
702
789
  ).values(),
703
790
  ];
704
791
  const currentProducerPressure = failures.some(
705
- (queue) => queue.outstanding >= queue.capacity,
792
+ (queue) =>
793
+ queue.outstanding >= queue.capacity ||
794
+ (typeof queue.byteCapacity === "number" &&
795
+ Number.isFinite(queue.byteCapacity) &&
796
+ queue.byteCapacity > 0 &&
797
+ (queue.outstandingBytes ?? 0) >= queue.byteCapacity) ||
798
+ (queue.retryingRecords ?? 0) > 0,
706
799
  );
707
800
  const runtimeFailureHistoryIncomplete = failures.some((queue) => {
708
801
  if (!(Number(queue.failureHistoryEvicted) > 0)) {
@@ -767,7 +860,7 @@ export async function checkProxyTelemetry({
767
860
  const unhealthyCaptures = captureIndexes.filter(
768
861
  (row) =>
769
862
  row.captureError !== undefined ||
770
- !["transport_acknowledged", "no_body", "policy_excluded"].includes(
863
+ !["transport_acknowledged", "reference", "no_body", "policy_excluded"].includes(
771
864
  row.bodyDelivery?.status ?? "",
772
865
  ),
773
866
  );
@@ -1014,9 +1107,25 @@ export async function checkProxyTelemetry({
1014
1107
  (row) =>
1015
1108
  row.captureError ||
1016
1109
  row.bodyTruncated ||
1017
- !["transport_acknowledged", "policy_excluded", "no_body"].includes(
1110
+ (row.bodyDelivery &&
1111
+ "reason" in row.bodyDelivery &&
1112
+ row.bodyDelivery.reason === "body_byte_budget_invalid") ||
1113
+ !["transport_acknowledged", "reference", "policy_excluded", "no_body"].includes(
1018
1114
  row.bodyDelivery?.status ?? "",
1019
- ),
1115
+ ) ||
1116
+ (() => {
1117
+ try {
1118
+ if (
1119
+ row.bodyDelivery?.status === "reference" ||
1120
+ ("bodyReference" in row && row.bodyReference)
1121
+ ) {
1122
+ resolveProxyBodyCaptureTarget(row);
1123
+ }
1124
+ return false;
1125
+ } catch {
1126
+ return true;
1127
+ }
1128
+ })(),
1020
1129
  );
1021
1130
  add(
1022
1131
  "capture_delivery",
@@ -1039,12 +1148,21 @@ export async function checkProxyTelemetry({
1039
1148
  const lossyCaptures = indexes.filter(
1040
1149
  (row) => row.bodyTruncated || row.redactionLossy,
1041
1150
  );
1151
+ const omittedCaptures = indexes.filter(
1152
+ (row) =>
1153
+ row.bodyDelivery?.status === "policy_excluded" &&
1154
+ (!("reason" in row.bodyDelivery) ||
1155
+ row.bodyDelivery.reason !== "borrowed_traffic"),
1156
+ );
1042
1157
  add(
1043
1158
  "capture_content_completeness",
1044
- lossyCaptures.length ? "warn" : indexes.length ? "pass" : "unverified",
1159
+ lossyCaptures.length || omittedCaptures.length
1160
+ ? "warn"
1161
+ : indexes.length ? "pass" : "unverified",
1045
1162
  {
1046
1163
  captures: indexes.length,
1047
1164
  truncatedOrUnparseable: lossyCaptures.length,
1165
+ policyExcluded: omittedCaptures.length,
1048
1166
  captureIds: lossyCaptures.slice(0, 50).map((row) => row.captureId),
1049
1167
  boundary:
1050
1168
  "Transport acknowledgement does not restore source truncation, processing limits, or invalid frames removed by redaction",
@@ -1054,13 +1172,14 @@ export async function checkProxyTelemetry({
1054
1172
  for (const index of indexes
1055
1173
  .filter(
1056
1174
  (row) =>
1057
- row.bodySha256 && row.bodyDelivery?.status === "transport_acknowledged",
1175
+ row.bodySha256 &&
1176
+ ["transport_acknowledged", "reference"].includes(
1177
+ row.bodyDelivery?.status ?? "",
1178
+ ),
1058
1179
  )
1059
1180
  .sort((a, b) => (b.redactedBodyBytes ?? 0) - (a.redactedBodyBytes ?? 0))
1060
1181
  .slice(0, 3)) {
1061
- if (!/^[a-f0-9-]{36}$/i.test(index.captureId ?? "")) {
1062
- throw new Error("Invalid capture identifier");
1063
- }
1182
+ const target = resolveProxyBodyCaptureTarget(index);
1064
1183
  if (
1065
1184
  !Number.isSafeInteger(index.redactedBodyBytes) ||
1066
1185
  (index.redactedBodyBytes ?? -1) < 0 ||
@@ -1069,53 +1188,19 @@ export async function checkProxyTelemetry({
1069
1188
  throw new Error("Capture verification exceeds the 8 MiB per-body bound");
1070
1189
  }
1071
1190
  const rawChunks = await query(
1072
- `SELECT _timestamp, proxy_event_id, body_capture_id, body_chunk_index, body_chunk_count, body FROM "${backend.bodyStream ?? backend.stream}" WHERE proxy_record_kind='body' AND body_capture_id='${index.captureId}' ORDER BY body_chunk_index ASC, proxy_event_id ASC`,
1191
+ `SELECT _timestamp, proxy_event_id, request_id, body_capture_id, body_chunk_index, body_chunk_count, body FROM "${backend.bodyStream ?? backend.stream}" WHERE proxy_record_kind='body' AND body_capture_id='${target.captureId}' ORDER BY body_chunk_index ASC, proxy_event_id ASC`,
1073
1192
  "logs",
1074
1193
  {
1075
- startTime: startTime - 120e6,
1076
- endTime: Math.min(Date.now() * 1000, endTime + 120e6),
1194
+ startTime: (target.exportedAt === undefined ? startTime : target.exportedAt * 1000) - 120e6,
1195
+ endTime: Math.min(Date.now() * 1000, (target.exportedAt === undefined ? endTime : target.exportedAt * 1000) + 120e6),
1077
1196
  size: 1000,
1078
1197
  },
1079
1198
  );
1080
1199
  if (rawChunks.length >= 1000) {
1081
1200
  throw new Error("Capture verification exceeded its chunk budget");
1082
1201
  }
1083
- const normalizedChunks = normalizeBodyChunkRows(rawChunks);
1084
- const chunks = normalizedChunks.chunks;
1085
- const bytes = chunks.reduce(
1086
- (sum, r) =>
1087
- sum +
1088
- (typeof r.body === "string" ? Buffer.byteLength(r.body) : Infinity),
1089
- 0,
1090
- );
1091
- if (bytes > 8 * 1024 * 1024) {
1092
- throw new Error("Stored capture exceeds the 8 MiB verification bound");
1093
- }
1094
- const raw = chunks.map((r) => r.body).join(""),
1095
- expected = Number(chunks[0]?.body_chunk_count ?? 0);
1096
- const contiguous = chunks.every(
1097
- (row, i) =>
1098
- Number(row.body_chunk_index) === i &&
1099
- Number(row.body_chunk_count) === expected,
1100
- );
1101
- const actualSha256 = createHash("sha256").update(raw).digest("hex");
1102
- bodyChecks.push({
1103
- captureId: index.captureId,
1104
- expectedChunks: expected,
1105
- storedChunks: chunks.length,
1106
- rawStoredChunks: rawChunks.length,
1107
- exactRetryDuplicates: normalizedChunks.diagnostics.exactRetryDuplicates,
1108
- missingEventIds: normalizedChunks.diagnostics.missingEventIds,
1109
- identityConflicts: normalizedChunks.diagnostics.conflicts,
1110
- verified:
1111
- expected > 0 &&
1112
- expected === chunks.length &&
1113
- contiguous &&
1114
- normalizedChunks.diagnostics.missingEventIds === 0 &&
1115
- normalizedChunks.diagnostics.conflicts.length === 0 &&
1116
- Buffer.byteLength(raw) === index.redactedBodyBytes &&
1117
- actualSha256 === index.bodySha256,
1118
- });
1202
+ const { body: _body, ...checked } = reconstructProxyBodyCapture(index, rawChunks);
1203
+ bodyChecks.push(checked);
1119
1204
  }
1120
1205
  add(
1121
1206
  "sample_body_integrity",