@juspay/neurolink 9.92.3 → 9.93.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 (50) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/browser/neurolink.min.js +379 -382
  3. package/dist/cli/commands/proxy.d.ts +5 -3
  4. package/dist/cli/commands/proxy.js +360 -69
  5. package/dist/cli/commands/proxyAnalyze.d.ts +3 -0
  6. package/dist/cli/commands/proxyAnalyze.js +147 -0
  7. package/dist/cli/parser.js +3 -1
  8. package/dist/lib/proxy/proxyActivity.d.ts +2 -2
  9. package/dist/lib/proxy/proxyActivity.js +49 -9
  10. package/dist/lib/proxy/proxyAnalysis.d.ts +3 -0
  11. package/dist/lib/proxy/proxyAnalysis.js +454 -0
  12. package/dist/lib/proxy/proxyHealth.d.ts +4 -0
  13. package/dist/lib/proxy/proxyHealth.js +21 -0
  14. package/dist/lib/proxy/proxyLifecycle.d.ts +8 -0
  15. package/dist/lib/proxy/proxyLifecycle.js +333 -0
  16. package/dist/lib/proxy/requestLogger.js +6 -0
  17. package/dist/lib/proxy/sseInterceptor.d.ts +9 -1
  18. package/dist/lib/proxy/sseInterceptor.js +6 -5
  19. package/dist/lib/proxy/streamOutcome.d.ts +3 -1
  20. package/dist/lib/proxy/streamOutcome.js +77 -0
  21. package/dist/lib/proxy/updateCoordinator.d.ts +7 -0
  22. package/dist/lib/proxy/updateCoordinator.js +60 -0
  23. package/dist/lib/proxy/updateState.d.ts +4 -0
  24. package/dist/lib/proxy/updateState.js +51 -0
  25. package/dist/lib/server/routes/claudeProxyRoutes.d.ts +1 -0
  26. package/dist/lib/server/routes/claudeProxyRoutes.js +137 -35
  27. package/dist/lib/types/cli.d.ts +7 -0
  28. package/dist/lib/types/proxy.d.ts +269 -0
  29. package/dist/proxy/proxyActivity.d.ts +2 -2
  30. package/dist/proxy/proxyActivity.js +49 -9
  31. package/dist/proxy/proxyAnalysis.d.ts +3 -0
  32. package/dist/proxy/proxyAnalysis.js +453 -0
  33. package/dist/proxy/proxyHealth.d.ts +4 -0
  34. package/dist/proxy/proxyHealth.js +21 -0
  35. package/dist/proxy/proxyLifecycle.d.ts +8 -0
  36. package/dist/proxy/proxyLifecycle.js +332 -0
  37. package/dist/proxy/requestLogger.js +6 -0
  38. package/dist/proxy/sseInterceptor.d.ts +9 -1
  39. package/dist/proxy/sseInterceptor.js +6 -5
  40. package/dist/proxy/streamOutcome.d.ts +3 -1
  41. package/dist/proxy/streamOutcome.js +77 -0
  42. package/dist/proxy/updateCoordinator.d.ts +7 -0
  43. package/dist/proxy/updateCoordinator.js +59 -0
  44. package/dist/proxy/updateState.d.ts +4 -0
  45. package/dist/proxy/updateState.js +51 -0
  46. package/dist/server/routes/claudeProxyRoutes.d.ts +1 -0
  47. package/dist/server/routes/claudeProxyRoutes.js +137 -35
  48. package/dist/types/cli.d.ts +7 -0
  49. package/dist/types/proxy.d.ts +269 -0
  50. package/package.json +4 -1
@@ -25,7 +25,7 @@ import { createRawStreamCapture } from "../../proxy/rawStreamCapture.js";
25
25
  import { relocateClientSystemIntoMessages } from "../../proxy/systemRelocation.js";
26
26
  import { logBodyCapture, logRequest, logRequestAttempt, } from "../../proxy/requestLogger.js";
27
27
  import { createSSEInterceptor } from "../../proxy/sseInterceptor.js";
28
- import { createStreamTerminalOutcomeTracker, mergeStreamTerminalOutcome, } from "../../proxy/streamOutcome.js";
28
+ import { createStreamTerminalOutcomeTracker, mergeStreamTerminalOutcome, preflightAnthropicStream, } from "../../proxy/streamOutcome.js";
29
29
  import { isPermanentRefreshFailure, needsRefresh, persistTokens, refreshToken, } from "../../proxy/tokenRefresh.js";
30
30
  import { buildProxyTranslationPlan, parseRetryAfterMs, } from "../../proxy/routingPolicy.js";
31
31
  import { writeJsonSnapshotAtomically } from "../../proxy/snapshotPersistence.js";
@@ -2182,7 +2182,7 @@ function buildClaudeAnthropicFailureResponse(args) {
2182
2182
  });
2183
2183
  }
