@juspay/neurolink 9.93.0 → 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 (40) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/browser/neurolink.min.js +379 -382
  3. package/dist/cli/commands/proxy.d.ts +3 -2
  4. package/dist/cli/commands/proxy.js +209 -48
  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/proxyAnalysis.d.ts +3 -0
  9. package/dist/lib/proxy/proxyAnalysis.js +454 -0
  10. package/dist/lib/proxy/proxyHealth.d.ts +4 -0
  11. package/dist/lib/proxy/proxyHealth.js +21 -0
  12. package/dist/lib/proxy/sseInterceptor.d.ts +9 -1
  13. package/dist/lib/proxy/sseInterceptor.js +6 -5
  14. package/dist/lib/proxy/streamOutcome.d.ts +3 -1
  15. package/dist/lib/proxy/streamOutcome.js +77 -0
  16. package/dist/lib/proxy/updateCoordinator.d.ts +7 -0
  17. package/dist/lib/proxy/updateCoordinator.js +60 -0
  18. package/dist/lib/proxy/updateState.d.ts +4 -0
  19. package/dist/lib/proxy/updateState.js +51 -0
  20. package/dist/lib/server/routes/claudeProxyRoutes.d.ts +1 -0
  21. package/dist/lib/server/routes/claudeProxyRoutes.js +114 -17
  22. package/dist/lib/types/cli.d.ts +7 -0
  23. package/dist/lib/types/proxy.d.ts +187 -0
  24. package/dist/proxy/proxyAnalysis.d.ts +3 -0
  25. package/dist/proxy/proxyAnalysis.js +453 -0
  26. package/dist/proxy/proxyHealth.d.ts +4 -0
  27. package/dist/proxy/proxyHealth.js +21 -0
  28. package/dist/proxy/sseInterceptor.d.ts +9 -1
  29. package/dist/proxy/sseInterceptor.js +6 -5
  30. package/dist/proxy/streamOutcome.d.ts +3 -1
  31. package/dist/proxy/streamOutcome.js +77 -0
  32. package/dist/proxy/updateCoordinator.d.ts +7 -0
  33. package/dist/proxy/updateCoordinator.js +59 -0
  34. package/dist/proxy/updateState.d.ts +4 -0
  35. package/dist/proxy/updateState.js +51 -0
  36. package/dist/server/routes/claudeProxyRoutes.d.ts +1 -0
  37. package/dist/server/routes/claudeProxyRoutes.js +114 -17
  38. package/dist/types/cli.d.ts +7 -0
  39. package/dist/types/proxy.d.ts +187 -0
  40. package/package.json +4 -1
