@juspay/neurolink 12.14.13 → 12.14.15
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 +2 -2
- package/dist/browser/neurolink.min.js +309 -309
- package/dist/cli/commands/proxy.js +171 -17
- package/dist/providers/anthropic/client.js +13 -1
- package/dist/proxy/otelLogSink.js +121 -37
- package/dist/proxy/proxyLifecycle.js +4 -0
- package/dist/types/proxy.d.ts +4 -0
- package/docs-site/static/search-index.json +1 -1
- package/package.json +2 -1
- package/scripts/observability/docker-compose.proxy-observability.yaml +4 -0
- package/scripts/observability/otel-collector.proxy-observability.yaml +19 -1
- package/scripts/observability/proxy-observability.env.example +4 -0
- package/scripts/observability/proxy-telemetry-check.mjs +145 -28
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juspay/neurolink",
|
|
3
|
-
"version": "12.14.
|
|
3
|
+
"version": "12.14.15",
|
|
4
4
|
"packageManager": "pnpm@10.15.1",
|
|
5
5
|
"description": "TypeScript AI SDK with 24+ LLM providers behind one consistent API. MCP-native (connect any MCP server), voice TTS/STT/realtime, RAG, agents, memory, context compaction. OpenAI · Anthropic · Gemini · Bedrock · Azure · Ollama · DeepSeek · NVIDIA NIM and more.",
|
|
6
6
|
"author": {
|
|
@@ -95,6 +95,7 @@
|
|
|
95
95
|
"test:proxy-sharing": "pnpm run build && pnpm exec tsx test/continuous-test-suite-proxy-sharing.ts",
|
|
96
96
|
"test:openai-compat-streaming-retry": "pnpm exec tsx test/continuous-test-suite-openai-compat-streaming-retry.ts",
|
|
97
97
|
"test:anthropic-streaming-retry": "pnpm exec tsx test/continuous-test-suite-anthropic-streaming-retry.ts",
|
|
98
|
+
"test:anthropic-silent-truncation": "pnpm exec tsx test/continuous-test-suite-anthropic-silent-truncation.ts",
|
|
98
99
|
"test:adjust-body-after-400": "pnpm exec tsx test/continuous-test-suite-adjust-body-after-400.ts",
|
|
99
100
|
"test:zod3-schema-native-path": "pnpm exec tsx test/continuous-test-suite-zod3-schema-native-path.ts",
|
|
100
101
|
"test:error-classification-e2e": "pnpm exec tsx test/continuous-test-suite-error-classification-e2e.ts",
|
|
@@ -18,6 +18,7 @@ services:
|
|
|
18
18
|
NEUROLINK_PROXY_STREAM_HEADER: ${NEUROLINK_PROXY_STREAM_HEADER:-neurolink-proxy}
|
|
19
19
|
volumes:
|
|
20
20
|
- ./otel-collector.proxy-observability.yaml:/etc/otelcol/config.yaml:ro
|
|
21
|
+
- otel-collector-data:/var/lib/otelcol
|
|
21
22
|
ports:
|
|
22
23
|
- "${NEUROLINK_OTLP_GRPC_PORT:-14317}:4317"
|
|
23
24
|
- "${NEUROLINK_OTLP_HTTP_PORT:-14318}:4318"
|
|
@@ -41,6 +42,7 @@ services:
|
|
|
41
42
|
ZO_DATA_DIR: /data
|
|
42
43
|
RUST_LOG: ${NEUROLINK_OPENOBSERVE_LOG_LEVEL:-info}
|
|
43
44
|
ZO_INGEST_ALLOWED_UPTO: ${NEUROLINK_OPENOBSERVE_INGEST_HOURS:-720}
|
|
45
|
+
ZO_COMPACT_DATA_RETENTION_DAYS: ${NEUROLINK_OPENOBSERVE_RETENTION_DAYS:-30}
|
|
44
46
|
volumes:
|
|
45
47
|
- openobserve-data:/data
|
|
46
48
|
networks:
|
|
@@ -51,5 +53,7 @@ networks:
|
|
|
51
53
|
driver: bridge
|
|
52
54
|
|
|
53
55
|
volumes:
|
|
56
|
+
otel-collector-data:
|
|
57
|
+
name: ${NEUROLINK_OTEL_COLLECTOR_VOLUME_NAME:-neurolink-otel-collector-data}
|
|
54
58
|
openobserve-data:
|
|
55
59
|
name: ${NEUROLINK_OPENOBSERVE_VOLUME_NAME:-neurolink-openobserve-data}
|
|
@@ -38,8 +38,26 @@ exporters:
|
|
|
38
38
|
stream-name: ${env:NEUROLINK_PROXY_STREAM_HEADER}
|
|
39
39
|
tls:
|
|
40
40
|
insecure: true
|
|
41
|
+
retry_on_failure:
|
|
42
|
+
enabled: true
|
|
43
|
+
initial_interval: 1s
|
|
44
|
+
max_interval: 30s
|
|
45
|
+
max_elapsed_time: 0s
|
|
46
|
+
sending_queue:
|
|
47
|
+
enabled: true
|
|
48
|
+
num_consumers: 4
|
|
49
|
+
queue_size: 4096
|
|
50
|
+
storage: file_storage
|
|
41
51
|
|
|
42
52
|
extensions:
|
|
53
|
+
file_storage:
|
|
54
|
+
directory: /var/lib/otelcol/storage
|
|
55
|
+
create_directory: true
|
|
56
|
+
compaction:
|
|
57
|
+
directory: /var/lib/otelcol/compaction
|
|
58
|
+
on_start: true
|
|
59
|
+
on_rebound: true
|
|
60
|
+
|
|
43
61
|
health_check:
|
|
44
62
|
endpoint: 0.0.0.0:13133
|
|
45
63
|
|
|
@@ -47,7 +65,7 @@ extensions:
|
|
|
47
65
|
endpoint: 0.0.0.0:1777
|
|
48
66
|
|
|
49
67
|
service:
|
|
50
|
-
extensions: [health_check, pprof]
|
|
68
|
+
extensions: [file_storage, health_check, pprof]
|
|
51
69
|
|
|
52
70
|
pipelines:
|
|
53
71
|
traces:
|
|
@@ -17,6 +17,10 @@ NEUROLINK_OTEL_PPROF_PORT=11777
|
|
|
17
17
|
# Optional names if you want multiple local stacks in parallel.
|
|
18
18
|
# NEUROLINK_OBSERVABILITY_PROJECT_NAME=neurolink-proxy-observability
|
|
19
19
|
# NEUROLINK_OPENOBSERVE_VOLUME_NAME=neurolink-openobserve-data
|
|
20
|
+
# NEUROLINK_OTEL_COLLECTOR_VOLUME_NAME=neurolink-otel-collector-data
|
|
21
|
+
|
|
22
|
+
# Bound local telemetry storage while retaining enough incident history.
|
|
23
|
+
NEUROLINK_OPENOBSERVE_RETENTION_DAYS=30
|
|
20
24
|
|
|
21
25
|
# The collector exports proxy telemetry into this OpenObserve stream header.
|
|
22
26
|
# OpenObserve normalizes the logs and traces stream name to "neurolink_proxy".
|
|
@@ -112,30 +112,6 @@ export async function checkProxyTelemetry({
|
|
|
112
112
|
failureHistoryEvicted: q.failureHistoryEvicted,
|
|
113
113
|
}),
|
|
114
114
|
);
|
|
115
|
-
add(
|
|
116
|
-
"producer_delivery",
|
|
117
|
-
["metadata", "bodies"].every((kind) =>
|
|
118
|
-
failures.some((q) => q.kind === kind),
|
|
119
|
-
) &&
|
|
120
|
-
failures.every(
|
|
121
|
-
(q) =>
|
|
122
|
-
q.dropped === 0 &&
|
|
123
|
-
q.exportUnconfirmed === 0 &&
|
|
124
|
-
q.outstanding < q.capacity,
|
|
125
|
-
)
|
|
126
|
-
? "pass"
|
|
127
|
-
: "warn",
|
|
128
|
-
failures,
|
|
129
|
-
);
|
|
130
|
-
add(
|
|
131
|
-
"capture_admission",
|
|
132
|
-
logs?.bodyCapture &&
|
|
133
|
-
logs.bodyCapture.rejected === 0 &&
|
|
134
|
-
logs.bodyCapture.failed === 0
|
|
135
|
-
? "pass"
|
|
136
|
-
: "warn",
|
|
137
|
-
logs?.bodyCapture ?? { status: "unavailable" },
|
|
138
|
-
);
|
|
139
115
|
for (const [signal, table] of [
|
|
140
116
|
["logs", backend.stream],
|
|
141
117
|
["traces", backend.stream],
|
|
@@ -165,13 +141,24 @@ export async function checkProxyTelemetry({
|
|
|
165
141
|
}
|
|
166
142
|
/** @type {Record<string, import("../../src/lib/types/index.js").ProxyTelemetryStoredRecord[]>} */
|
|
167
143
|
const history = Object.create(null);
|
|
168
|
-
for (const kind of [
|
|
144
|
+
for (const kind of [
|
|
145
|
+
"request_final",
|
|
146
|
+
"body_capture_index",
|
|
147
|
+
"lifecycle",
|
|
148
|
+
"telemetry_delivery",
|
|
149
|
+
]) {
|
|
169
150
|
const result = await queryProxyHistory({
|
|
170
151
|
...backend,
|
|
171
152
|
// Capture publication is asynchronous. Include its bounded settling
|
|
172
153
|
// window, and a request-start margin, when reconciling phases to finals.
|
|
173
|
-
startTime:
|
|
174
|
-
|
|
154
|
+
startTime:
|
|
155
|
+
kind === "body_capture_index"
|
|
156
|
+
? Math.max(0, startTime - 120e6)
|
|
157
|
+
: startTime,
|
|
158
|
+
endTime:
|
|
159
|
+
kind === "body_capture_index" || kind === "telemetry_delivery"
|
|
160
|
+
? Math.min(Date.now() * 1000, endTime + 120e6)
|
|
161
|
+
: endTime,
|
|
175
162
|
kind,
|
|
176
163
|
maxRows,
|
|
177
164
|
fetchImpl,
|
|
@@ -186,13 +173,143 @@ export async function checkProxyTelemetry({
|
|
|
186
173
|
if (
|
|
187
174
|
!value ||
|
|
188
175
|
typeof value !== "object" ||
|
|
189
|
-
|
|
176
|
+
(kind === "telemetry_delivery"
|
|
177
|
+
? typeof value.id !== "string" ||
|
|
178
|
+
typeof value.at !== "string" ||
|
|
179
|
+
!Number.isFinite(Date.parse(value.at))
|
|
180
|
+
: typeof value.requestId !== "string")
|
|
190
181
|
) {
|
|
191
182
|
throw new Error("Stored telemetry metadata has an invalid schema");
|
|
192
183
|
}
|
|
193
184
|
return { ...value, recordedAtMicroseconds: Number(row._timestamp) };
|
|
194
185
|
});
|
|
195
186
|
}
|
|
187
|
+
/** @param {{at?: unknown}} row */
|
|
188
|
+
const failedInsideInterval = (row) => {
|
|
189
|
+
const at = Date.parse(typeof row.at === "string" ? row.at : "");
|
|
190
|
+
return (
|
|
191
|
+
Number.isFinite(at) &&
|
|
192
|
+
at * 1000 >= startTime &&
|
|
193
|
+
at * 1000 < endTime
|
|
194
|
+
);
|
|
195
|
+
};
|
|
196
|
+
const storedDeliveryFailures = history.telemetry_delivery.filter(
|
|
197
|
+
failedInsideInterval,
|
|
198
|
+
);
|
|
199
|
+
const runtimeDeliveryFailures = failures.flatMap((queue) =>
|
|
200
|
+
Array.isArray(queue.recentFailures)
|
|
201
|
+
? queue.recentFailures.filter(
|
|
202
|
+
(failure) =>
|
|
203
|
+
failure &&
|
|
204
|
+
typeof failure === "object" &&
|
|
205
|
+
failedInsideInterval(failure),
|
|
206
|
+
)
|
|
207
|
+
: [],
|
|
208
|
+
);
|
|
209
|
+
const deliveryFailures = [
|
|
210
|
+
...new Map(
|
|
211
|
+
[...storedDeliveryFailures, ...runtimeDeliveryFailures].map(
|
|
212
|
+
(failure, index) => [
|
|
213
|
+
typeof failure.id === "string"
|
|
214
|
+
? failure.id
|
|
215
|
+
: `unidentified-${index}`,
|
|
216
|
+
failure,
|
|
217
|
+
],
|
|
218
|
+
),
|
|
219
|
+
).values(),
|
|
220
|
+
];
|
|
221
|
+
const currentProducerPressure = failures.some(
|
|
222
|
+
(queue) => queue.outstanding >= queue.capacity,
|
|
223
|
+
);
|
|
224
|
+
const runtimeFailureHistoryIncomplete = failures.some((queue) => {
|
|
225
|
+
if (!(Number(queue.failureHistoryEvicted) > 0)) {
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
if (!Array.isArray(queue.recentFailures) || !queue.recentFailures.length) {
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
const oldestRetainedFailure = Math.min(
|
|
232
|
+
...queue.recentFailures.map((failure) =>
|
|
233
|
+
Date.parse(typeof failure?.at === "string" ? failure.at : ""),
|
|
234
|
+
),
|
|
235
|
+
);
|
|
236
|
+
// The bounded runtime buffer proves absence only from its oldest retained
|
|
237
|
+
// failure onward. Older selected windows need stored recovery diagnostics;
|
|
238
|
+
// if those are missing, report uncertainty instead of a false pass.
|
|
239
|
+
return (
|
|
240
|
+
!Number.isFinite(oldestRetainedFailure) ||
|
|
241
|
+
startTime < oldestRetainedFailure * 1000
|
|
242
|
+
);
|
|
243
|
+
});
|
|
244
|
+
add(
|
|
245
|
+
"producer_delivery",
|
|
246
|
+
!["metadata", "bodies"].every((kind) =>
|
|
247
|
+
failures.some((queue) => queue.kind === kind),
|
|
248
|
+
)
|
|
249
|
+
? "unverified"
|
|
250
|
+
: currentProducerPressure || deliveryFailures.length
|
|
251
|
+
? "warn"
|
|
252
|
+
: runtimeFailureHistoryIncomplete
|
|
253
|
+
? "unverified"
|
|
254
|
+
: "pass",
|
|
255
|
+
{
|
|
256
|
+
scope: "selected_interval",
|
|
257
|
+
deliveryFailures: deliveryFailures.length,
|
|
258
|
+
failureIds: deliveryFailures.slice(0, 50).map((row) => row.id),
|
|
259
|
+
sources: {
|
|
260
|
+
storedRecoveryDiagnostics: storedDeliveryFailures.length,
|
|
261
|
+
runtimeRecentFailures: runtimeDeliveryFailures.length,
|
|
262
|
+
},
|
|
263
|
+
currentPressure: currentProducerPressure,
|
|
264
|
+
runtimeFailureHistoryComplete: !runtimeFailureHistoryIncomplete,
|
|
265
|
+
workerLifetime: {
|
|
266
|
+
scope: "worker_lifetime",
|
|
267
|
+
queues: failures,
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
);
|
|
271
|
+
const captureIndexes = history.body_capture_index.filter((row) => {
|
|
272
|
+
const eventTime = Date.parse(
|
|
273
|
+
typeof row.timestamp === "string" ? row.timestamp : "",
|
|
274
|
+
);
|
|
275
|
+
const atMicroseconds = Number.isFinite(eventTime)
|
|
276
|
+
? eventTime * 1000
|
|
277
|
+
: row.recordedAtMicroseconds;
|
|
278
|
+
return (
|
|
279
|
+
Number.isFinite(atMicroseconds) &&
|
|
280
|
+
(atMicroseconds ?? 0) >= startTime &&
|
|
281
|
+
(atMicroseconds ?? Infinity) < endTime
|
|
282
|
+
);
|
|
283
|
+
});
|
|
284
|
+
const unhealthyCaptures = captureIndexes.filter(
|
|
285
|
+
(row) =>
|
|
286
|
+
row.captureError !== undefined ||
|
|
287
|
+
![
|
|
288
|
+
"transport_acknowledged",
|
|
289
|
+
"no_body",
|
|
290
|
+
"policy_excluded",
|
|
291
|
+
].includes(row.bodyDelivery?.status ?? ""),
|
|
292
|
+
);
|
|
293
|
+
add(
|
|
294
|
+
"capture_admission",
|
|
295
|
+
!captureIndexes.length
|
|
296
|
+
? "unverified"
|
|
297
|
+
: unhealthyCaptures.length
|
|
298
|
+
? "warn"
|
|
299
|
+
: "pass",
|
|
300
|
+
{
|
|
301
|
+
scope: "selected_interval",
|
|
302
|
+
captures: captureIndexes.length,
|
|
303
|
+
unhealthy: unhealthyCaptures.length,
|
|
304
|
+
requestIds: unhealthyCaptures
|
|
305
|
+
.slice(0, 50)
|
|
306
|
+
.map((row) => row.requestId),
|
|
307
|
+
workerLifetime: {
|
|
308
|
+
scope: "worker_lifetime",
|
|
309
|
+
counters: logs?.bodyCapture ?? { status: "unavailable" },
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
);
|
|
196
313
|
const finals = history.request_final,
|
|
197
314
|
unique = new Set(finals.map((r) => r.requestId));
|
|
198
315
|
add(
|