2184
2184
  async function handleAnthropicSuccessfulResponse(args) {
2185
- const { ctx, body, account, accountState, response, tracer, requestStartTime, fetchStartMs, attemptNumber, finalBodyStr, upstreamSpan, logProxyBody, logFinalRequest, } = args;
2185
+ const { ctx, body, account, accountState, response, tracer, requestStartTime, fetchStartMs, attemptNumber, finalBodyStr, upstreamSpan, logAttempt, logProxyBody, logFinalRequest, } = args;
2186
2186
  accountState.consecutiveRefreshFailures = 0;
2187
2187
  logger.always(`[proxy] ← ${response.status} account=${account.label}`);
2188
2188
  const quota = parseQuotaHeaders(response.headers);
@@ -2222,6 +2222,7 @@ async function handleAnthropicSuccessfulResponse(args) {
2222
2222
  attemptNumber,
2223
2223
  finalBodyStr,
2224
2224
  upstreamSpan,
2225
+ logAttempt,
2225
2226
  logProxyBody,
2226
2227
  logFinalRequest,
2227
2228
  });
@@ -2241,7 +2242,7 @@ async function handleAnthropicSuccessfulResponse(args) {
2241
2242
  });
2242
2243
  }
2243
2244
  async function handleAnthropicStreamingSuccessResponse(args) {
2244
- const { account, accountState: _accountState, response, responseHeaders, tracer, requestStartTime, fetchStartMs, attemptNumber, finalBodyStr, upstreamSpan, logProxyBody, logFinalRequest, } = args;
2245
+ const { account, accountState, response, responseHeaders, tracer, requestStartTime, fetchStartMs, attemptNumber, finalBodyStr, upstreamSpan, logAttempt, logProxyBody, logFinalRequest, } = args;
2245
2246
  if (!response.body) {
2246
2247
  upstreamSpan?.end();
2247
2248
  tracer?.setError("stream_error", "No response body from upstream");
@@ -2265,44 +2266,129 @@ async function handleAnthropicStreamingSuccessResponse(args) {
2265
2266
  return { response: clientError };
2266
2267
  }
2267
2268
  const reader = response.body.getReader();
2268
- let firstChunk;
2269
- try {
2270
- firstChunk = await reader.read();
2271
- }
2272
- catch (error) {
2273
- const message = error instanceof Error ? error.message : String(error);
2269
+ const preflight = await preflightAnthropicStream(reader);
2270
+ if (preflight.kind === "transport_error") {
2271
+ const message = describeTransportError(preflight.error);
2272
+ const partialBody = Buffer.concat(preflight.chunks.map((chunk) => Buffer.from(chunk))).toString("utf8");
2274
2273
  logger.always(`[proxy] stream failed before first chunk account=${account.label}: ${message}; trying next account`);
2275
2274
  recordAttemptError(account.label, account.type, 502);
2275
+ logAttempt(502, "stream_error", message, { retryable: true });
2276
2276
  tracer?.recordRetry(account.label, "stream_before_first_chunk");
2277
2277
  upstreamSpan?.end();
2278
2278
  logProxyBody({
2279
2279
  phase: "upstream_response",
2280
2280
  headers: responseHeaders,
2281
- body: message,
2282
- bodySize: Buffer.byteLength(message, "utf8"),
2281
+ body: partialBody,
2282
+ bodySize: Buffer.byteLength(partialBody, "utf8"),
2283
2283
  contentType: responseHeaders["content-type"] ?? "text/event-stream",
2284
2284
  account: account.label,
2285
2285
  accountType: account.type,
2286
2286
  attempt: attemptNumber,
2287
- responseStatus: 502,
2287
+ responseStatus: response.status,
2288
2288
  durationMs: Date.now() - fetchStartMs,
2289
+ metadata: { logicalStatus: 502, transportError: message },
2289
2290
  });
2290
- return { retryNextAccount: true };
2291
+ return {
2292
+ retryNextAccount: true,
2293
+ failure: { message, rateLimit: false },
2294
+ };
2291
2295
  }
2292
- if (firstChunk.done || !firstChunk.value || firstChunk.value.length === 0) {
2296
+ if (preflight.kind === "empty") {
2293
2297
  await reader.cancel().catch(() => {
2294
2298
  // Best-effort release before rotating to another account.
2295
2299
  });
2296
2300
  logger.always(`[proxy] ← empty stream from account=${account.label}, trying next`);
2301
+ recordAttemptError(account.label, account.type, 502);
2302
+ logAttempt(502, "empty_stream", "Empty upstream stream", {
2303
+ retryable: true,
2304
+ });
2297
2305
  tracer?.recordRetry(account.label, "empty_stream");
2298
2306
  upstreamSpan?.end();
2299
- return { retryNextAccount: true };
2307
+ logProxyBody({
2308
+ phase: "upstream_response",
2309
+ headers: responseHeaders,
2310
+ body: "",
2311
+ bodySize: 0,
2312
+ contentType: responseHeaders["content-type"] ?? "text/event-stream",
2313
+ account: account.label,
2314
+ accountType: account.type,
2315
+ attempt: attemptNumber,
2316
+ responseStatus: response.status,
2317
+ durationMs: Date.now() - fetchStartMs,
2318
+ metadata: { logicalStatus: 502, upstreamErrorType: "empty_stream" },
2319
+ });
2320
+ return {
2321
+ retryNextAccount: true,
2322
+ failure: { message: "Empty upstream stream", rateLimit: false },
2323
+ };
2324
+ }
2325
+ if (preflight.kind === "sse_error") {
2326
+ await reader.cancel().catch(() => {
2327
+ // Best-effort release before rotating to another account.
2328
+ });
2329
+ const bodyText = Buffer.concat(preflight.chunks.map((chunk) => Buffer.from(chunk))).toString("utf8");
2330
+ const isRateLimit = preflight.errorType === "rate_limit_error";
2331
+ const logicalStatus = isRateLimit ? 429 : 502;
2332
+ const quota = parseQuotaHeaders(responseHeaders);
2333
+ const now = Date.now();
2334
+ if (isRateLimit) {
2335
+ const cooldownPlan = planCooldownFor429(quota, parseRetryAfterMs(responseHeaders["retry-after"] ?? null), now, getUnifiedRateLimitStatus(responseHeaders));
2336
+ accountState.quota = quota ?? accountState.quota;
2337
+ const rateLimitKind = cooldownPlan.reason === "transient" ? "transient" : "quota";
2338
+ if (!accountState.coolingUntil ||
2339
+ cooldownPlan.coolingUntil > accountState.coolingUntil) {
2340
+ accountState.coolingUntil = cooldownPlan.coolingUntil;
2341
+ accountState.coolingReason = cooldownPlan.reason;
2342
+ await saveAccountCooldown(account.key, cooldownPlan.coolingUntil, cooldownPlan.reason).catch(() => {
2343
+ // Non-fatal: routing already has the in-memory cooldown.
2344
+ });
2345
+ }
2346
+ recordAttemptError(account.label, account.type, 429, rateLimitKind);
2347
+ logAttempt(429, "rate_limit_error", preflight.message, {
2348
+ retryable: true,
2349
+ rateLimitKind,
2350
+ cooldownReason: cooldownPlan.reason,
2351
+ });
2352
+ }
2353
+ else {
2354
+ recordAttemptError(account.label, account.type, logicalStatus);
2355
+ logAttempt(logicalStatus, preflight.errorType, preflight.message, {
2356
+ retryable: true,
2357
+ });
2358
+ }
2359
+ logger.always(`[proxy] immediate SSE ${preflight.errorType} account=${account.label}: ${preflight.message}; rotating before client commit`);
2360
+ tracer?.recordRetry(account.label, isRateLimit
2361
+ ? "stream_rate_limit_before_commit"
2362
+ : "stream_error_before_commit");
2363
+ upstreamSpan?.end();
2364
+ logProxyBody({
2365
+ phase: "upstream_response",
2366
+ headers: responseHeaders,
2367
+ body: bodyText,
2368
+ bodySize: Buffer.byteLength(bodyText, "utf8"),
2369
+ contentType: responseHeaders["content-type"] ?? "text/event-stream",
2370
+ account: account.label,
2371
+ accountType: account.type,
2372
+ attempt: attemptNumber,
2373
+ responseStatus: response.status,
2374
+ durationMs: Date.now() - fetchStartMs,
2375
+ metadata: {
2376
+ logicalStatus,
2377
+ upstreamErrorType: preflight.errorType,
2378
+ },
2379
+ });
2380
+ return {
2381
+ retryNextAccount: true,
2382
+ failure: { message: preflight.message, rateLimit: isRateLimit },
2383
+ };
2300
2384
  }
2301
2385
  const streamOutcomeTracker = createStreamTerminalOutcomeTracker();
2302
2386
  let mainStreamClosed = false;
2303
2387
  const remainingStream = new ReadableStream({
2304
2388
  start(controller) {
2305
- controller.enqueue(firstChunk.value);
2389
+ for (const chunk of preflight.chunks) {
2390
+ controller.enqueue(chunk);
2391
+ }
2306
2392
  },
2307
2393
  async pull(controller) {
2308
2394
  if (mainStreamClosed) {
@@ -2865,10 +2951,12 @@ async function handleAnthropicAuthRetry(args) {
2865
2951
  await clearAuthCooldownAfterRefresh(account, accountState);
2866
2952
  headers.authorization = `Bearer ${account.token}`;
2867
2953
  const retryAttemptNumber = allocateAttemptNumber();
2954
+ const retryAttemptStartedAt = Date.now();
2868
2955
  recordAttempt(account.label, account.type);
2869
2956
  const retryLogAttempt = (status, errorType, errorMessage, extra) => logAttempt(status, errorType, errorMessage, {
2870
2957
  ...extra,
2871
2958
  attempt: retryAttemptNumber,
2959
+ attemptDurationMs: Date.now() - retryAttemptStartedAt,
2872
2960
  });
2873
2961
  const retryBodyStr = buildUpstreamBody(account.token).bodyStr;
2874
2962
  const retryFetchStartMs = Date.now();
@@ -3062,7 +3150,10 @@ async function handleAnthropicAuthRetry(args) {
3062
3150
  : String(retryFetchErr);
3063
3151
  authRetryError = `network error on retry ${authRetry + 1}: ${message}`;
3064
3152
  currentLastError = authRetryError;
3065
- retryLogAttempt(502, "network_error", message, { retryable: true });
3153
+ retryLogAttempt(502, "network_error", message, {
3154
+ retryable: isRetryableNetworkError(retryFetchErr),
3155
+ errorCode: getErrorCode(retryFetchErr) ?? "unknown",
3156
+ });
3066
3157
  logger.debug(`[proxy] ${authRetryError}`);
3067
3158
  break;
3068
3159
  }
@@ -3420,7 +3511,9 @@ function createClaudeRequestRuntimeContext(args) {
3420
3511
  }
3421
3512
  function createAnthropicAttemptLogger(args) {
3422
3513
  const { ctx, body, toolCount, requestStart, tracer, account, attemptNumber } = args;
3514
+ const attemptStartedAt = Date.now();
3423
3515
  return (status, errorType, errorMessage, extra) => {
3516
+ const attemptCompletedAt = Date.now();
3424
3517
  const traceCtx = tracer?.getTraceContext();
3425
3518
  logRequestAttempt({
3426
3519
  timestamp: new Date().toISOString(),
@@ -3434,9 +3527,11 @@ function createAnthropicAttemptLogger(args) {
3434
3527
  account: account.label,
3435
3528
  accountType: account.type,
3436
3529
  responseStatus: status,
3437
- responseTimeMs: Date.now() - requestStart,
3530
+ responseTimeMs: attemptCompletedAt - requestStart,
3531
+ attemptDurationMs: extra?.attemptDurationMs ?? attemptCompletedAt - attemptStartedAt,
3438
3532
  ...(errorType ? { errorType } : {}),
3439
3533
  ...(errorMessage ? { errorMessage } : {}),
3534
+ ...(extra?.errorCode ? { errorCode: extra.errorCode } : {}),
3440
3535
  ...(extra?.inputTokens !== undefined
3441
3536
  ? { inputTokens: extra.inputTokens }
3442
3537
  : {}),
@@ -3651,19 +3746,27 @@ async function fetchAnthropicAccountResponse(args) {
3651
3746
  });
3652
3747
  }
3653
3748
  catch (fetchErr) {
3654
- if (!isRetryableNetworkError(fetchErr)) {
3655
- throw fetchErr;
3656
- }
3749
+ const retryable = isRetryableNetworkError(fetchErr);
3750
+ // Every dispatched upstream request is an attempt, including terminal
3751
+ // transport failures. Record it once before preserving the throw behavior.
3657
3752
  sawNetworkError = true;
3658
3753
  recordAttemptError(account.label, account.type, 502);
3659
3754
  const errorCode = getErrorCode(fetchErr) ?? "unknown";
3660
3755
  const errorMessage = fetchErr instanceof Error ? fetchErr.message : String(fetchErr);
3661
3756
  lastError = errorMessage;
3662
- logger.always(`[proxy] fetch error account=${account.label} code=${errorCode} (retryable): ${errorMessage}`);
3663
- logAttempt(502, "network_error", errorMessage);
3757
+ logger.always(`[proxy] fetch error account=${account.label} code=${errorCode} (${retryable ? "retryable" : "terminal"}): ${errorMessage}`);
3758
+ logAttempt(502, "network_error", errorMessage, {
3759
+ retryable,
3760
+ errorCode,
3761
+ });
3664
3762
  tracer?.setError("network_error", errorMessage);
3665
- tracer?.recordRetry(account.label, "network_error");
3763
+ if (retryable) {
3764
+ tracer?.recordRetry(account.label, "network_error");
3765
+ }
3666
3766
  currentUpstreamSpan?.end();
3767
+ if (!retryable) {
3768
+ throw fetchErr;
3769
+ }
3667
3770
  return {
3668
3771
  continueLoop: true,
3669
3772
  retrySameAccount: true,
@@ -4083,10 +4186,16 @@ async function handleAnthropicRoutedClaudeRequest(args) {
4083
4186
  attemptNumber: loopState.attemptNumber,
4084
4187
  finalBodyStr: preparedAttempt.finalBodyStr,
4085
4188
  upstreamSpan,
4189
+ logAttempt,
4086
4190
  logProxyBody,
4087
4191
  logFinalRequest,
4088
4192
  });
4089
4193
  if ("retryNextAccount" in successResult) {
4194
+ if (successResult.failure) {
4195
+ loopState.lastError = successResult.failure.message;
4196
+ loopState.sawRateLimit ||= successResult.failure.rateLimit;
4197
+ loopState.sawTransientFailure ||= !successResult.failure.rateLimit;
4198
+ }
4090
4199
  continue accountLoop;
4091
4200
  }
4092
4201
  return successResult.response;
@@ -4469,16 +4578,13 @@ function describeTransportError(error) {
4469
4578
  */
4470
4579
  function isRetryableNetworkError(error) {
4471
4580
  const code = getErrorCode(error);
4472
- // Check non-retryable codes FIRST — before the string-based heuristic
4473
- // which could false-positive on error messages containing these strings.
4474
- const NON_RETRYABLE_CODES = ["ENOTFOUND"];
4475
- if (code && NON_RETRYABLE_CODES.includes(code)) {
4476
- return false;
4477
- }
4478
4581
  if (code &&
4479
4582
  [
4480
4583
  "ECONNREFUSED",
4481
4584
  "ECONNRESET",
4585
+ // The Anthropic host is fixed, so ENOTFOUND can be a transient resolver
4586
+ // outage. Keep it inside the existing bounded same-account retry budget.
4587
+ "ENOTFOUND",
4482
4588
  "ETIMEDOUT",
4483
4589
  "EHOSTUNREACH",
4484
4590
  "UND_ERR_CONNECT_TIMEOUT",
@@ -4490,13 +4596,9 @@ function isRetryableNetworkError(error) {
4490
4596
  }
4491
4597
  const message = error instanceof Error ? error.message : String(error);
4492
4598
  const normalized = message.toLowerCase();
4493
- // Exclude ENOTFOUND from string-based heuristic — DNS failures are permanent
4494
- // and rotating accounts won't help since they all hit the same host.
4495
- if (normalized.includes("enotfound")) {
4496
- return false;
4497
- }
4498
4599
  return (normalized.includes("econnrefused") ||
4499
4600
  normalized.includes("econnreset") ||
4601
+ normalized.includes("enotfound") ||
4500
4602
  normalized.includes("etimedout") ||
4501
4603
  normalized.includes("timed out") ||
4502
4604
  normalized.includes("connection error") ||
@@ -791,6 +791,13 @@ export type ProxyStatusArgs = {
791
791
  format?: "text" | "json";
792
792
  quiet?: boolean;
793
793
  };
794
+ /** Arguments accepted by `neurolink proxy analyze` */
795
+ export type ProxyAnalyzeArgs = {
796
+ logsDir?: string;
797
+ since?: string;
798
+ format?: "text" | "json";
799
+ quiet?: boolean;
800
+ };
794
801
  /** Arguments accepted by hidden `neurolink proxy guard` command */
795
802
  export type ProxyGuardArgs = {
796
803
  host?: string;
@@ -437,6 +437,8 @@ export type RequestLogEntry = {
437
437
  responseTimeMs: number;
438
438
  errorType?: string;
439
439
  errorMessage?: string;
440
+ /** Low-level transport code such as ETIMEDOUT or EADDRNOTAVAIL. */
441
+ errorCode?: string;
440
442
  inputTokens?: number;
441
443
  outputTokens?: number;
442
444
  cacheCreationTokens?: number;
@@ -458,9 +460,14 @@ export type RequestAttemptLogEntry = {
458
460
  account: string;
459
461
  accountType: string;
460
462
  responseStatus: number;
463
+ /** End-to-end request age when this attempt completed. */
461
464
  responseTimeMs: number;
465
+ /** Time spent in this specific account attempt. */
466
+ attemptDurationMs?: number;
462
467
  errorType?: string;
463
468
  errorMessage?: string;
469
+ /** Low-level transport code such as ETIMEDOUT or EADDRNOTAVAIL. */
470
+ errorCode?: string;
464
471
  /** Whether this failed attempt may be retried without changing the request. */
465
472
  retryable?: boolean;
466
473
  /** Distinguishes short-lived admission throttles from exhausted quota windows. */
@@ -514,8 +521,12 @@ export type AnthropicAttemptLogger = (status: number, errorType?: string, errorM
514
521
  cacheCreationTokens?: number;
515
522
  cacheReadTokens?: number;
516
523
  retryable?: boolean;
524
+ /** Low-level transport code such as ETIMEDOUT or EADDRNOTAVAIL. */
525
+ errorCode?: string;
517
526
  rateLimitKind?: "transient" | "quota";
518
527
  cooldownReason?: "transient" | "session" | "weekly" | "unified";
528
+ /** Override for nested retries whose attempt starts after this logger. */
529
+ attemptDurationMs?: number;
519
530
  /** Override used when one account selection performs an OAuth retry fetch. */
520
531
  attempt?: number;
521
532
  }) => void;
@@ -552,9 +563,40 @@ export type LoadedClaudeAccountContext = {
552
563
  };
553
564
  export type AnthropicSuccessResult = {
554
565
  retryNextAccount: true;
566
+ failure?: {
567
+ message: string;
568
+ rateLimit: boolean;
569
+ };
555
570
  } | {
556
571
  response: Response | unknown;
557
572
  };
573
+ /** Result of buffering only enough upstream SSE to make a retry-safe decision. */
574
+ export type AnthropicStreamPreflightResult = {
575
+ kind: "ready";
576
+ chunks: Uint8Array[];
577
+ } | {
578
+ kind: "empty";
579
+ chunks: Uint8Array[];
580
+ } | {
581
+ kind: "transport_error";
582
+ chunks: Uint8Array[];
583
+ error: unknown;
584
+ } | {
585
+ kind: "sse_error";
586
+ chunks: Uint8Array[];
587
+ errorType: string;
588
+ message: string;
589
+ };
590
+ /** One complete Server-Sent Event extracted from an incremental buffer. */
591
+ export type ParsedSSEEvent = {
592
+ event: string;
593
+ data: string;
594
+ };
595
+ /** Complete SSE events plus the trailing partial frame. */
596
+ export type ParsedSSEBuffer = {
597
+ events: ParsedSSEEvent[];
598
+ remainder: string;
599
+ };
558
600
  export type AnthropicAuthRetryResult = {
559
601
  response?: Response | unknown;
560
602
  continueLoop: boolean;
@@ -804,6 +846,8 @@ export type ProxyReadinessState = {
804
846
  startTimeMs: number;
805
847
  acceptingConnections: boolean;
806
848
  ready: boolean;
849
+ /** True only while the updater is draining inference traffic. */
850
+ drainingForUpdate: boolean;
807
851
  readyAtMs?: number;
808
852
  };
809
853
  /** Structured response returned by the proxy /health endpoint. */
@@ -811,6 +855,7 @@ export type ProxyHealthResponse = {
811
855
  status: "ok" | "starting";
812
856
  ready: boolean;
813
857
  acceptingConnections: boolean;
858
+ drainingForUpdate: boolean;
814
859
  strategy: string;
815
860
  passthrough: boolean;
816
861
  version: string;
@@ -945,6 +990,198 @@ export type ProxyRuntimeActivity = {
945
990
  activeRequests: number;
946
991
  lastActivityAt: string | null;
947
992
  };
993
+ /** Terminal state observed while the HTTP adapter relays a response body. */
994
+ export type ProxyResponseTerminalOutcome = "completed" | "bodyless" | "client_cancelled" | "stream_error";
995
+ /** Non-blocking callbacks for response lifecycle metadata. */
996
+ export type ProxyResponseTrackingObserver = {
997
+ onFirstChunk?: (details: {
998
+ /** Decoded response-body bytes observed by the adapter. */
999
+ observedBodyBytes: number;
1000
+ responseChunks: 1;
1001
+ }) => void;
1002
+ onTerminal?: (details: {
1003
+ outcome: ProxyResponseTerminalOutcome;
1004
+ /** Decoded response-body bytes observed by the adapter. */
1005
+ observedBodyBytes: number;
1006
+ responseChunks: number;
1007
+ }) => void;
1008
+ };
1009
+ /** Versioned lifecycle event names persisted by the proxy adapter. */
1010
+ export type ProxyLifecycleEventName = "request_accepted" | "response_headers" | "response_first_chunk" | "request_terminal";
1011
+ /** Client-facing terminal classifications recorded by lifecycle metadata. */
1012
+ export type ProxyLifecycleTerminalOutcome = ProxyResponseTerminalOutcome | "handler_error";
1013
+ /** Content-free lifecycle event accepted by the bounded metadata logger. */
1014
+ export type ProxyLifecycleEventInput = {
1015
+ event: ProxyLifecycleEventName;
1016
+ requestId: string;
1017
+ method: string;
1018
+ path: string;
1019
+ model?: string;
1020
+ stream?: boolean;
1021
+ toolCount?: number;
1022
+ sessionHash?: string;
1023
+ requestBytes?: number;
1024
+ responseStatus?: number;
1025
+ /** Decoded response-body bytes observed by the adapter. */
1026
+ observedBodyBytes?: number;
1027
+ responseChunks?: number;
1028
+ elapsedMs?: number;
1029
+ terminalOutcome?: ProxyLifecycleTerminalOutcome;
1030
+ errorType?: string;
1031
+ errorCode?: string;
1032
+ timestampMs?: number;
1033
+ monotonicMs?: number;
1034
+ };
1035
+ /** Data-quality counters for the bounded lifecycle metadata sink. */
1036
+ export type ProxyLifecycleLoggerSnapshot = {
1037
+ enabled: boolean;
1038
+ schemaVersion: number;
1039
+ processInstanceId: string;
1040
+ nextSequence: number;
1041
+ attempted: number;
1042
+ enqueued: number;
1043
+ written: number;
1044
+ dropped: number;
1045
+ queueDrops: number;
1046
+ invalidDrops: number;
1047
+ writeDrops: number;
1048
+ writeFailures: number;
1049
+ pending: number;
1050
+ inFlight: number;
1051
+ flushing: boolean;
1052
+ };
1053
+ /** Lifecycle logger configuration. Queue overrides are used by stress tests. */
1054
+ export type ProxyLifecycleLoggerOptions = {
1055
+ enabled: boolean;
1056
+ logDir?: string;
1057
+ queueCapacity?: number;
1058
+ batchSize?: number;
1059
+ flushIntervalMs?: number;
1060
+ };
1061
+ /** Serialized lifecycle line awaiting a bounded batch write. */
1062
+ export type QueuedProxyLifecycleEvent = {
1063
+ logDir: string;
1064
+ date: string;
1065
+ record: Record<string, unknown>;
1066
+ };
1067
+ /** Percentile summary used by offline proxy log analysis. */
1068
+ export type ProxyLatencySummary = {
1069
+ count: number;
1070
+ p50: number | null;
1071
+ p95: number | null;
1072
+ p99: number | null;
1073
+ max: number | null;
1074
+ };
1075
+ /** Per-account request and rate-limit totals reconstructed from JSONL logs. */
1076
+ export type ProxyAnalysisAccount = {
1077
+ account: string;
1078
+ accountType: string;
1079
+ attempts: number;
1080
+ attemptErrors: number;
1081
+ finalRequests: number;
1082
+ finalErrors: number;
1083
+ transientRateLimits: number;
1084
+ quotaRateLimits: number;
1085
+ unclassifiedRateLimits: number;
1086
+ };
1087
+ /** Offline report generated from proxy request, attempt, and lifecycle logs. */
1088
+ export type ProxyAnalysisReport = {
1089
+ generatedAt: string;
1090
+ since: string;
1091
+ logsDir: string;
1092
+ files: {
1093
+ lifecycle: number;
1094
+ requests: number;
1095
+ attempts: number;
1096
+ };
1097
+ coverage: {
1098
+ lifecycle: boolean;
1099
+ finalRequests: boolean;
1100
+ attempts: boolean;
1101
+ attemptLatency: boolean;
1102
+ cacheUsage: boolean;
1103
+ };
1104
+ dataQuality: {
1105
+ linesRead: number;
1106
+ malformedLines: number;
1107
+ unsupportedLifecycleLines: number;
1108
+ lifecycleSequenceGaps: number;
1109
+ lifecycleSequenceDuplicates: number;
1110
+ };
1111
+ lifecycle: {
1112
+ accepted: number;
1113
+ headers: number;
1114
+ firstChunks: number;
1115
+ terminal: number;
1116
+ unsettled: number;
1117
+ terminalOutcomes: Record<string, number>;
1118
+ errorTypes: Record<string, number>;
1119
+ errorCodes: Record<string, number>;
1120
+ };
1121
+ requests: {
1122
+ completed: number;
1123
+ success: number;
1124
+ errors: number;
1125
+ finalRateLimits: number;
1126
+ recoveredAfterRetry: number;
1127
+ errorTypes: Record<string, number>;
1128
+ errorCodes: Record<string, number>;
1129
+ };
1130
+ attempts: {
1131
+ total: number;
1132
+ errors: number;
1133
+ errorTypes: Record<string, number>;
1134
+ errorCodes: Record<string, number>;
1135
+ };
1136
+ rateLimits: {
1137
+ attemptRateLimits: number;
1138
+ transient: number;
1139
+ quota: number;
1140
+ unclassified: number;
1141
+ };
1142
+ latencyMs: {
1143
+ headers: ProxyLatencySummary;
1144
+ firstChunk: ProxyLatencySummary;
1145
+ terminal: ProxyLatencySummary;
1146
+ finalRequest: ProxyLatencySummary;
1147
+ attempt: ProxyLatencySummary;
1148
+ singleAttemptDelta: ProxyLatencySummary;
1149
+ };
1150
+ cache: {
1151
+ requestsWithUsage: number;
1152
+ requestsWithCacheRead: number;
1153
+ cacheReadTokens: number;
1154
+ cacheCreationTokens: number;
1155
+ inputTokens: number;
1156
+ requestHitRate: number | null;
1157
+ };
1158
+ accounts: ProxyAnalysisAccount[];
1159
+ };
1160
+ /** Inputs for the offline proxy JSONL analyzer. */
1161
+ export type ProxyAnalysisOptions = {
1162
+ logsDir?: string;
1163
+ since?: string;
1164
+ nowMs?: number;
1165
+ };
1166
+ /** Attempt timing retained while joining offline proxy log records. */
1167
+ export type ProxyAnalysisAttemptRecord = {
1168
+ count: number;
1169
+ hadError: boolean;
1170
+ totalDurationMs: number;
1171
+ durationCount: number;
1172
+ };
1173
+ /** Final request fields retained while joining offline proxy log records. */
1174
+ export type ProxyAnalysisFinalRequestRecord = {
1175
+ status: number;
1176
+ durationMs: number | null;
1177
+ account: string;
1178
+ accountType: string;
1179
+ inputTokens: number | null;
1180
+ cacheReadTokens: number | null;
1181
+ cacheCreationTokens: number | null;
1182
+ errorType: string | null;
1183
+ errorCode: string | null;
1184
+ };
948
1185
  /** Request metadata retained by the HTTP adapter for terminal error logging. */
949
1186
  export type RuntimeRequestMetadata = {
950
1187
  requestId: string;
@@ -954,6 +1191,10 @@ export type RuntimeRequestMetadata = {
954
1191
  model: string;
955
1192
  stream: boolean;
956
1193
  toolCount: number;
1194
+ /** Admission decision captured before an updater drain can race the route. */
1195
+ rejectForUpdate?: boolean;
1196
+ terminalErrorType?: string;
1197
+ terminalErrorCode?: string;
957
1198
  };
958
1199
  /** Accumulated upstream body capture from a raw stream. */
959
1200
  export type RawStreamCapture = {
@@ -1119,6 +1360,14 @@ export type UpdateState = {
1119
1360
  lastUpdateVersion: string | null;
1120
1361
  /** Installed by the updater but not yet confirmed as the running version. */
1121
1362
  pendingRestartVersion: string | null;
1363
+ /** Why an available update has not yet reached a safe install/restart boundary. */
1364
+ deferredUpdate: {
1365
+ version: string;
1366
+ since: string;
1367
+ updatedAt: string;
1368
+ reason: "waiting_for_quiet" | "draining" | "drain_timeout" | "drain_unavailable" | "activity_unavailable";
1369
+ activeRequests: number | null;
1370
+ } | null;
1122
1371
  /** Last updater failure, retained until a successful update or replacement. */
1123
1372
  lastFailure: {
1124
1373
  at: string;
@@ -1127,6 +1376,26 @@ export type UpdateState = {
1127
1376
  message: string;
1128
1377
  } | null;
1129
1378
  };
1379
+ /** Result from waiting for a non-disruptive updater execution window. */
1380
+ export type ProxyUpdateWindowResult = {
1381
+ ready: boolean;
1382
+ draining: boolean;
1383
+ reason?: "stopping" | "parent_stopped" | "drain_failed" | "drain_timeout";
1384
+ };
1385
+ /** Dependencies and timing controls for the updater's safe-window coordinator. */
1386
+ export type ProxyUpdateWindowOptions = {
1387
+ quietThresholdMs: number;
1388
+ quietWaitMs: number;
1389
+ drainWaitMs: number;
1390
+ pollIntervalMs: number;
1391
+ getActivity: () => Promise<ProxyRuntimeActivity | null>;
1392
+ setDraining: (draining: boolean) => Promise<boolean>;
1393
+ isStopping: () => boolean;
1394
+ isParentAlive: () => boolean;
1395
+ onPhase?: (phase: "waiting_for_quiet" | "draining" | "drain_timeout", activity: ProxyRuntimeActivity | null) => void;
1396
+ now?: () => number;
1397
+ sleep?: (ms: number) => Promise<void>;
1398
+ };
1130
1399
  /** Result of validating a newly installed CLI through the stable trampoline. */
1131
1400
  export type InstalledVersionValidation = {
1132
1401
  version?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/neurolink",
3
- "version": "9.92.3",
3
+ "version": "9.93.1",
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": {
@@ -177,6 +177,9 @@
177
177
  "proxy:observability:status": "bash scripts/observability/manage-local-openobserve.sh status",
178
178
  "proxy:observability:doctor": "node scripts/observability/check-proxy-telemetry.mjs",
179
179
  "proxy:observability:import-dashboard": "bash scripts/observability/manage-local-openobserve.sh import-dashboard",
180
+ "proxy:performance:lifecycle": "tsx tools/testing/proxyLifecycleBenchmark.ts",
181
+ "proxy:performance:transport": "tsx tools/testing/proxyTransportBenchmark.ts",
182
+ "proxy:performance": "pnpm run proxy:performance:lifecycle && pnpm run proxy:performance:transport",
180
183
  "// Development & Monitoring": "",
181
184
  "dev:health": "tsx tools/development/healthMonitor.ts",
182
185
  "dev:demo": "concurrently \"pnpm run dev\" \"node neurolink-demo/complete-enhanced-server.js\"",