@@ -0,0 +1,60 @@
1
+ function isQuiet(activity, quietThresholdMs, nowMs) {
2
+ if (activity.activeRequests > 0) {
3
+ return false;
4
+ }
5
+ if (!activity.lastActivityAt) {
6
+ return true;
7
+ }
8
+ const lastActivityMs = Date.parse(activity.lastActivityAt);
9
+ return (Number.isFinite(lastActivityMs) &&
10
+ nowMs - lastActivityMs >= quietThresholdMs);
11
+ }
12
+ /**
13
+ * Prefer a naturally quiet window, then briefly drain new inference traffic.
14
+ * Existing requests are never interrupted; a bounded drain failure is returned
15
+ * to the caller so admission can be reopened and retried later.
16
+ */
17
+ export async function waitForProxyUpdateWindow(options) {
18
+ const now = options.now ?? Date.now;
19
+ const wait = options.sleep ??
20
+ ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
21
+ const quietDeadline = now() + options.quietWaitMs;
22
+ while (now() < quietDeadline) {
23
+ if (options.isStopping()) {
24
+ return { ready: false, draining: false, reason: "stopping" };
25
+ }
26
+ if (!options.isParentAlive()) {
27
+ return { ready: false, draining: false, reason: "parent_stopped" };
28
+ }
29
+ const activity = await options.getActivity();
30
+ if (activity && isQuiet(activity, options.quietThresholdMs, now())) {
31
+ return { ready: true, draining: false };
32
+ }
33
+ options.onPhase?.("waiting_for_quiet", activity);
34
+ await wait(options.pollIntervalMs);
35
+ }
36
+ if (!(await options.setDraining(true))) {
37
+ // The control response may have been lost after the parent applied it.
38
+ // Treat state as unknown/draining so the caller always attempts resume.
39
+ return { ready: false, draining: true, reason: "drain_failed" };
40
+ }
41
+ const drainDeadline = now() + options.drainWaitMs;
42
+ while (now() < drainDeadline) {
43
+ if (options.isStopping()) {
44
+ return { ready: false, draining: true, reason: "stopping" };
45
+ }
46
+ if (!options.isParentAlive()) {
47
+ return { ready: false, draining: true, reason: "parent_stopped" };
48
+ }
49
+ const activity = await options.getActivity();
50
+ options.onPhase?.("draining", activity);
51
+ if (activity?.activeRequests === 0) {
52
+ return { ready: true, draining: true };
53
+ }
54
+ await wait(options.pollIntervalMs);
55
+ }
56
+ const activity = await options.getActivity();
57
+ options.onPhase?.("drain_timeout", activity);
58
+ return { ready: false, draining: true, reason: "drain_timeout" };
59
+ }
60
+ //# sourceMappingURL=updateCoordinator.js.map
@@ -54,6 +54,10 @@ export declare function recordUpdateInstalled(version: string, stateFilePath?: s
54
54
  export declare function abandonPendingUpdate(version: string, stateFilePath?: string): boolean;
55
55
  /** Persist a stage-specific updater failure so status remains actionable. */
56
56
  export declare function recordUpdateFailure(version: string, stage: NonNullable<UpdateState["lastFailure"]>["stage"], message: string, stateFilePath?: string): void;
57
+ /** Persist why a discovered update is waiting without treating it as a failure. */
58
+ export declare function recordUpdateDeferred(version: string, reason: NonNullable<UpdateState["deferredUpdate"]>["reason"], activeRequests: number | null, stateFilePath?: string): void;
59
+ /** Clear matching updater deferral metadata once work can proceed. */
60
+ export declare function clearUpdateDeferral(version?: string, stateFilePath?: string): boolean;
57
61
  /** Complete an updater-managed install after a manual or automatic restart. */
58
62
  export declare function reconcileRunningUpdate(runningVersion: string, stateFilePath?: string): boolean;
59
63
  /**
@@ -56,6 +56,26 @@ function isValidLastFailure(value) {
56
56
  UPDATE_FAILURE_STAGES.has(candidate.stage) &&
57
57
  typeof candidate.message === "string");
58
58
  }
59
+ function isValidDeferredUpdate(value) {
60
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
61
+ return false;
62
+ }
63
+ const candidate = value;
64
+ return (typeof candidate.version === "string" &&
65
+ typeof candidate.since === "string" &&
66
+ typeof candidate.updatedAt === "string" &&
67
+ [
68
+ "waiting_for_quiet",
69
+ "draining",
70
+ "drain_timeout",
71
+ "drain_unavailable",
72
+ "activity_unavailable",
73
+ ].includes(String(candidate.reason)) &&
74
+ (candidate.activeRequests === null ||
75
+ (typeof candidate.activeRequests === "number" &&
76
+ Number.isInteger(candidate.activeRequests) &&
77
+ candidate.activeRequests >= 0)));
78
+ }
59
79
  // ============================================
60
80
  // Exported Functions
61
81
  // ============================================
@@ -70,6 +90,7 @@ export function getDefaultUpdateState() {
70
90
  lastUpdateAt: null,
71
91
  lastUpdateVersion: null,
72
92
  pendingRestartVersion: null,
93
+ deferredUpdate: null,
73
94
  lastFailure: null,
74
95
  };
75
96
  }
@@ -106,6 +127,9 @@ export function loadUpdateState(stateFilePath) {
106
127
  pendingRestartVersion: typeof candidate.pendingRestartVersion === "string"
107
128
  ? candidate.pendingRestartVersion
108
129
  : null,
130
+ deferredUpdate: isValidDeferredUpdate(candidate.deferredUpdate)
131
+ ? candidate.deferredUpdate
132
+ : null,
109
133
  lastFailure: isValidLastFailure(candidate.lastFailure)
110
134
  ? candidate.lastFailure
111
135
  : null,
@@ -185,6 +209,7 @@ export function recordSuccessfulUpdate(version, stateFilePath) {
185
209
  state.lastUpdateAt = new Date().toISOString();
186
210
  state.lastUpdateVersion = version;
187
211
  state.pendingRestartVersion = null;
212
+ state.deferredUpdate = null;
188
213
  state.lastFailure = null;
189
214
  delete state.suppressedVersions[version];
190
215
  saveUpdateState(state, stateFilePath);
@@ -217,6 +242,32 @@ export function recordUpdateFailure(version, stage, message, stateFilePath) {
217
242
  };
218
243
  saveUpdateState(state, stateFilePath);
219
244
  }
245
+ /** Persist why a discovered update is waiting without treating it as a failure. */
246
+ export function recordUpdateDeferred(version, reason, activeRequests, stateFilePath) {
247
+ const state = loadUpdateState(stateFilePath) ?? getDefaultUpdateState();
248
+ const now = new Date().toISOString();
249
+ state.deferredUpdate = {
250
+ version,
251
+ since: state.deferredUpdate?.version === version
252
+ ? state.deferredUpdate.since
253
+ : now,
254
+ updatedAt: now,
255
+ reason,
256
+ activeRequests,
257
+ };
258
+ saveUpdateState(state, stateFilePath);
259
+ }
260
+ /** Clear matching updater deferral metadata once work can proceed. */
261
+ export function clearUpdateDeferral(version, stateFilePath) {
262
+ const state = loadUpdateState(stateFilePath);
263
+ if (!state?.deferredUpdate ||
264
+ (version !== undefined && state.deferredUpdate.version !== version)) {
265
+ return false;
266
+ }
267
+ state.deferredUpdate = null;
268
+ saveUpdateState(state, stateFilePath);
269
+ return true;
270
+ }
220
271
  /** Complete an updater-managed install after a manual or automatic restart. */
221
272
  export function reconcileRunningUpdate(runningVersion, stateFilePath) {
222
273
  const state = loadUpdateState(stateFilePath);
@@ -143,6 +143,7 @@ declare function handleAnthropicStreamingSuccessResponse(args: {
143
143
  attemptNumber: number;
144
144
  finalBodyStr: string;
145
145
  upstreamSpan?: import("@opentelemetry/api").Span;
146
+ logAttempt: AnthropicAttemptLogger;
146
147
  logProxyBody: ProxyBodyCaptureLogger;
147
148
  logFinalRequest: (status: number, accountLabel: string, accountType: string, errorType?: string, errorMessage?: string, extra?: {
148
149
  inputTokens?: number;
@@ -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();
@@ -3423,7 +3511,9 @@ function createClaudeRequestRuntimeContext(args) {
3423
3511
  }
3424
3512
  function createAnthropicAttemptLogger(args) {
3425
3513
  const { ctx, body, toolCount, requestStart, tracer, account, attemptNumber } = args;
3514
+ const attemptStartedAt = Date.now();
3426
3515
  return (status, errorType, errorMessage, extra) => {
3516
+ const attemptCompletedAt = Date.now();
3427
3517
  const traceCtx = tracer?.getTraceContext();
3428
3518
  logRequestAttempt({
3429
3519
  timestamp: new Date().toISOString(),
@@ -3437,7 +3527,8 @@ function createAnthropicAttemptLogger(args) {
3437
3527
  account: account.label,
3438
3528
  accountType: account.type,
3439
3529
  responseStatus: status,
3440
- responseTimeMs: Date.now() - requestStart,
3530
+ responseTimeMs: attemptCompletedAt - requestStart,
3531
+ attemptDurationMs: extra?.attemptDurationMs ?? attemptCompletedAt - attemptStartedAt,
3441
3532
  ...(errorType ? { errorType } : {}),
3442
3533
  ...(errorMessage ? { errorMessage } : {}),
3443
3534
  ...(extra?.errorCode ? { errorCode: extra.errorCode } : {}),
@@ -4095,10 +4186,16 @@ async function handleAnthropicRoutedClaudeRequest(args) {
4095
4186
  attemptNumber: loopState.attemptNumber,
4096
4187
  finalBodyStr: preparedAttempt.finalBodyStr,
4097
4188
  upstreamSpan,
4189
+ logAttempt,
4098
4190
  logProxyBody,
4099
4191
  logFinalRequest,
4100
4192
  });
4101
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
+ }
4102
4199
  continue accountLoop;
4103
4200
  }
4104
4201
  return successResult.response;
@@ -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;
@@ -460,7 +460,10 @@ export type RequestAttemptLogEntry = {
460
460
  account: string;
461
461
  accountType: string;
462
462
  responseStatus: number;
463
+ /** End-to-end request age when this attempt completed. */
463
464
  responseTimeMs: number;
465
+ /** Time spent in this specific account attempt. */
466
+ attemptDurationMs?: number;
464
467
  errorType?: string;
465
468
  errorMessage?: string;
466
469
  /** Low-level transport code such as ETIMEDOUT or EADDRNOTAVAIL. */
@@ -522,6 +525,8 @@ export type AnthropicAttemptLogger = (status: number, errorType?: string, errorM
522
525
  errorCode?: string;
523
526
  rateLimitKind?: "transient" | "quota";
524
527
  cooldownReason?: "transient" | "session" | "weekly" | "unified";
528
+ /** Override for nested retries whose attempt starts after this logger. */
529
+ attemptDurationMs?: number;
525
530
  /** Override used when one account selection performs an OAuth retry fetch. */
526
531
  attempt?: number;
527
532
  }) => void;
@@ -558,9 +563,40 @@ export type LoadedClaudeAccountContext = {
558
563
  };
559
564
  export type AnthropicSuccessResult = {
560
565
  retryNextAccount: true;
566
+ failure?: {
567
+ message: string;
568
+ rateLimit: boolean;
569
+ };
561
570
  } | {
562
571
  response: Response | unknown;
563
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
+ };
564
600
  export type AnthropicAuthRetryResult = {
565
601
  response?: Response | unknown;
566
602
  continueLoop: boolean;
@@ -810,6 +846,8 @@ export type ProxyReadinessState = {
810
846
  startTimeMs: number;
811
847
  acceptingConnections: boolean;
812
848
  ready: boolean;
849
+ /** True only while the updater is draining inference traffic. */
850
+ drainingForUpdate: boolean;
813
851
  readyAtMs?: number;
814
852
  };
815
853
  /** Structured response returned by the proxy /health endpoint. */
@@ -817,6 +855,7 @@ export type ProxyHealthResponse = {
817
855
  status: "ok" | "starting";
818
856
  ready: boolean;
819
857
  acceptingConnections: boolean;
858
+ drainingForUpdate: boolean;
820
859
  strategy: string;
821
860
  passthrough: boolean;
822
861
  version: string;
@@ -1025,6 +1064,124 @@ export type QueuedProxyLifecycleEvent = {
1025
1064
  date: string;
1026
1065
  record: Record<string, unknown>;
1027
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
+ };
1028
1185
  /** Request metadata retained by the HTTP adapter for terminal error logging. */
1029
1186
  export type RuntimeRequestMetadata = {
1030
1187
  requestId: string;
@@ -1034,6 +1191,8 @@ export type RuntimeRequestMetadata = {
1034
1191
  model: string;
1035
1192
  stream: boolean;
1036
1193
  toolCount: number;
1194
+ /** Admission decision captured before an updater drain can race the route. */
1195
+ rejectForUpdate?: boolean;
1037
1196
  terminalErrorType?: string;
1038
1197
  terminalErrorCode?: string;
1039
1198
  };
@@ -1201,6 +1360,14 @@ export type UpdateState = {
1201
1360
  lastUpdateVersion: string | null;
1202
1361
  /** Installed by the updater but not yet confirmed as the running version. */
1203
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;
1204
1371
  /** Last updater failure, retained until a successful update or replacement. */
1205
1372
  lastFailure: {
1206
1373
  at: string;
@@ -1209,6 +1376,26 @@ export type UpdateState = {
1209
1376
  message: string;
1210
1377
  } | null;
1211
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
+ };
1212
1399
  /** Result of validating a newly installed CLI through the stable trampoline. */
1213
1400
  export type InstalledVersionValidation = {
1214
1401
  version?: string;
@@ -0,0 +1,3 @@
1
+ import type { ProxyAnalysisOptions, ProxyAnalysisReport } from "../types/index.js";
2
+ /** Analyze local proxy logs without reading request or response body artifacts. */
3
+ export declare function analyzeProxyLogs(options?: ProxyAnalysisOptions): Promise<ProxyAnalysisReport>;